copy last stage button
This commit is contained in:
+101
-7
@@ -19,6 +19,8 @@
|
|||||||
let importAnimationDefinitionAction;
|
let importAnimationDefinitionAction;
|
||||||
let automaticImportAction;
|
let automaticImportAction;
|
||||||
let packFoldersAction;
|
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_PREVIEWS_PROPERTY = 'animationdirector_prop_previews';
|
||||||
const PROP_SLOTS = { left: 'propleft', right: 'propright', floor: 'propfloor' };
|
const PROP_SLOTS = { left: 'propleft', right: 'propright', floor: 'propfloor' };
|
||||||
let propAction, propProjectProperty, propDialog, propListener;
|
let propAction, propProjectProperty, propDialog, propListener;
|
||||||
@@ -2467,6 +2469,76 @@
|
|||||||
return { groups, ignored };
|
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) {
|
function cleanSoundPreviewFields(clip) {
|
||||||
for (const cues of Object.values(clip?.sound_effects || {})) {
|
for (const cues of Object.values(clip?.sound_effects || {})) {
|
||||||
for (const cue of Array.isArray(cues) ? cues : [cues]) {
|
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',
|
version: '2.0.0',
|
||||||
variant: 'both',
|
variant: 'both',
|
||||||
onload() {
|
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') {
|
if (typeof Property === 'function' && typeof KeyframeDataPoint !== 'undefined') {
|
||||||
soundPreviewProperty = new Property(KeyframeDataPoint, 'string', SOUND_PREVIEW_PROPERTY, {
|
soundPreviewProperty = new Property(KeyframeDataPoint, 'string', SOUND_PREVIEW_PROPERTY, {
|
||||||
exposed: false, condition: (point) => point.keyframe.channel === 'sound',
|
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', {
|
replaceActorAction = new Action('replace_actor_gecko_json', {
|
||||||
name: 'Replace Actor Model',
|
name: 'Replace Actor Model',
|
||||||
description: 'Replace one existing actor model while preserving its actorN_ prefix, animations, and preferences.',
|
description: 'Replace one existing actor model while preserving its actorN_ prefix, animations, and preferences.',
|
||||||
icon: 'published_with_changes',
|
icon: 'swap_horiz',
|
||||||
click: replaceActorGeckoJson,
|
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', {
|
actorPreferencesAction = new Action('edit_actor_preferences', {
|
||||||
name: 'Actor Preferences',
|
name: 'Actor Preferences',
|
||||||
description: 'Set project-specific export names, preview textures, and animation-definition metadata for each actor.',
|
description: 'Set project-specific export names, preview textures, and animation-definition metadata for each actor.',
|
||||||
icon: 'badge',
|
icon: 'psychology',
|
||||||
click: editActorPreferences,
|
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', {
|
importAnimationDefinitionAction = new Action('import_afw_animation_definition', {
|
||||||
name: 'Import Animation Definition',
|
name: 'Import Animation Definition',
|
||||||
description: 'Load generator fields and block requirement guides from an existing animdef JSON.',
|
description: 'Load generator fields and block requirement guides from an existing animdef JSON.',
|
||||||
icon: 'file_open',
|
icon: 'assignment_add',
|
||||||
click: importAnimationDefinition,
|
click: importAnimationDefinition,
|
||||||
});
|
});
|
||||||
automaticImportAction = new Action('animationdirector_automatic_import', {
|
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.',
|
description: 'Import an animation definition, actor models and all stages from extracted packs into a new project.',
|
||||||
click: automaticallyImportAnimation,
|
click: automaticallyImportAnimation,
|
||||||
});
|
});
|
||||||
@@ -5912,14 +6000,18 @@ You can import or export complete AFW animation IDs as one file containing all s
|
|||||||
icon: 'movie',
|
icon: 'movie',
|
||||||
children: [
|
children: [
|
||||||
automaticImportAction,
|
automaticImportAction,
|
||||||
{ id: 'animationdirector_manual_import', name: 'Manual Import', icon: 'file_open', children: [
|
importAction,
|
||||||
importAction, importConjoinedAnimationAction, importAnimationDefinitionAction, packFoldersAction,
|
importConjoinedAnimationAction,
|
||||||
] },
|
importAnimationDefinitionAction,
|
||||||
|
packFoldersAction,
|
||||||
|
'_',
|
||||||
actorPreferencesAction,
|
actorPreferencesAction,
|
||||||
replaceActorAction,
|
replaceActorAction,
|
||||||
|
'_',
|
||||||
blockRequirementAction,
|
blockRequirementAction,
|
||||||
propAction,
|
propAction,
|
||||||
soundLibraryAction,
|
soundLibraryAction,
|
||||||
|
'_',
|
||||||
createAnimationDefinitionAction,
|
createAnimationDefinitionAction,
|
||||||
exportConjoinedAction,
|
exportConjoinedAction,
|
||||||
],
|
],
|
||||||
@@ -5934,6 +6026,8 @@ You can import or export complete AFW animation IDs as one file containing all s
|
|||||||
MenuBar.update();
|
MenuBar.update();
|
||||||
},
|
},
|
||||||
onunload() {
|
onunload() {
|
||||||
|
for (const event of POSE_BUTTON_EVENTS) Blockbench.removeListener(event, refreshPreviousStagePoseButton);
|
||||||
|
previousStagePoseAction?.delete(); previousStagePoseAction = undefined;
|
||||||
Blockbench.removeListener('timeline_play', figuraSoundPlayListener);
|
Blockbench.removeListener('timeline_play', figuraSoundPlayListener);
|
||||||
removeFiguraSoundCompat();
|
removeFiguraSoundCompat();
|
||||||
for (const event of SOUND_EVENTS) Blockbench.removeListener(event, soundLibraryListener);
|
for (const event of SOUND_EVENTS) Blockbench.removeListener(event, soundLibraryListener);
|
||||||
|
|||||||
@@ -105,12 +105,11 @@ test('animation menu orders import and creation actions and has no deprecated sp
|
|||||||
const h = harness();
|
const h = harness();
|
||||||
h.context.registration.onload();
|
h.context.registration.onload();
|
||||||
const menu = h.menus.find((entry) => entry.id === 'animationdirector_anims');
|
const menu = h.menus.find((entry) => entry.id === 'animationdirector_anims');
|
||||||
assert.deepEqual(Array.from(menu.children, (action) => action.name), [
|
assert.deepEqual(Array.from(menu.children, (action) => action === '_' ? '_' : action.name), [
|
||||||
'Automatically Import Animation', 'Manual Import', 'Actor Preferences',
|
'Automatically Import Animation', 'Import Actor', 'Import Animation', 'Import Animation Definition', 'Pack Folders',
|
||||||
'Replace Actor Model', 'Block Requirements', 'Item Props', 'NoN Sound Library', 'Create Animation Definition', 'Export Animations',
|
'_', 'Actor Preferences', 'Replace Actor Model',
|
||||||
]);
|
'_', 'Block Requirements', 'Item Props', 'NoN Sound Library',
|
||||||
assert.deepEqual(Array.from(menu.children[1].children, (action) => action.name), [
|
'_', 'Create Animation Definition', 'Export Animations',
|
||||||
'Import Actor', 'Import Animation', 'Import Animation Definition', 'Pack Folders',
|
|
||||||
]);
|
]);
|
||||||
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8');
|
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8');
|
||||||
assert.doesNotMatch(source, /importSplitActorAnimation|buildSplitImportFile|analyzeSplitAnimationFiles|exportSelectedActorAnimation|exportAllActorAnimations|animationdirector_deprecated/);
|
assert.doesNotMatch(source, /importSplitActorAnimation|buildSplitImportFile|analyzeSplitAnimationFiles|exportSelectedActorAnimation|exportAllActorAnimations|animationdirector_deprecated/);
|
||||||
|
|||||||
Reference in New Issue
Block a user