This commit is contained in:
2026-09-29 14:54:06 +02:00
parent f7c0c38ec2
commit 5cb0ba631e
6 changed files with 462 additions and 166 deletions
+39 -1
View File
@@ -12,7 +12,8 @@ function harness(capture = false) {
const source = plugin.replace(' Plugin.register(PLUGIN_ID, {', `
globalThis.api = { desktopFiles, importFilePath, insidePack, importResourceId, readImportPack, findImportPack, importPackRegistry,
resolveImportModel, importModelCandidates, companionImportFiles, definitionActorKeys, defaultImportClipRoles,
validateImportGeometry, automaticallyImportAnimation, loadImportTextures, commitAutomaticImport, resolveDefinitionImportStages };
validateImportGeometry, automaticallyImportAnimation, loadImportTextures, commitAutomaticImport, resolveDefinitionImportStages,
createImportAssetLookup, importGeometryIntoCurrentProject };
Plugin.register(PLUGIN_ID, {`).replace(' function commitAutomaticImport(plan) {',
` function commitAutomaticImport(plan) { ${capture ? 'globalThis.importedPlan = plan; return;' : ''}`);
const storage = new Map(), dialogs = [], errors = [], undo = [], messages = [];
@@ -38,6 +39,7 @@ function harness(capture = false) {
Formats: { geckolib_model: { id: 'geckolib_model' } },
newProject(format) {
assert.equal(format.id, 'geckolib_model');
context.Format = format;
context.Project = { uuid: 'new-project' };
return true;
},
@@ -530,3 +532,39 @@ test('unexpected animation-load failures cancel the batch rather than keeping a
definition: {}, imported: { fileObj: {}, stageNames: [] } }), /load failed/);
assert.deepEqual(h.undo.map((entry) => entry.type), ['begin', 'cancel']);
});
test('manual replacement rejects duplicate and cyclic bones before touching the old actor', () => {
const h = harness(), original = new h.context.Group({ name: 'actor1_body' }); original.init();
for (const bones of [[{ name: 'body' }, { name: 'body' }], [{ name: 'body', parent: 'head' }, { name: 'head', parent: 'body' }]]) {
assert.throws(() => h.importGeometryIntoCurrentProject({ bones }, 1, true), /unique|Cyclic/);
assert.equal(h.context.Group.all[0], original);
assert.equal(h.undo.length, 0);
}
});
test('manual geometry import cancels its undo transaction when model creation fails', () => {
const h = harness();
h.context.Group.prototype.init = () => { throw new Error('model creation failed'); };
assert.throws(() => h.importGeometryIntoCurrentProject({ bones: [{ name: 'body' }] }, 1), /model creation failed/);
assert.deepEqual(h.undo.map((entry) => entry.type), ['begin', 'cancel']);
});
test('import asset lookup opens sources lazily and reuses positive and negative results', (t) => {
const f = fixtures(t), h = harness(), first = f.makePack('first'), second = f.makePack('second');
const relative = 'assets/test/textures/demo.png', png = Buffer.alloc(24);
Buffer.from('89504e470d0a1a0a', 'hex').copy(png); png.writeUInt32BE(32, 16); png.writeUInt32BE(64, 20);
const file = path.join(first.root, relative); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, png);
let secondOpened = 0, textureReads = 0;
const counted = { path, fs: { ...fs, statSync(file, ...args) {
if (file === second.root) secondOpened++;
return fs.statSync(file, ...args);
}, readFileSync(file, ...args) { if (file.endsWith('demo.png')) textureReads++; return fs.readFileSync(file, ...args); } } };
const assets = h.createImportAssetLookup(counted, [first.root, second.root]);
assert.equal(assets.read(relative).dimensions.width, 32);
assert.equal(secondOpened, 0);
assets.read(relative); assert.equal(textureReads, 1);
assert.equal(assets.read('assets/test/textures/missing.png'), null);
const opened = secondOpened;
assert.equal(assets.read('assets/test/textures/missing.png'), null);
assert.equal(secondOpened, opened);
});