copy first keyframe button

This commit is contained in:
2026-09-29 13:45:53 +02:00
parent 9920f1da4c
commit 4735c33e6e
3 changed files with 129 additions and 0 deletions
+77
View File
@@ -21,6 +21,8 @@
let packFoldersAction;
let previousStagePoseAction;
let loopPoseAction;
let autoplayStagesAction, autoplayLoopOriginal, autoplayLoopWrapper;
let autoplayTransitioning = false;
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' };
@@ -2489,6 +2491,63 @@
return { source: matches[0], previous };
}
function nextAutoplayStage(animation) {
const current = parseAfwStageAnimationName(animation);
if (!current) return null;
const stages = Animation.all.map(parseAfwStageAnimationName)
.filter((stage) => stage?.id === current.id && stage.stageNumber > current.stageNumber)
.sort((a, b) => a.stageNumber - b.stageNumber);
if (!stages.length || stages.filter((stage) => stage.stageNumber === stages[0].stageNumber).length !== 1) return null;
return stages[0].animation;
}
function updateAutoplayIconColor() {
if (!autoplayStagesAction) return;
const color = autoplayStagesAction.value ? '#000000' : '#b8a6cc';
autoplayStagesAction.color = color;
if (autoplayStagesAction.icon_node) autoplayStagesAction.icon_node.style.color = color;
for (const node of autoplayStagesAction.nodes || []) {
const icon = node.querySelector('.icon');
if (icon) icon.style.color = color;
}
}
function installStageAutoplay() {
autoplayLoopOriginal = Timeline.loop;
// Native playback resets once-clips before emitting pause. Observe the completed
// playback step instead of treating a user's pause as a stage transition.
autoplayLoopWrapper = function (...args) {
// start() invokes loop() immediately. Defer that nested step to the next
// normal playback frame, but render the new pose synchronously below.
if (autoplayTransitioning) return;
const animation = Animation.selected;
const eligible = autoplayStagesAction?.value && Format.id === 'geckolib_model'
&& Timeline.playing && animation?.length > 0 && animation.loop !== 'loop'
&& !(Timeline.custom_range?.[0] || Timeline.custom_range?.[1]);
const result = autoplayLoopOriginal.apply(this, args);
if (eligible && autoplayStagesAction?.value && Animation.selected === animation && !Timeline.playing) {
const next = nextAutoplayStage(animation);
if (next) {
autoplayTransitioning = true;
try {
Timeline.setTime(0);
next.select();
Timeline.start();
Animator.preview(true);
} finally {
autoplayTransitioning = false;
}
}
}
return result;
};
Timeline.loop = autoplayLoopWrapper;
}
function removeStageAutoplay() {
if (Timeline.loop === autoplayLoopWrapper) Timeline.loop = autoplayLoopOriginal;
}
function refreshPreviousStagePoseButton() {
if (!previousStagePoseAction) return;
const { reason } = previousStagePoseSource();
@@ -5822,6 +5881,22 @@ You can import or export complete AFW animation IDs as one file containing all s
version: '2.0.0',
variant: 'both',
onload() {
autoplayStagesAction = new Toggle('animationdirector_autoplay_stages', {
name: 'Autoplay Stages', icon: 'resume', color: '#b8a6cc', category: 'animation',
description: 'Automatically play the next stage when a non-looping stage finishes. Looping stages stay on their current clip.',
default: false, save_on_restart: true,
onChange: updateAutoplayIconColor,
condition: () => typeof Format !== 'undefined' && Format.id === 'geckolib_model',
});
if (typeof Toolbars !== 'undefined') {
const toolbar = Toolbars.animations;
if (toolbar) {
const length = toolbar.children.findIndex((item) => item === 'slider_animation_length' || item?.id === 'slider_animation_length');
toolbar.add(autoplayStagesAction, length >= 0 ? length + 1 : 0);
}
}
updateAutoplayIconColor();
installStageAutoplay();
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.',
@@ -6073,6 +6148,8 @@ You can import or export complete AFW animation IDs as one file containing all s
MenuBar.update();
},
onunload() {
removeStageAutoplay();
autoplayStagesAction?.delete(); autoplayStagesAction = undefined;
for (const event of [...POSE_BUTTON_EVENTS, 'update_animation_length']) Blockbench.removeListener(event, refreshLoopPoseButton);
loopPoseAction?.delete(); loopPoseAction = undefined;
for (const event of POSE_BUTTON_EVENTS) Blockbench.removeListener(event, refreshPreviousStagePoseButton);