@hyperframes/studio 0.7.55 → 0.7.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/hyperframes-player--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inspectRuntimeProtocol,
|
|
3
|
+
runtimeProtocolMetadata,
|
|
4
|
+
type RuntimeProtocolInspection,
|
|
5
|
+
} from "@hyperframes/core/runtime/protocol";
|
|
6
|
+
|
|
7
|
+
export type RuntimeControlMessage = {
|
|
8
|
+
source: "hf-parent";
|
|
9
|
+
type: "control";
|
|
10
|
+
action: string;
|
|
11
|
+
} & ReturnType<typeof runtimeProtocolMetadata> &
|
|
12
|
+
Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
export function createRuntimeControlMessage(
|
|
15
|
+
action: string,
|
|
16
|
+
payload: Record<string, unknown> = {},
|
|
17
|
+
fps = 30,
|
|
18
|
+
): RuntimeControlMessage {
|
|
19
|
+
return {
|
|
20
|
+
...payload,
|
|
21
|
+
source: "hf-parent",
|
|
22
|
+
type: "control",
|
|
23
|
+
action,
|
|
24
|
+
...runtimeProtocolMetadata(fps),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function postRuntimeControlMessage(
|
|
29
|
+
target: Pick<Window, "postMessage"> | null | undefined,
|
|
30
|
+
action: string,
|
|
31
|
+
payload: Record<string, unknown> = {},
|
|
32
|
+
fps = 30,
|
|
33
|
+
): void {
|
|
34
|
+
target?.postMessage(createRuntimeControlMessage(action, payload, fps), "*");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function inspectStudioRuntimeMessage(value: unknown): RuntimeProtocolInspection {
|
|
38
|
+
return inspectRuntimeProtocol(value, 30);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dispatchRuntimeProtocolError(inspection: RuntimeProtocolInspection): void {
|
|
42
|
+
if (inspection.status !== "unsupported") return;
|
|
43
|
+
window.dispatchEvent(
|
|
44
|
+
new CustomEvent("runtimeprotocolerror", {
|
|
45
|
+
detail: {
|
|
46
|
+
code: inspection.code,
|
|
47
|
+
receivedVersion: inspection.receivedVersion,
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function acceptStudioRuntimeMessage(
|
|
54
|
+
value: unknown,
|
|
55
|
+
): Exclude<RuntimeProtocolInspection, { status: "unsupported" }> | null {
|
|
56
|
+
const inspection = inspectStudioRuntimeMessage(value);
|
|
57
|
+
if (inspection.status !== "unsupported") return inspection;
|
|
58
|
+
dispatchRuntimeProtocolError(inspection);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function acceptedRuntimeMessageFps(value: unknown): number {
|
|
63
|
+
const inspection = inspectStudioRuntimeMessage(value);
|
|
64
|
+
return inspection.status === "supported" ? inspection.fps : 30;
|
|
65
|
+
}
|
|
@@ -3,26 +3,26 @@ import { formatFrameTime, frameToSeconds, secondsToFrame, stepFrameTime, formatT
|
|
|
3
3
|
|
|
4
4
|
describe("formatTime", () => {
|
|
5
5
|
it("formats zero seconds", () => {
|
|
6
|
-
expect(formatTime(0)).toBe("
|
|
6
|
+
expect(formatTime(0)).toBe("00:00");
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// fallow-ignore-next-line code-duplication
|
|
10
10
|
it("formats seconds less than a minute", () => {
|
|
11
|
-
expect(formatTime(5)).toBe("
|
|
12
|
-
expect(formatTime(30)).toBe("
|
|
13
|
-
expect(formatTime(59)).toBe("
|
|
11
|
+
expect(formatTime(5)).toBe("00:05");
|
|
12
|
+
expect(formatTime(30)).toBe("00:30");
|
|
13
|
+
expect(formatTime(59)).toBe("00:59");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
it("formats exact minutes", () => {
|
|
17
|
-
expect(formatTime(60)).toBe("
|
|
18
|
-
expect(formatTime(120)).toBe("
|
|
17
|
+
expect(formatTime(60)).toBe("01:00");
|
|
18
|
+
expect(formatTime(120)).toBe("02:00");
|
|
19
19
|
expect(formatTime(600)).toBe("10:00");
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("formats minutes and seconds", () => {
|
|
23
|
-
expect(formatTime(65)).toBe("
|
|
24
|
-
expect(formatTime(90)).toBe("
|
|
25
|
-
expect(formatTime(125)).toBe("
|
|
23
|
+
expect(formatTime(65)).toBe("01:05");
|
|
24
|
+
expect(formatTime(90)).toBe("01:30");
|
|
25
|
+
expect(formatTime(125)).toBe("02:05");
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it("formats large values (over an hour)", () => {
|
|
@@ -32,28 +32,28 @@ describe("formatTime", () => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it("floors fractional seconds", () => {
|
|
35
|
-
expect(formatTime(0.9)).toBe("
|
|
36
|
-
expect(formatTime(1.5)).toBe("
|
|
37
|
-
expect(formatTime(59.99)).toBe("
|
|
38
|
-
expect(formatTime(60.5)).toBe("
|
|
35
|
+
expect(formatTime(0.9)).toBe("00:00");
|
|
36
|
+
expect(formatTime(1.5)).toBe("00:01");
|
|
37
|
+
expect(formatTime(59.99)).toBe("00:59");
|
|
38
|
+
expect(formatTime(60.5)).toBe("01:00");
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it("pads single-digit seconds with leading
|
|
42
|
-
expect(formatTime(1)).toBe("
|
|
43
|
-
expect(formatTime(61)).toBe("
|
|
41
|
+
it("pads single-digit minutes and seconds with leading zeros", () => {
|
|
42
|
+
expect(formatTime(1)).toBe("00:01");
|
|
43
|
+
expect(formatTime(61)).toBe("01:01");
|
|
44
44
|
expect(formatTime(609)).toBe("10:09");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
it("guards against negative values", () => {
|
|
48
|
-
expect(formatTime(-1)).toBe("
|
|
48
|
+
expect(formatTime(-1)).toBe("00:00");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it("guards against NaN", () => {
|
|
52
|
-
expect(formatTime(NaN)).toBe("
|
|
52
|
+
expect(formatTime(NaN)).toBe("00:00");
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it("guards against Infinity", () => {
|
|
56
|
-
expect(formatTime(Infinity)).toBe("
|
|
56
|
+
expect(formatTime(Infinity)).toBe("00:00");
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
package/src/player/lib/time.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export const STUDIO_PREVIEW_FPS = 30;
|
|
2
2
|
|
|
3
3
|
export function formatTime(time: number): string {
|
|
4
|
-
if (!Number.isFinite(time) || time < 0) return "
|
|
4
|
+
if (!Number.isFinite(time) || time < 0) return "00:00";
|
|
5
5
|
const mins = Math.floor(time / 60);
|
|
6
6
|
const secs = Math.floor(time % 60);
|
|
7
|
-
|
|
7
|
+
// Zero-pad minutes as well as seconds so every readout is a stable-width
|
|
8
|
+
// MM:SS (e.g. "00:44", not "0:44"). Minutes past 99 keep their extra digits
|
|
9
|
+
// ("120:00"); an hours-style H:MM:SS grouping is applied by callers that need
|
|
10
|
+
// it (see formatTimelineTickLabel), never here.
|
|
11
|
+
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
export function secondsToFrame(time: number, fps = STUDIO_PREVIEW_FPS): number {
|
|
@@ -4,8 +4,13 @@ import {
|
|
|
4
4
|
createTimelineElementFromManifestClip,
|
|
5
5
|
parseTimelineFromDOM,
|
|
6
6
|
createImplicitTimelineLayersFromDOM,
|
|
7
|
-
|
|
7
|
+
mergeTimelineElementsPreservingDowngrades,
|
|
8
8
|
} from "./timelineDOM";
|
|
9
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
10
|
+
|
|
11
|
+
function el(id: string, extra: Partial<TimelineElement> = {}): TimelineElement {
|
|
12
|
+
return { id, tag: "img", start: 0, duration: 5, track: 0, ...extra };
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
function makeDoc(html: string): Document {
|
|
11
16
|
const d = document.implementation.createHTMLDocument();
|
|
@@ -13,29 +18,6 @@ function makeDoc(html: string): Document {
|
|
|
13
18
|
return d;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
function makeLiveDoc(html: string): Document {
|
|
17
|
-
document.head.innerHTML = "";
|
|
18
|
-
document.body.innerHTML = html;
|
|
19
|
-
return document;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function mockComputedZIndex(doc: Document, zIndexById: ReadonlyMap<string, string>): void {
|
|
23
|
-
const win = doc.defaultView;
|
|
24
|
-
if (!win) throw new Error("Expected document window");
|
|
25
|
-
const original = win.getComputedStyle.bind(win);
|
|
26
|
-
Object.defineProperty(win, "getComputedStyle", {
|
|
27
|
-
configurable: true,
|
|
28
|
-
value: (element: Element, pseudoElt?: string | null) => {
|
|
29
|
-
const style = original(element, pseudoElt);
|
|
30
|
-
const zIndex = zIndexById.get(element.id);
|
|
31
|
-
if (zIndex != null) {
|
|
32
|
-
Object.defineProperty(style, "zIndex", { configurable: true, value: zIndex });
|
|
33
|
-
}
|
|
34
|
-
return style;
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
21
|
describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
|
40
22
|
it("harvests hfId from a data-start element that has data-hf-id", () => {
|
|
41
23
|
const doc = makeDoc(`
|
|
@@ -126,58 +108,6 @@ describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
|
|
126
108
|
|
|
127
109
|
expect(element.hidden).toBe(true);
|
|
128
110
|
});
|
|
129
|
-
|
|
130
|
-
it("captures the effective z-index from the live element, not the runtime inline-only value", () => {
|
|
131
|
-
// The runtime reports inline-only z-index (0 for CSS-rule authored z-index),
|
|
132
|
-
// which must NOT override the live element's effective z-index — otherwise
|
|
133
|
-
// the timeline collapses every CSS-styled clip to a z=0 tie and mis-orders.
|
|
134
|
-
const doc = makeDoc(`
|
|
135
|
-
<div data-composition-id="root">
|
|
136
|
-
<div id="hero" class="clip" data-start="0" data-duration="5" style="z-index: 30"></div>
|
|
137
|
-
</div>
|
|
138
|
-
`);
|
|
139
|
-
const hostEl = doc.getElementById("hero");
|
|
140
|
-
|
|
141
|
-
const element = createTimelineElementFromManifestClip({
|
|
142
|
-
clip: {
|
|
143
|
-
id: "hero",
|
|
144
|
-
label: "Hero",
|
|
145
|
-
kind: "element",
|
|
146
|
-
tagName: "div",
|
|
147
|
-
start: 0,
|
|
148
|
-
duration: 5,
|
|
149
|
-
track: 0,
|
|
150
|
-
zIndex: 0,
|
|
151
|
-
compositionId: null,
|
|
152
|
-
parentCompositionId: null,
|
|
153
|
-
compositionSrc: null,
|
|
154
|
-
assetUrl: null,
|
|
155
|
-
},
|
|
156
|
-
fallbackIndex: 0,
|
|
157
|
-
doc,
|
|
158
|
-
hostEl,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
expect(element.zIndex).toBe(30);
|
|
162
|
-
expect(element.hasExplicitZIndex).toBe(true);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("marks parsed inline, CSS-rule, and auto z-index authorship accurately", () => {
|
|
166
|
-
const doc = makeLiveDoc(`
|
|
167
|
-
<div data-composition-id="root">
|
|
168
|
-
<div id="inline" class="clip" data-start="0" data-duration="2" style="z-index: 3"></div>
|
|
169
|
-
<div id="rule" class="clip" data-start="0" data-duration="2"></div>
|
|
170
|
-
<div id="auto" class="clip" data-start="0" data-duration="2"></div>
|
|
171
|
-
</div>
|
|
172
|
-
`);
|
|
173
|
-
mockComputedZIndex(doc, new Map([["rule", "12"]]));
|
|
174
|
-
|
|
175
|
-
const elements = parseTimelineFromDOM(doc, 10);
|
|
176
|
-
|
|
177
|
-
expect(elements.find((el) => el.id === "inline")?.hasExplicitZIndex).toBe(true);
|
|
178
|
-
expect(elements.find((el) => el.id === "rule")?.hasExplicitZIndex).toBe(true);
|
|
179
|
-
expect(elements.find((el) => el.id === "auto")?.hasExplicitZIndex).toBe(false);
|
|
180
|
-
});
|
|
181
111
|
});
|
|
182
112
|
|
|
183
113
|
describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
|
|
@@ -211,31 +141,28 @@ describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
|
|
|
211
141
|
|
|
212
142
|
expect(layers).toEqual([]);
|
|
213
143
|
});
|
|
144
|
+
});
|
|
214
145
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
mockComputedZIndex(doc, new Map([["layer", "8"]]));
|
|
222
|
-
|
|
223
|
-
const layers = createImplicitTimelineLayersFromDOM(doc, 10);
|
|
224
|
-
|
|
225
|
-
expect(layers[0]?.zIndex).toBe(8);
|
|
226
|
-
expect(layers[0]?.hasExplicitZIndex).toBe(true);
|
|
146
|
+
describe("mergeTimelineElementsPreservingDowngrades — genuine removal vs transient downgrade", () => {
|
|
147
|
+
it("drops a removed TOP-LEVEL element (undo of a split) instead of ghosting it", () => {
|
|
148
|
+
const current = [el("a"), el("a-split")]; // post-split store: original + clone
|
|
149
|
+
const next = [el("a")]; // fresh scan of the reverted file: clone gone
|
|
150
|
+
const merged = mergeTimelineElementsPreservingDowngrades(current, next, 30, 30);
|
|
151
|
+
expect(merged.map((e) => e.id)).toEqual(["a"]);
|
|
227
152
|
});
|
|
228
|
-
});
|
|
229
153
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
iframeSrc: "/preview/comp/index.html",
|
|
237
|
-
});
|
|
154
|
+
it("still preserves an enriched sub-composition child a bare re-scan drops", () => {
|
|
155
|
+
const current = [el("a"), el("sub-child", { compositionSrc: "sub.html" })];
|
|
156
|
+
const next = [el("a")]; // bare DOM scan misses the enriched sub-comp child
|
|
157
|
+
const merged = mergeTimelineElementsPreservingDowngrades(current, next, 30, 30);
|
|
158
|
+
expect(merged.map((e) => e.id).sort()).toEqual(["a", "sub-child"]);
|
|
159
|
+
});
|
|
238
160
|
|
|
239
|
-
|
|
161
|
+
it("trusts the fresh scan fully when it is not shorter", () => {
|
|
162
|
+
const current = [el("a"), el("b", { compositionSrc: "sub.html" })];
|
|
163
|
+
const next = [el("a"), el("c")];
|
|
164
|
+
expect(
|
|
165
|
+
mergeTimelineElementsPreservingDowngrades(current, next, 30, 30).map((e) => e.id),
|
|
166
|
+
).toEqual(["a", "c"]);
|
|
240
167
|
});
|
|
241
168
|
});
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
import type { TimelineElement } from "../store/playerStore";
|
|
12
12
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
13
|
-
import { getElementZIndex, hasExplicitZIndex } from "./layerOrdering";
|
|
14
13
|
import {
|
|
15
14
|
resolveMediaElement,
|
|
16
15
|
applyMediaMetadataFromElement,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
buildTimelineElementIdentity,
|
|
25
24
|
getTimelineElementIdentity,
|
|
26
25
|
isTimelineIgnoredElement,
|
|
26
|
+
readTimelineElementZIndex,
|
|
27
27
|
} from "./timelineElementHelpers";
|
|
28
28
|
|
|
29
29
|
// Re-export helpers that were previously public from this module so that
|
|
@@ -66,49 +66,6 @@ function resolveClipTag(clip: ClipManifestClip): string {
|
|
|
66
66
|
return clip.tagName || clip.kind || "div";
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function resolveDomCompositionContext(
|
|
70
|
-
element: Element,
|
|
71
|
-
root: Element | null,
|
|
72
|
-
): {
|
|
73
|
-
parentCompositionId: string | null;
|
|
74
|
-
compositionAncestors: string[];
|
|
75
|
-
stackingContextId: string | null;
|
|
76
|
-
} {
|
|
77
|
-
const ancestors: string[] = [];
|
|
78
|
-
let parentCompositionId: string | null = null;
|
|
79
|
-
let cursor = element.parentElement;
|
|
80
|
-
while (cursor) {
|
|
81
|
-
const compositionId = cursor.getAttribute("data-composition-id");
|
|
82
|
-
if (compositionId) {
|
|
83
|
-
ancestors.push(compositionId);
|
|
84
|
-
if (!parentCompositionId && cursor !== root) {
|
|
85
|
-
parentCompositionId = compositionId;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
cursor = cursor.parentElement;
|
|
89
|
-
}
|
|
90
|
-
const compositionAncestors = ancestors.reverse();
|
|
91
|
-
return {
|
|
92
|
-
parentCompositionId,
|
|
93
|
-
compositionAncestors,
|
|
94
|
-
stackingContextId: parentCompositionId ?? compositionAncestors[0] ?? null,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function isHTMLElement(element: Element | null): element is HTMLElement {
|
|
99
|
-
if (!element) return false;
|
|
100
|
-
const HtmlElementCtor = element.ownerDocument.defaultView?.HTMLElement ?? globalThis.HTMLElement;
|
|
101
|
-
return typeof HtmlElementCtor !== "undefined" && element instanceof HtmlElementCtor;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function getTimelineElementZIndex(element: Element | null): number | undefined {
|
|
105
|
-
return isHTMLElement(element) ? getElementZIndex(element) : undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function getTimelineElementHasExplicitZIndex(element: Element | null): boolean {
|
|
109
|
-
return isHTMLElement(element) ? hasExplicitZIndex(element) : false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
69
|
// fallow-ignore-next-line complexity
|
|
113
70
|
export function createTimelineElementFromManifestClip(params: {
|
|
114
71
|
clip: ClipManifestClip;
|
|
@@ -130,14 +87,6 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
130
87
|
let sourceFile: string | undefined;
|
|
131
88
|
|
|
132
89
|
let hfId: string | undefined;
|
|
133
|
-
const domContext = hostEl
|
|
134
|
-
? resolveDomCompositionContext(hostEl, doc?.querySelector("[data-composition-id]") ?? null)
|
|
135
|
-
: null;
|
|
136
|
-
const compositionAncestors = clip.compositionAncestors ?? domContext?.compositionAncestors;
|
|
137
|
-
const parentCompositionId = clip.parentCompositionId ?? domContext?.parentCompositionId;
|
|
138
|
-
const stackingContextId =
|
|
139
|
-
clip.stackingContextId ?? parentCompositionId ?? compositionAncestors?.[0] ?? null;
|
|
140
|
-
|
|
141
90
|
if (hostEl) {
|
|
142
91
|
domId = hostEl.id || undefined;
|
|
143
92
|
hfId = hostEl.getAttribute("data-hf-id") || undefined;
|
|
@@ -164,15 +113,15 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
164
113
|
start: clip.start,
|
|
165
114
|
duration: clip.duration,
|
|
166
115
|
track: clip.track,
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
116
|
+
// clip.track IS the authored data-track-index verbatim (the runtime honors
|
|
117
|
+
// it; see parseAuthoredTrack in core/runtime/timeline.ts). Record it at this
|
|
118
|
+
// translation boundary so later display-lane remaps (normalizeToZones,
|
|
119
|
+
// expanded-child rows) can persist in AUTHORED space instead of
|
|
120
|
+
// reconstructing it from lane occupants.
|
|
121
|
+
authoredTrack: clip.track,
|
|
122
|
+
// Runtime-computed stacking context — authoritative; helpers read it, never
|
|
123
|
+
// re-derive it.
|
|
124
|
+
stackingContextId: clip.stackingContextId ?? null,
|
|
176
125
|
domId,
|
|
177
126
|
hfId,
|
|
178
127
|
selector,
|
|
@@ -185,6 +134,7 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
185
134
|
if (hostEl.hasAttribute("data-hidden")) entry.hidden = true;
|
|
186
135
|
const timelineRole = hostEl.getAttribute("data-timeline-role");
|
|
187
136
|
if (timelineRole) entry.timelineRole = timelineRole;
|
|
137
|
+
entry.zIndex = readTimelineElementZIndex(hostEl);
|
|
188
138
|
}
|
|
189
139
|
if (clip.assetUrl) entry.src = clip.assetUrl;
|
|
190
140
|
if (clip.kind === "composition" && clip.compositionId) {
|
|
@@ -207,8 +157,6 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
207
157
|
}
|
|
208
158
|
}
|
|
209
159
|
if (hostEl) {
|
|
210
|
-
entry.zIndex = getTimelineElementZIndex(hostEl) ?? entry.zIndex;
|
|
211
|
-
entry.hasExplicitZIndex = getTimelineElementHasExplicitZIndex(hostEl);
|
|
212
160
|
entry.domId = hostEl.id || undefined;
|
|
213
161
|
entry.hfId = hostEl.getAttribute("data-hf-id") || undefined;
|
|
214
162
|
entry.selector = getTimelineElementSelector(hostEl);
|
|
@@ -269,10 +217,10 @@ export function createImplicitTimelineLayersFromDOM(
|
|
|
269
217
|
});
|
|
270
218
|
if (existingKeys.has(identity.key) || existingKeys.has(identity.id)) continue;
|
|
271
219
|
|
|
272
|
-
const compositionContext = resolveDomCompositionContext(child, rootComp);
|
|
273
220
|
layers.push({
|
|
274
221
|
domId: child.id || undefined,
|
|
275
222
|
hfId: child.getAttribute("data-hf-id") || undefined,
|
|
223
|
+
zIndex: readTimelineElementZIndex(child),
|
|
276
224
|
duration: rootDuration,
|
|
277
225
|
id: identity.id,
|
|
278
226
|
key: identity.key,
|
|
@@ -281,11 +229,6 @@ export function createImplicitTimelineLayersFromDOM(
|
|
|
281
229
|
selectorIndex,
|
|
282
230
|
sourceFile,
|
|
283
231
|
start: 0,
|
|
284
|
-
zIndex: getTimelineElementZIndex(child),
|
|
285
|
-
hasExplicitZIndex: getTimelineElementHasExplicitZIndex(child),
|
|
286
|
-
stackingContextId: compositionContext.stackingContextId,
|
|
287
|
-
parentCompositionId: compositionContext.parentCompositionId,
|
|
288
|
-
compositionAncestors: compositionContext.compositionAncestors,
|
|
289
232
|
tag: child.tagName.toLowerCase(),
|
|
290
233
|
timingSource: "implicit",
|
|
291
234
|
track: maxTrack + 1 + layers.length,
|
|
@@ -347,7 +290,6 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
347
290
|
selectorIndex,
|
|
348
291
|
sourceFile,
|
|
349
292
|
});
|
|
350
|
-
const compositionContext = resolveDomCompositionContext(el, rootComp);
|
|
351
293
|
const entry: TimelineElement = {
|
|
352
294
|
id: identity.id,
|
|
353
295
|
label,
|
|
@@ -356,17 +298,13 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
356
298
|
start,
|
|
357
299
|
duration: dur,
|
|
358
300
|
track: isNaN(track) ? 0 : track,
|
|
359
|
-
zIndex: getTimelineElementZIndex(el),
|
|
360
|
-
hasExplicitZIndex: getTimelineElementHasExplicitZIndex(el),
|
|
361
|
-
stackingContextId: compositionContext.stackingContextId,
|
|
362
|
-
parentCompositionId: compositionContext.parentCompositionId,
|
|
363
|
-
compositionAncestors: compositionContext.compositionAncestors,
|
|
364
301
|
domId: el.id || undefined,
|
|
365
302
|
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
366
303
|
selector,
|
|
367
304
|
selectorIndex,
|
|
368
305
|
sourceFile,
|
|
369
306
|
timingSource: "authored",
|
|
307
|
+
zIndex: readTimelineElementZIndex(el),
|
|
370
308
|
};
|
|
371
309
|
|
|
372
310
|
const mediaEl = resolveMediaElement(el);
|
|
@@ -437,7 +375,14 @@ export function mergeTimelineElementsPreservingDowngrades(
|
|
|
437
375
|
|
|
438
376
|
const nextIdentities = new Set(nextElements.map(getTimelineElementIdentity));
|
|
439
377
|
const preserved = currentElements.filter(
|
|
440
|
-
(element) =>
|
|
378
|
+
(element) =>
|
|
379
|
+
!nextIdentities.has(getTimelineElementIdentity(element)) &&
|
|
380
|
+
// Only preserve enriched sub-composition children (compositionSrc set),
|
|
381
|
+
// which a bare DOM re-scan legitimately drops and enrichMissingCompositions
|
|
382
|
+
// re-adds. A TOP-LEVEL element missing from the fresh scan was genuinely
|
|
383
|
+
// removed (undo of a split, a delete), so let it go — otherwise undoing a
|
|
384
|
+
// split leaves a ghost clip in the timeline even though the file is reverted.
|
|
385
|
+
element.compositionSrc != null,
|
|
441
386
|
);
|
|
442
387
|
if (preserved.length === 0) return nextElements;
|
|
443
388
|
return [...nextElements, ...preserved];
|
|
@@ -481,11 +426,6 @@ export function buildStandaloneRootTimelineElement(params: {
|
|
|
481
426
|
start: 0,
|
|
482
427
|
duration: params.rootDuration,
|
|
483
428
|
track: 0,
|
|
484
|
-
zIndex: 0,
|
|
485
|
-
hasExplicitZIndex: false,
|
|
486
|
-
stackingContextId: params.compositionId,
|
|
487
|
-
parentCompositionId: null,
|
|
488
|
-
compositionAncestors: [params.compositionId],
|
|
489
429
|
compositionSrc,
|
|
490
430
|
selector: params.selector,
|
|
491
431
|
selectorIndex: params.selectorIndex,
|
|
@@ -15,6 +15,31 @@ import { isFinitePositive } from "./playbackAdapter";
|
|
|
15
15
|
// Duration attribute helpers
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Read a host element's effective CSS stacking order for the timeline's reverse
|
|
20
|
+
* z→lane mapping. Prefers the inline `style.zIndex` (what the canvas context
|
|
21
|
+
* menu and LayersPanel z-edits write via handleDomZIndexReorderCommit), falls
|
|
22
|
+
* back to computed style; "auto" / empty / unparseable ⇒ 0. Works with a
|
|
23
|
+
* detached parse Document (no defaultView) as well as a live iframe. Mirrors
|
|
24
|
+
* canvasContextMenuZOrder.parseZIndex semantics so the two directions agree.
|
|
25
|
+
*/
|
|
26
|
+
export function readTimelineElementZIndex(el: Element): number {
|
|
27
|
+
const html = el as HTMLElement;
|
|
28
|
+
const parseZ = (value: string | null | undefined): number | null => {
|
|
29
|
+
if (value == null || value === "" || value === "auto") return null;
|
|
30
|
+
const n = Number.parseInt(value, 10);
|
|
31
|
+
return Number.isFinite(n) ? n : null;
|
|
32
|
+
};
|
|
33
|
+
const fromInline = parseZ(html.style?.zIndex);
|
|
34
|
+
if (fromInline != null) return fromInline;
|
|
35
|
+
const view = el.ownerDocument?.defaultView;
|
|
36
|
+
if (view?.getComputedStyle) {
|
|
37
|
+
const fromComputed = parseZ(view.getComputedStyle(html).zIndex);
|
|
38
|
+
if (fromComputed != null) return fromComputed;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
function readDurationAttribute(el: Element | null | undefined): number {
|
|
19
44
|
if (!el) return 0;
|
|
20
45
|
const duration =
|
|
@@ -36,14 +61,20 @@ export function isTimelineIgnoredElement(el: Element): boolean {
|
|
|
36
61
|
);
|
|
37
62
|
}
|
|
38
63
|
|
|
39
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Furthest clip end (start + RAW `data-duration`) over every non-root clip in the
|
|
66
|
+
* document. Reads the authored attribute, NOT any runtime-computed value — so it
|
|
67
|
+
* is immune to the runtime's clamp that truncates a clip's live duration to the
|
|
68
|
+
* composition length. This is the source of truth for content-driven duration:
|
|
69
|
+
* computing it from the store instead would feed the truncated value back in and
|
|
70
|
+
* make the composition length ratchet down (research HANDOFF-3 §6.1 feedback loop).
|
|
71
|
+
*/
|
|
72
|
+
export function furthestClipEndFromDocument(doc: Document | null | undefined): number {
|
|
40
73
|
if (!doc) return 0;
|
|
41
|
-
const
|
|
42
|
-
if (rootDuration > 0) return rootDuration;
|
|
43
|
-
|
|
74
|
+
const root = doc.querySelector("[data-composition-id]");
|
|
44
75
|
let maxEnd = 0;
|
|
45
76
|
for (const node of Array.from(doc.querySelectorAll("[data-start]"))) {
|
|
46
|
-
if (isTimelineIgnoredElement(node)) continue;
|
|
77
|
+
if (node === root || isTimelineIgnoredElement(node)) continue;
|
|
47
78
|
const start = Number.parseFloat(node.getAttribute("data-start") ?? "");
|
|
48
79
|
const duration = readDurationAttribute(node);
|
|
49
80
|
if (!Number.isFinite(start) || start < 0 || duration <= 0) continue;
|
|
@@ -52,6 +83,24 @@ export function readTimelineDurationFromDocument(doc: Document | null | undefine
|
|
|
52
83
|
return maxEnd;
|
|
53
84
|
}
|
|
54
85
|
|
|
86
|
+
export function readTimelineDurationFromDocument(doc: Document | null | undefined): number {
|
|
87
|
+
if (!doc) return 0;
|
|
88
|
+
const rootDuration = readDurationAttribute(doc.querySelector("[data-composition-id]"));
|
|
89
|
+
if (rootDuration > 0) return rootDuration;
|
|
90
|
+
return furthestClipEndFromDocument(doc);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Furthest clip end parsed straight from a composition SOURCE STRING (the HTML
|
|
95
|
+
* being saved). Uses raw `data-duration`, so it is the correct input for syncing
|
|
96
|
+
* the root duration after an edit — reading the store instead would use the
|
|
97
|
+
* runtime-truncated durations and shrink the composition (the feedback loop).
|
|
98
|
+
*/
|
|
99
|
+
export function furthestClipEndFromSource(source: string): number {
|
|
100
|
+
if (!source) return 0;
|
|
101
|
+
return furthestClipEndFromDocument(new DOMParser().parseFromString(source, "text/html"));
|
|
102
|
+
}
|
|
103
|
+
|
|
55
104
|
// ---------------------------------------------------------------------------
|
|
56
105
|
// DOM element type guards
|
|
57
106
|
// ---------------------------------------------------------------------------
|
|
@@ -249,6 +298,25 @@ export function getTimelineElementIdentity(element: { key?: string | null; id: s
|
|
|
249
298
|
return element.key ?? element.id;
|
|
250
299
|
}
|
|
251
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Timeline store key for a z-reorder entry built OUTSIDE the timeline
|
|
303
|
+
* expansion (canvas context menu / LayersPanel), so the reorder commit can
|
|
304
|
+
* update the store's zIndex synchronously. Matches buildTimelineElementKey's
|
|
305
|
+
* stable branches (`sourceFile#domId`, else the selector-based key). Undefined
|
|
306
|
+
* when the element has neither a DOM id nor a selector — the timeline's
|
|
307
|
+
* fallback branch needs its own fallbackIndex, which these callers don't have,
|
|
308
|
+
* so such an entry simply skips the synchronous store update.
|
|
309
|
+
*/
|
|
310
|
+
export function deriveTimelineStoreKey(params: {
|
|
311
|
+
domId?: string;
|
|
312
|
+
selector?: string;
|
|
313
|
+
selectorIndex?: number;
|
|
314
|
+
sourceFile?: string;
|
|
315
|
+
}): string | undefined {
|
|
316
|
+
if (!params.domId && !params.selector) return undefined;
|
|
317
|
+
return buildTimelineElementKey({ id: "", fallbackIndex: 0, ...params });
|
|
318
|
+
}
|
|
319
|
+
|
|
252
320
|
// ---------------------------------------------------------------------------
|
|
253
321
|
// DOM node querying
|
|
254
322
|
// ---------------------------------------------------------------------------
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
getTimelineElementSelectorIndex,
|
|
19
19
|
getTimelineElementDisplayLabel,
|
|
20
20
|
buildTimelineElementIdentity,
|
|
21
|
+
readTimelineElementZIndex,
|
|
21
22
|
} from "./timelineElementHelpers";
|
|
23
|
+
import { postRuntimeControlMessage } from "./runtimeProtocol";
|
|
22
24
|
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
24
26
|
// Viewport / DOM normalisation
|
|
@@ -102,10 +104,7 @@ function postPreviewControl(
|
|
|
102
104
|
action: string,
|
|
103
105
|
payload: Record<string, unknown>,
|
|
104
106
|
): void {
|
|
105
|
-
iframe.contentWindow
|
|
106
|
-
{ source: "hf-parent", type: "control", action, ...payload },
|
|
107
|
-
"*",
|
|
108
|
-
);
|
|
107
|
+
postRuntimeControlMessage(iframe.contentWindow, action, payload);
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
export function shouldMutePreviewAudio(audioMuted: boolean, playbackRate: number): boolean {
|
|
@@ -377,6 +376,7 @@ export function buildMissingCompositionElements(
|
|
|
377
376
|
selector,
|
|
378
377
|
selectorIndex,
|
|
379
378
|
sourceFile,
|
|
379
|
+
zIndex: readTimelineElementZIndex(el),
|
|
380
380
|
};
|
|
381
381
|
if (compSrc) {
|
|
382
382
|
entry.compositionSrc = compSrc;
|