improved and fixed stuff

This commit is contained in:
2026-09-28 21:55:40 +02:00
parent 2486c3cb63
commit 8d1346986b
3 changed files with 90 additions and 4 deletions
+38 -2
View File
@@ -2686,12 +2686,16 @@
const stageNumbers = new Set(analysis.stages.map((stage) => stage.stageNumber));
const seenStages = new Set();
for (const stage of definition.stages) {
if (!isObject(stage) || !Number.isInteger(stage.stage) || !stageNumbers.has(stage.stage) || seenStages.has(stage.stage)) {
if (!isObject(stage) || !Number.isInteger(stage.stage) || stage.stage < 1 || seenStages.has(stage.stage)) {
throw new Error('Imported stages must have unique stage numbers matching the target animation.');
}
const playback = stage.use_stage ?? stage.stage;
if (!Number.isInteger(playback) || !stageNumbers.has(playback)) {
throw new Error(`Stage p${stage.stage} requires animation clips for playback stage p${playback}.`);
}
seenStages.add(stage.stage);
}
if (seenStages.size !== stageNumbers.size) throw new Error('The definition must include every stage of the target animation.');
if ([...stageNumbers].some((number) => !seenStages.has(number))) throw new Error('The definition must include every stage of the target animation.');
const roles = analysis.actorIndices.map((index) => analysis.actorLabels.get(index));
const labels = new Set();
@@ -2797,6 +2801,11 @@
const saved = getSavedAnimdef(group.id);
const savedActors = new Map((Array.isArray(saved.actors) ? saved.actors : []).map((actor) => [actor.label, actor]));
const savedStages = new Map((Array.isArray(saved.stages) ? saved.stages : []).map((stage) => [stage.stage, stage]));
const editableStages = new Map(analysis.stages.map((stage) => [stage.stageNumber, stage]));
const reusedStages = [...savedStages.values()].filter((stage) => !editableStages.has(stage.stage)
&& editableStages.has(stage.use_stage)).map((stage) => ({ ...editableStages.get(stage.use_stage),
stageNumber: stage.stage, loop: stage.loop ?? editableStages.get(stage.use_stage).loop }));
analysis = { ...analysis, stages: [...analysis.stages, ...reusedStages].sort((a, b) => a.stageNumber - b.stageNumber) };
const savedSystemTags = uniqueTrimmedStrings((Array.isArray(saved.system_tags) ? saved.system_tags : [])
.map((tag) => String(tag).toLowerCase()));
const savedTag = savedSystemTags[0] || '';
@@ -3592,6 +3601,32 @@
return combined;
}
function resolveDefinitionImportStages(parsed, definition) {
if (!Array.isArray(definition.stages) || !definition.stages.length) {
throw new Error('The definition must contain at least one stage.');
}
const numbers = new Set();
for (const stage of definition.stages) {
if (!stage || !Number.isInteger(stage.stage) || stage.stage < 1 || numbers.has(stage.stage)) {
throw new Error('Definition stages must have unique positive stage numbers.');
}
numbers.add(stage.stage);
}
const clips = new Map(parsed.stages.map((stage) => [stage.stageNumber, stage]));
for (const stage of definition.stages) {
const playback = stage.use_stage ?? stage.stage;
if (!Number.isInteger(playback) || !numbers.has(playback)) {
throw new Error(`Stage p${stage.stage} references missing playback stage p${playback}.`);
}
// NoN references the target's clips directly, not its own use_stage setting.
const source = clips.get(playback);
if (!source) throw new Error(`Stage p${stage.stage} requires animation clips for playback stage p${playback}.`);
}
// Reused stages remain definition metadata, not duplicate editable animations.
return definition.stages.filter((stage) => stage.use_stage == null || stage.use_stage === stage.stage)
.map((stage) => clips.get(stage.stage)).sort((a, b) => a.stageNumber - b.stageNumber);
}
function buildConjoinedImportFile(parsed, outputAnimationId, actorMapping) {
const animations = {};
const stageNames = [];
@@ -4239,6 +4274,7 @@
parsed.stages = parsed.stages.map((stage) => ({ ...stage,
entries: stage.entries.filter((entry) => selectedLabels.has(entry.actorLabel))
})).filter((stage) => stage.entries.length);
parsed.stages = resolveDefinitionImportStages(parsed, definition);
// Preserve role references while relabeling the definition for the selected asset clips.
const relabeled = clone(definition);
relabeled.actors.forEach((actor, index) => { actor.label ||= keys[index]; });
+18 -1
View File
@@ -43,7 +43,7 @@ function createHarness() {
context.adTest.showAnimationDefinitionReviewDialog(group, analysis);
return dialogs.at(-1);
}
return { open, exports, errors, project };
return { open, exports, errors, project, analysis };
}
function defaults(form) {
@@ -52,6 +52,23 @@ function defaults(form) {
.map(([key, field]) => [key, field.value]));
}
test('metadata-only playback stages remain editable and survive definition export', () => {
const h = createHarness();
h.analysis.stages = h.analysis.stages.slice(0, 1);
h.project.animationdirector_animdef_definitions = JSON.stringify({ test_animation: {
actors: [{ label: 'player', entity_types: ['minecraft:player'] }, { label: 'wolf', entity_types: ['minecraft:wolf'] }],
stages: [{ stage: 1, loop: true }, { stage: 2, use_stage: 1, speed: 0.8, non_peak: true, escapable: false }],
} });
const dialog = h.open();
assert.equal(dialog.form.stub_stage_2_use_stage.value, '1');
dialog.onConfirm(defaults(dialog.form));
assert.deepEqual(h.errors, []);
assert.equal(h.exports[0].stages.length, 2);
assert.equal(h.exports[0].stages[1].use_stage, 1);
assert.equal(h.exports[0].stages[1].speed, 0.8);
assert.equal(h.analysis.stages.length, 1);
});
test('generates extended animdef fields and restores them on reopen', () => {
const harness = createHarness();
const dialog = harness.open();
+34 -1
View File
@@ -12,7 +12,7 @@ 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 };
validateImportGeometry, automaticallyImportAnimation, loadImportTextures, commitAutomaticImport, resolveDefinitionImportStages };
Plugin.register(PLUGIN_ID, {`).replace(' function commitAutomaticImport(plan) {',
` function commitAutomaticImport(plan) { ${capture ? 'globalThis.importedPlan = plan; return;' : ''}`);
const storage = new Map(), dialogs = [], errors = [], undo = [], messages = [];
@@ -309,6 +309,39 @@ test('a definition actor without a distinct clip still requires a valid mapping'
assert.equal(h.context.Project.uuid, 'original');
});
test('automatic import retains use_stage metadata without duplicating editor animations', async (t) => {
const f = fixtures(t), h = harness(true), source = f.makePack('source');
const file = f.write(path.join(source.root, 'data/test/afw_animdefs/example.json'), {
actors: [{ label: 'wolf', entity_types: ['minecraft:wolf'] }],
stages: [{ stage: 1, escapable: true }, { stage: 2, use_stage: 1, speed: 0.8, non_peak: true, escapable: false }],
});
f.write(path.join(source.root, 'assets/test/animations/afw/example.animation.json'), { animations: {
p1_wolf: { animation_length: 1, bones: { body: { rotation: [1, 2, 3] } },
sound_effects: { '0': { effect: 'example' } } },
} });
f.model(source, 'test:source/entity/wolf.m');
h.context.selectedFile = { path: file };
await h.automaticallyImportAnimation();
assert.deepEqual(h.errors, []);
const plan = h.context.importedPlan;
assert.deepEqual(Array.from(plan.imported.stageNames), ['example.p1']);
const clips = plan.imported.fileObj.animations;
assert.equal(clips['example.p2'], undefined);
assert.ok(clips['example.p1'].bones.actor1_body);
assert.equal(plan.definition.stages[1].use_stage, 1);
assert.equal(plan.definition.stages[1].speed, 0.8);
assert.equal(plan.definition.stages[1].non_peak, true);
assert.equal(plan.definition.stages[1].escapable, false);
});
test('playback references require declared stages and physical clips, not recursive aliases', () => {
const h = harness();
const parsed = { stages: [{ stageNumber: 1, entries: [{ actorLabel: 'wolf', key: 'p1_wolf', animObj: { bones: {} } }] }] };
assert.throws(() => h.resolveDefinitionImportStages(parsed, { stages: [{ stage: 1, use_stage: 9 }] }), /missing playback stage p9/);
assert.throws(() => h.resolveDefinitionImportStages(parsed, { stages: [{ stage: 1 }, { stage: 1 }] }), /unique positive/);
assert.throws(() => h.resolveDefinitionImportStages(parsed, { stages: [{ stage: 1, use_stage: 2 }, { stage: 2, use_stage: 1 }] }), /requires animation clips.*p2/);
});
test('canceling actor review leaves the open project and saved folders unchanged', async (t) => {
const f = fixtures(t), h = harness(true), source = f.makePack('source');
const file = f.write(path.join(source.root, 'data/test/afw_animdefs/example.json'), {