feat(studio): keyframe hooks wiring — session, cache, toolbar [5/6] (#1171)

* feat(core): GSAP keyframe parsing, mutations, and API routes

* feat(core): spring physics solver + runtime fixes + spring ease editor

* feat(core): spring physics solver + runtime fixes + spring ease editor

Revert totalTime nudge that caused black first frames in from() tweens.
Keep stale CSS offset cleanup. Regenerate baselines for offset cleanup.

* ci: trigger regression run

* fix(producer): use video stream duration for PSNR checkpoint range

The regression harness used container duration (format.duration) to
compute PSNR checkpoints. Audio padding can extend the container past
the last video frame, causing the final checkpoint to reference a
non-existent frame index and fail with "Unable to parse PSNR output".

Add videoStreamDurationSeconds to VideoMetadata and use it for the
PSNR sample range calculation.

* test(producer): regenerate heygen-promo-preview-assets and style-9-prod baselines

Baselines regenerated inside Dockerfile.test on the devbox to match
the current runtime init.ts changes. Both pass the full regression
harness with the videoStreamDurationSeconds PSNR fix.

* test(producer): allow 2-frame PSNR tolerance for style-9-prod

A single transition frame at 10.742s renders with marginal PSNR
(26.6 dB vs 30 threshold) on CI runners but passes on the devbox
Docker image. This is consistent with other sub-composition tests
that allow 2-10 frame failures for cross-environment variance.

* feat(studio): GSAP runtime bridge + optimistic update pattern

* feat(studio): keyframe diamonds, navigation controls, context menu

* feat(studio): keyframe hooks wiring — session, commits, cache, toolbar toggle
This commit is contained in:
Miguel Ángel
2026-06-05 11:51:52 -04:00
committed by GitHub
parent 5984c58846
commit 7a0883264d
7 changed files with 607 additions and 19 deletions
+35 -1
View File
@@ -77,6 +77,9 @@ interface UseAppHotkeysParams {
handleCopy: () => boolean;
handlePaste: () => Promise<void>;
handleCut: () => Promise<boolean>;
onResetKeyframes: () => boolean;
onDeleteSelectedKeyframes: () => void;
onAfterUndoRedo?: () => void;
}
// ── Hook ──
@@ -98,6 +101,9 @@ export function useAppHotkeys({
handleCopy,
handlePaste,
handleCut,
onResetKeyframes,
onDeleteSelectedKeyframes,
onAfterUndoRedo,
}: UseAppHotkeysParams) {
const previewHotkeyWindowRef = useRef<Window | null>(null);
const handleAppKeyDownRef = useRef<((event: KeyboardEvent) => void) | undefined>(undefined);
@@ -144,6 +150,7 @@ export function useAppHotkeys({
return;
}
if (result.ok && result.label) {
onAfterUndoRedo?.();
await syncHistoryPreviewAfterApply(result.paths);
showToast(`Undid ${result.label}`, "info");
}
@@ -154,6 +161,7 @@ export function useAppHotkeys({
syncHistoryPreviewAfterApply,
waitForPendingDomEditSaves,
writeHistoryProjectFile,
onAfterUndoRedo,
]);
const handleRedo = useCallback(async () => {
@@ -167,6 +175,7 @@ export function useAppHotkeys({
return;
}
if (result.ok && result.label) {
onAfterUndoRedo?.();
await syncHistoryPreviewAfterApply(result.paths);
showToast(`Redid ${result.label}`, "info");
}
@@ -177,6 +186,7 @@ export function useAppHotkeys({
syncHistoryPreviewAfterApply,
waitForPendingDomEditSaves,
writeHistoryProjectFile,
onAfterUndoRedo,
]);
// ── Stable refs for the consolidated keydown handler ──
@@ -197,6 +207,10 @@ export function useAppHotkeys({
handlePasteRef.current = handlePaste;
const handleCutRef = useRef(handleCut);
handleCutRef.current = handleCut;
const onResetKeyframesRef = useRef(onResetKeyframes);
onResetKeyframesRef.current = onResetKeyframes;
const onDeleteSelectedKeyframesRef = useRef(onDeleteSelectedKeyframes);
onDeleteSelectedKeyframesRef.current = onDeleteSelectedKeyframes;
// ── Consolidated keydown handler ──
@@ -292,7 +306,7 @@ export function useAppHotkeys({
return;
}
// Delete / Backspace — remove selected element (timeline clip or preview selection)
// Delete / Backspace — remove selected keyframes > reset keyframes > remove element
if (
(event.key === "Delete" || event.key === "Backspace") &&
!event.metaKey &&
@@ -300,6 +314,26 @@ export function useAppHotkeys({
!event.altKey &&
!isEditableTarget(event.target)
) {
// Priority: selected keyframes take precedence over clip deletion
const { selectedKeyframes } = usePlayerStore.getState();
if (selectedKeyframes.size > 0) {
onDeleteSelectedKeyframesRef.current();
usePlayerStore.getState().clearSelectedKeyframes();
event.preventDefault();
return;
}
// Backspace: try resetting keyframes first; fall through to delete if none found
if (event.key === "Backspace") {
const { selectedElementId, keyframeCache } = usePlayerStore.getState();
if (selectedElementId && keyframeCache.has(selectedElementId)) {
if (onResetKeyframesRef.current()) {
event.preventDefault();
return;
}
}
}
const { selectedElementId, elements } = usePlayerStore.getState();
if (selectedElementId) {
const element = elements.find((el) => (el.key ?? el.id) === selectedElementId);