copy last stage button

This commit is contained in:
2026-09-29 13:19:39 +02:00
parent 3489315611
commit 94b3145254
2 changed files with 106 additions and 13 deletions
+101 -7
View File
@@ -19,6 +19,8 @@
let importAnimationDefinitionAction;
let automaticImportAction;
let packFoldersAction;
let previousStagePoseAction;
const POSE_BUTTON_EVENTS = ['select_animation', 'select_project', 'unselect_project', 'load_project', 'undo', 'redo', 'add_animation', 'remove_animation'];
const PROP_PREVIEWS_PROPERTY = 'animationdirector_prop_previews';
const PROP_SLOTS = { left: 'propleft', right: 'propright', floor: 'propfloor' };
let propAction, propProjectProperty, propDialog, propListener;
@@ -2467,6 +2469,76 @@
return { groups, ignored };
}
function previousStagePoseSource() {
if (typeof Format === 'undefined' || Format.id !== 'geckolib_model') return { reason: 'Open a GeckoLib animation project first.' };
const current = parseAfwStageAnimationName(Animation.selected);
if (!current) return { reason: 'Select an animation stage named animationID.pN.' };
const stages = getSavedAnimdef(current.id).stages || [];
const candidates = [...Animation.all.map(parseAfwStageAnimationName).filter((stage) => stage?.id === current.id),
...stages.map((stage) => ({ stageNumber: stage.stage }))].filter((stage) => stage.stageNumber < current.stageNumber);
if (!candidates.length) return { reason: 'This animation has no previous stage.' };
const previous = Math.max(...candidates.map((stage) => stage.stageNumber));
const playback = stages.find((stage) => stage.stage === previous)?.use_stage ?? previous;
const matches = Animation.all.filter((animation) => {
const stage = parseAfwStageAnimationName(animation);
return stage?.id === current.id && stage.stageNumber === playback;
});
if (matches.length !== 1) return { reason: matches.length ? 'The previous stage has duplicate animations.' : `Previous stage p${previous} uses missing animation p${playback}.` };
if (matches[0] === Animation.selected) return { reason: 'The previous stage reuses the current animation.' };
return { source: matches[0], previous };
}
function refreshPreviousStagePoseButton() {
if (!previousStagePoseAction) return;
const { reason } = previousStagePoseSource();
const description = reason || 'Copy the final transforms of all actors from the previous stage into time 0. Existing time-0 transforms are replaced.';
previousStagePoseAction.description = description;
for (const node of previousStagePoseAction.nodes || []) {
node.classList.toggle('disabled', !!reason);
node.setAttribute('aria-disabled', String(!!reason));
const label = node.querySelector('.tooltip_description');
if (label) label.textContent = description;
}
}
function copyPreviousStagePose() {
const { source, reason } = previousStagePoseSource();
if (reason) { Blockbench.showQuickMessage(reason, 2500); return; }
const target = Animation.selected, poses = [];
const originalTime = Timeline.time;
try {
Timeline.time = source.length;
for (const group of Group.all) {
if (readActorIndexFromName(group.name) === null || isBlockRequirementGuide(group)) continue;
const animator = source.animators[group.uuid];
Animator.resetLastValues();
for (const channel of ['position', 'rotation', 'scale']) {
const value = animator?.[channel]?.length ? animator.interpolate(channel, false) : null;
const values = value || (channel === 'scale' ? [1, 1, 1] : [0, 0, 0]);
if (values.length !== 3 || !values.every(Number.isFinite)) throw new Error(`Cannot evaluate ${group.name} ${channel} at the previous stage endpoint.`);
poses.push({ group, channel, values });
}
}
} finally {
Timeline.time = originalTime;
Animator.resetLastValues();
}
if (!poses.length) { Blockbench.showQuickMessage('There are no actor bones to copy.', 2500); return; }
const keyframes = poses.flatMap(({ group, channel }) => (target.animators[group.uuid]?.[channel] || []).filter((frame) => frame.time === 0));
Undo.initEdit({ animations: [target], keyframes });
for (const { group, channel, values } of poses) {
const animator = target.getBoneAnimator(group);
const frame = animator[channel].find((frame) => frame.time === 0);
const data = { interpolation: 'linear', uniform: false, data_points: [{ x: values[0], y: values[1], z: values[2] }] };
if (frame) frame.extend(data);
else keyframes.push(animator.createKeyframe(data, 0, channel, false, false));
}
Undo.finishEdit('Copy previous stage pose', { animations: [target], keyframes });
Animator.preview();
if (typeof updateKeyframeSelection === 'function') updateKeyframeSelection();
Blockbench.showQuickMessage('Copied previous stage pose to time 0.', 2500);
}
function cleanSoundPreviewFields(clip) {
for (const cues of Object.values(clip?.sound_effects || {})) {
for (const cue of Array.isArray(cues) ? cues : [cues]) {
@@ -5715,6 +5787,22 @@ You can import or export complete AFW animation IDs as one file containing all s
version: '2.0.0',
variant: 'both',
onload() {
previousStagePoseAction = new Action('animationdirector_previous_stage_pose', {
name: 'Copy Previous Stage Pose', icon: 'move_down', color: '#b8a6cc', category: 'animation',
description: 'Copy the final actor pose of the previous stage into time 0.',
click() {
try { copyPreviousStagePose(); }
catch (error) { Blockbench.showMessageBox({ title: 'Copy Previous Stage Pose', message: error.message }); }
},
});
if (typeof Toolbars !== 'undefined' && Toolbars.timeline) {
const toolbar = Toolbars.timeline;
const marker = toolbar.children.findIndex((item) => item === 'add_marker' || item?.id === 'add_marker');
toolbar.add(previousStagePoseAction, marker >= 0 ? marker + 1 : 0);
}
for (const event of POSE_BUTTON_EVENTS) Blockbench.on(event, refreshPreviousStagePoseButton);
previousStagePoseAction.node?.addEventListener('mouseenter', refreshPreviousStagePoseButton);
refreshPreviousStagePoseButton();
if (typeof Property === 'function' && typeof KeyframeDataPoint !== 'undefined') {
soundPreviewProperty = new Property(KeyframeDataPoint, 'string', SOUND_PREVIEW_PROPERTY, {
exposed: false, condition: (point) => point.keyframe.channel === 'sound',
@@ -5833,7 +5921,7 @@ You can import or export complete AFW animation IDs as one file containing all s
replaceActorAction = new Action('replace_actor_gecko_json', {
name: 'Replace Actor Model',
description: 'Replace one existing actor model while preserving its actorN_ prefix, animations, and preferences.',
icon: 'published_with_changes',
icon: 'swap_horiz',
click: replaceActorGeckoJson,
});
@@ -5847,7 +5935,7 @@ You can import or export complete AFW animation IDs as one file containing all s
actorPreferencesAction = new Action('edit_actor_preferences', {
name: 'Actor Preferences',
description: 'Set project-specific export names, preview textures, and animation-definition metadata for each actor.',
icon: 'badge',
icon: 'psychology',
click: editActorPreferences,
});
@@ -5882,11 +5970,11 @@ You can import or export complete AFW animation IDs as one file containing all s
importAnimationDefinitionAction = new Action('import_afw_animation_definition', {
name: 'Import Animation Definition',
description: 'Load generator fields and block requirement guides from an existing animdef JSON.',
icon: 'file_open',
icon: 'assignment_add',
click: importAnimationDefinition,
});
automaticImportAction = new Action('animationdirector_automatic_import', {
name: 'Automatically Import Animation', icon: 'auto_awesome',
name: 'Automatically Import Animation', icon: 'add_box',
description: 'Import an animation definition, actor models and all stages from extracted packs into a new project.',
click: automaticallyImportAnimation,
});
@@ -5912,14 +6000,18 @@ You can import or export complete AFW animation IDs as one file containing all s
icon: 'movie',
children: [
automaticImportAction,
{ id: 'animationdirector_manual_import', name: 'Manual Import', icon: 'file_open', children: [
importAction, importConjoinedAnimationAction, importAnimationDefinitionAction, packFoldersAction,
] },
importAction,
importConjoinedAnimationAction,
importAnimationDefinitionAction,
packFoldersAction,
'_',
actorPreferencesAction,
replaceActorAction,
'_',
blockRequirementAction,
propAction,
soundLibraryAction,
'_',
createAnimationDefinitionAction,
exportConjoinedAction,
],
@@ -5934,6 +6026,8 @@ You can import or export complete AFW animation IDs as one file containing all s
MenuBar.update();
},
onunload() {
for (const event of POSE_BUTTON_EVENTS) Blockbench.removeListener(event, refreshPreviousStagePoseButton);
previousStagePoseAction?.delete(); previousStagePoseAction = undefined;
Blockbench.removeListener('timeline_play', figuraSoundPlayListener);
removeFiguraSoundCompat();
for (const event of SOUND_EVENTS) Blockbench.removeListener(event, soundLibraryListener);