copy first keyframe button
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user