@hyperframes/studio 0.8.114 → 0.8.116
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-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
- package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
- package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
- package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
- package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
- package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
- package/dist/chunk-5EKWJZLM.js.map +1 -0
- package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
- package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
- package/dist/index.d.ts +146 -40
- package/dist/index.html +2 -2
- package/dist/index.js +4021 -3591
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +9 -8
- package/src/App.tsx +2 -2
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/LinkedSelectionToggle.tsx +3 -2
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/dock/Dock.tsx +5 -7
- package/src/components/editor/CanvasContextMenu.tsx +3 -2
- package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
- package/src/components/editor/DomEditCropHandles.tsx +7 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +13 -10
- package/src/components/editor/MotionPathOverlay.tsx +7 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
- package/src/components/editor/SnapToolbar.test.tsx +100 -1
- package/src/components/editor/SnapToolbar.tsx +43 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
- package/src/components/editor/audioFxTelemetry.ts +2 -3
- package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
- package/src/components/editor/cropResize.ts +4 -3
- package/src/components/editor/domEditingDom.ts +4 -0
- package/src/components/editor/domEditingLayers.ts +1 -5
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +7 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -6
- package/src/components/editor/marqueeCommit.test.tsx +106 -37
- package/src/components/editor/marqueeCommit.ts +24 -8
- package/src/components/editor/motionPathCommit.test.ts +29 -1
- package/src/components/editor/motionPathCommit.ts +21 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/probeSourceElement.test.ts +45 -0
- package/src/components/editor/probeSourceElement.ts +41 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
- package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
- package/src/components/editor/propertyPanelFxSection.tsx +30 -28
- package/src/components/editor/propertyPanelTypes.ts +6 -3
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditNudge.test.tsx +37 -0
- package/src/components/editor/useDomEditNudge.ts +2 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
- package/src/components/editor/useDomEditZOrder.ts +12 -2
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetContextMenu.tsx +4 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/components/ui/menuStyle.test.ts +61 -0
- package/src/components/ui/menuStyle.ts +10 -0
- package/src/contexts/DomEditContext.tsx +5 -1
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/domEditExports.test.tsx +9 -1
- package/src/hooks/domEditCommitRunner.ts +1 -1
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/elementOffsetStager.ts +5 -4
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +4 -125
- package/src/hooks/gsapDragPositionCommit.ts +36 -384
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGestureOutcome.test.ts +161 -0
- package/src/hooks/gsapGestureOutcome.ts +64 -0
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapKeyframeCommit.ts +3 -12
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +96 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -168
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +13 -0
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +8 -6
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/plainRotation.ts +3 -2
- package/src/hooks/previewPromotionSelection.test.tsx +1 -1
- package/src/hooks/refreshTimelineRowText.test.ts +65 -0
- package/src/hooks/refreshTimelineRowText.ts +29 -0
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
- package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
- package/src/hooks/useDomEditTextCommits.ts +13 -3
- package/src/hooks/useDomEditWiring.ts +4 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
- package/src/hooks/useDomGeometryCommit.ts +8 -4
- package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -2
- package/src/hooks/useDomSelection.test.ts +26 -0
- package/src/hooks/useDomSelection.ts +10 -3
- package/src/hooks/useDomSelectionTypes.ts +6 -1
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +34 -3
- package/src/hooks/useEnableKeyframes.ts +7 -4
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.ts +3 -2
- package/src/hooks/useGestureCommit.test.tsx +225 -55
- package/src/hooks/useGestureCommit.ts +86 -55
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +101 -195
- package/src/hooks/useGsapAwareGroupMove.ts +179 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
- package/src/hooks/useGsapKeyframeOps.ts +36 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
- package/src/hooks/useGsapScriptCommits.ts +15 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
- package/src/hooks/useGsapSelectionHandlers.ts +24 -18
- package/src/hooks/useGsapTweenCache.ts +18 -12
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +277 -1
- package/src/hooks/usePersistentEditHistory.ts +47 -8
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
- package/src/hooks/usePreviewInteraction.test.ts +21 -0
- package/src/hooks/usePreviewInteraction.ts +2 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useRenderClipContent.test.ts +18 -0
- package/src/hooks/useRenderClipContent.ts +4 -5
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
- package/src/hooks/useTimelineDeleteOps.ts +22 -27
- package/src/hooks/useTimelineEditing.test.tsx +1 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/AutomationSelectionMenu.tsx +3 -2
- package/src/player/components/ClipContextMenu.test.tsx +73 -0
- package/src/player/components/ClipContextMenu.tsx +33 -31
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
- package/src/player/components/OutOfSyncBadge.tsx +5 -5
- package/src/player/components/TextClipContent.test.ts +19 -0
- package/src/player/components/TextClipContent.tsx +27 -0
- package/src/player/components/TrackGapContextMenu.tsx +3 -6
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipMenuToolItems.test.tsx +23 -0
- package/src/player/components/clipMenuToolItems.tsx +13 -9
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
- package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
- package/src/player/components/timelineClipGestureHandlers.ts +20 -2
- package/src/player/components/timelineEditing.ts +3 -1
- package/src/player/components/timelineLinkSelection.ts +35 -1
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
- package/src/player/components/useTimelineClipDrag.ts +6 -7
- package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
- package/src/player/hooks/timelinePlayerSync.ts +6 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/lib/timelineElementHelpers.test.ts +87 -1
- package/src/player/lib/timelineElementHelpers.ts +6 -15
- package/src/player/lib/timelineText.ts +40 -0
- package/src/player/sceneSwap.ts +2 -1
- package/src/player/store/timelineElement.ts +12 -0
- package/src/styles/theme.css +3 -0
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.ts +2 -3
- package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/keyframeUsage.ts +79 -0
- package/src/utils/previewFeatureUsage.test.ts +32 -0
- package/src/utils/previewFeatureUsage.ts +46 -0
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +1 -3
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +4 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.test.ts +107 -0
- package/src/utils/studioTelemetry.ts +51 -5
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +4 -3
- package/dist/chunk-BIUXP3PG.js.map +0 -1
- package/dist/chunk-K47E2ZJA.js.map +0 -1
- /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
- /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
import type { TimelineEditCapabilities } from "./timelineEditCapabilities";
|
|
9
9
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
10
10
|
import { CLIP_HANDLE_W } from "./timelineLayout";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
exceedsHandEditLimit,
|
|
13
|
+
selectClipWithLinks,
|
|
14
|
+
toggleClipWithLinks,
|
|
15
|
+
} from "./timelineLinkSelection";
|
|
12
16
|
import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
|
|
13
17
|
|
|
14
18
|
export interface ClipGestureDeps {
|
|
@@ -73,6 +77,7 @@ function isIntentBlocked(
|
|
|
73
77
|
*/
|
|
74
78
|
function resolvePointerDownAction(
|
|
75
79
|
e: ReactPointerEvent,
|
|
80
|
+
grabbed: TimelineElement,
|
|
76
81
|
capabilities: TimelineEditCapabilities,
|
|
77
82
|
onResizeElement: ClipGestureDeps["onResizeElement"],
|
|
78
83
|
onMoveElement: ClipGestureDeps["onMoveElement"],
|
|
@@ -93,6 +98,8 @@ function resolvePointerDownAction(
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
if (!onMoveElement || !capabilities.canMove) return { kind: "ignore" };
|
|
101
|
+
if (exceedsHandEditLimit(grabbed, e.altKey, null))
|
|
102
|
+
return { kind: "block", intent: "edit-many", rect };
|
|
96
103
|
return { kind: "move", rect };
|
|
97
104
|
}
|
|
98
105
|
|
|
@@ -129,6 +136,17 @@ export function createClipGestureHandlers(
|
|
|
129
136
|
const onResizeStart = (edge: "start" | "end", e: ReactPointerEvent): void => {
|
|
130
137
|
if (!canStartResize(edge, e, capabilities, onResizeElement)) return;
|
|
131
138
|
e.stopPropagation();
|
|
139
|
+
if (exceedsHandEditLimit(el, e.altKey, edge)) {
|
|
140
|
+
blockedClipRef.current = {
|
|
141
|
+
pointerId: e.pointerId,
|
|
142
|
+
element: el,
|
|
143
|
+
intent: "edit-many",
|
|
144
|
+
originClientX: e.clientX,
|
|
145
|
+
originClientY: e.clientY,
|
|
146
|
+
started: false,
|
|
147
|
+
};
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
132
150
|
blockedClipRef.current = null;
|
|
133
151
|
setShowPopover(false);
|
|
134
152
|
setRangeSelection(null);
|
|
@@ -147,7 +165,7 @@ export function createClipGestureHandlers(
|
|
|
147
165
|
};
|
|
148
166
|
|
|
149
167
|
const onPointerDown = (e: ReactPointerEvent): void => {
|
|
150
|
-
const action = resolvePointerDownAction(e, capabilities, onResizeElement, onMoveElement);
|
|
168
|
+
const action = resolvePointerDownAction(e, el, capabilities, onResizeElement, onMoveElement);
|
|
151
169
|
if (action.kind === "ignore") return;
|
|
152
170
|
|
|
153
171
|
if (action.kind === "block") {
|
|
@@ -248,7 +248,9 @@ export interface TimelinePromptElement {
|
|
|
248
248
|
track: number;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
export type BlockedTimelineEditIntent = "move" | "resize-start" | "resize-end";
|
|
251
|
+
export type BlockedTimelineEditIntent = "move" | "edit-many" | "resize-start" | "resize-end";
|
|
252
|
+
|
|
253
|
+
export const MAX_HAND_EDIT_CLIPS = 3;
|
|
252
254
|
|
|
253
255
|
export interface TimelineRangeSelection {
|
|
254
256
|
start: number;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
dropMisalignedTrimPartners,
|
|
4
|
+
expandToLinkedMembers,
|
|
5
|
+
linkedGestureKeys,
|
|
6
|
+
linkedMembersOf,
|
|
7
|
+
} from "./audioClipLink";
|
|
8
|
+
import { buildTimelineGroupResizeMembers } from "./timelineGroupEditing";
|
|
9
|
+
import { resolveGroupMovers } from "./timelineMultiDragPreview";
|
|
10
|
+
import { MAX_HAND_EDIT_CLIPS } from "./timelineEditing";
|
|
3
11
|
import { isLinkedSelectionOn } from "../../utils/linkedClipPreferences";
|
|
4
12
|
|
|
5
13
|
/**
|
|
@@ -39,3 +47,29 @@ export function toggleClipWithLinks(key: string, altKey: boolean): TimelineEleme
|
|
|
39
47
|
const primary = usePlayerStore.getState().selectedElementId;
|
|
40
48
|
return state.elements.find((el) => (el.key ?? el.id) === primary) ?? null;
|
|
41
49
|
}
|
|
50
|
+
|
|
51
|
+
/** Whether a hand drag on `grabbed` (trimming `edge`, or null to move) would change more clips than allowed. */
|
|
52
|
+
export function exceedsHandEditLimit(
|
|
53
|
+
grabbed: TimelineElement,
|
|
54
|
+
altKey: boolean,
|
|
55
|
+
edge: "start" | "end" | null,
|
|
56
|
+
): boolean {
|
|
57
|
+
const { selectedElementIds, elements } = usePlayerStore.getState();
|
|
58
|
+
const grabbedKey = grabbed.key ?? grabbed.id;
|
|
59
|
+
const keys = linkedGestureKeys(
|
|
60
|
+
selectedElementIds,
|
|
61
|
+
grabbed,
|
|
62
|
+
elements,
|
|
63
|
+
altKey,
|
|
64
|
+
isLinkedSelectionOn(),
|
|
65
|
+
);
|
|
66
|
+
const changed = edge
|
|
67
|
+
? buildTimelineGroupResizeMembers(
|
|
68
|
+
elements,
|
|
69
|
+
dropMisalignedTrimPartners(keys, grabbed, elements, edge),
|
|
70
|
+
grabbedKey,
|
|
71
|
+
edge,
|
|
72
|
+
)
|
|
73
|
+
: resolveGroupMovers(elements, keys, grabbedKey);
|
|
74
|
+
return (changed?.length ?? 1) > MAX_HAND_EDIT_CLIPS;
|
|
75
|
+
}
|
|
@@ -49,6 +49,7 @@ function mount(order = [0, 1, 2]) {
|
|
|
49
49
|
const host = document.createElement("div");
|
|
50
50
|
const root = createRoot(host);
|
|
51
51
|
const onMoveElements = vi.fn();
|
|
52
|
+
const onBlockedEditAttempt = vi.fn();
|
|
52
53
|
const trackOrderRef = { current: order };
|
|
53
54
|
const trackInsertLayoutRef = { current: buildTimelineTrackInsertLayout(order, groups) };
|
|
54
55
|
let state: ReturnType<typeof useTimelineClipDrag>;
|
|
@@ -68,6 +69,7 @@ function mount(order = [0, 1, 2]) {
|
|
|
68
69
|
},
|
|
69
70
|
onMoveElement: vi.fn(),
|
|
70
71
|
onMoveElements,
|
|
72
|
+
onBlockedEditAttempt,
|
|
71
73
|
setShowPopover: () => {},
|
|
72
74
|
setRangeSelectionRef: { current: null },
|
|
73
75
|
});
|
|
@@ -83,6 +85,7 @@ function mount(order = [0, 1, 2]) {
|
|
|
83
85
|
key,
|
|
84
86
|
clip,
|
|
85
87
|
onMoveElements,
|
|
88
|
+
onBlockedEditAttempt,
|
|
86
89
|
root,
|
|
87
90
|
elements,
|
|
88
91
|
changeOrder(next: number[]) {
|
|
@@ -161,3 +164,14 @@ it("leaves Space on a nested fade slider to the slider", () => {
|
|
|
161
164
|
expect(event.defaultPrevented).toBe(false);
|
|
162
165
|
act(() => view.root.unmount());
|
|
163
166
|
});
|
|
167
|
+
|
|
168
|
+
it("does not pick up a clip held in a selection above the hand-move limit", () => {
|
|
169
|
+
const view = mount();
|
|
170
|
+
usePlayerStore.getState().setSelection(["top", "hero", "tail", "bottom"], "hero");
|
|
171
|
+
view.key(" ");
|
|
172
|
+
expect(view.drag).toBeNull();
|
|
173
|
+
expect(view.onBlockedEditAttempt).toHaveBeenCalledWith(
|
|
174
|
+
expect.objectContaining({ id: "hero" }),
|
|
175
|
+
"edit-many",
|
|
176
|
+
);
|
|
177
|
+
});
|
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
} from "./timelineClipDragTypes";
|
|
30
30
|
import { getTimelineElementIndexes } from "../lib/timelineElementIndexes";
|
|
31
31
|
import { dropMisalignedTrimPartners, linkedGestureKeys } from "./audioClipLink";
|
|
32
|
+
import { exceedsHandEditLimit } from "./timelineLinkSelection";
|
|
32
33
|
import { isLinkedSelectionOn } from "../../utils/linkedClipPreferences";
|
|
33
34
|
import { useTimelineClipCapabilities } from "./timelineReadOnly";
|
|
34
35
|
import { timelineClipFocusId } from "./timelineNavigationIdentity";
|
|
@@ -158,11 +159,8 @@ export function useTimelineClipDrag({
|
|
|
158
159
|
const elementsRef = useRef(elements);
|
|
159
160
|
elementsRef.current = elements;
|
|
160
161
|
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
// gesture), so build each ONCE and reuse it across every pointermove and every
|
|
164
|
-
// auto-scroll frame. Both caches are cleared at gesture teardown
|
|
165
|
-
// (stopClipDragAutoScroll), so the next gesture rebuilds against fresh state.
|
|
162
|
+
// The snap-target and audio-track sets are fixed for one drag/resize (the store is not
|
|
163
|
+
// re-authored mid gesture): built once, reused per pointermove, cleared at teardown.
|
|
166
164
|
const snapTargetsCacheRef = useRef<Map<string, TimelineSnapTarget[]>>(new Map());
|
|
167
165
|
const dragAudioTracksRef = useRef<ReadonlySet<number> | null>(null);
|
|
168
166
|
|
|
@@ -272,8 +270,7 @@ export function useTimelineClipDrag({
|
|
|
272
270
|
const blockedClipRef = useRef<BlockedClipState | null>(null);
|
|
273
271
|
const suppressClickRef = useRef(false);
|
|
274
272
|
|
|
275
|
-
//
|
|
276
|
-
// It owns a projection only; canonical store timing changes at commit.
|
|
273
|
+
// Group-resize session, created on first movement; a projection only, committed at the end.
|
|
277
274
|
const groupResizeRef = useRef<TimelineGroupResizeSession | null>(null);
|
|
278
275
|
|
|
279
276
|
const onMoveElementRef = useRef(onMoveElement);
|
|
@@ -541,6 +538,8 @@ export function useTimelineClipDrag({
|
|
|
541
538
|
if (!element || !getClipCapabilities(element).canMove) return;
|
|
542
539
|
event.preventDefault();
|
|
543
540
|
event.stopImmediatePropagation();
|
|
541
|
+
if (exceedsHandEditLimit(element, false, null))
|
|
542
|
+
return void onBlockedEditAttemptRef.current?.(element, "edit-many");
|
|
544
543
|
setShowPopover(false);
|
|
545
544
|
setRangeSelectionRef.current?.(null);
|
|
546
545
|
const row = keyboardPickupInsertRow(
|
|
@@ -31,4 +31,10 @@ describe("timelineElementsChanged", () => {
|
|
|
31
31
|
expect(timelineElementsChanged([grouped], [clip])).toBe(true);
|
|
32
32
|
expect(timelineElementsChanged([grouped], [{ ...grouped }])).toBe(false);
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
it("sees a change to a text row's words, but not an identical re-read", () => {
|
|
36
|
+
const row = { id: "t", tag: "h1", start: 0, duration: 2, track: 0, text: { value: "Old" } };
|
|
37
|
+
expect(timelineElementsChanged([row], [{ ...row, text: { value: "Old" } }])).toBe(false);
|
|
38
|
+
expect(timelineElementsChanged([row], [{ ...row, text: { value: "New" } }])).toBe(true);
|
|
39
|
+
});
|
|
34
40
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TimelineElement } from "../store/playerStore";
|
|
2
|
+
import { sameTimelineText } from "../lib/timelineText";
|
|
2
3
|
|
|
3
4
|
const RENDERED_FIELDS: readonly (keyof TimelineElement)[] = [
|
|
4
5
|
"id",
|
|
@@ -28,6 +29,10 @@ export function timelineElementsChanged(
|
|
|
28
29
|
if (next.length !== previous.length) return true;
|
|
29
30
|
return next.some((element, index) => {
|
|
30
31
|
const prior = previous[index];
|
|
31
|
-
return
|
|
32
|
+
return (
|
|
33
|
+
!prior ||
|
|
34
|
+
RENDERED_FIELDS.some((key) => element[key] !== prior[key]) ||
|
|
35
|
+
!sameTimelineText(element.text, prior.text)
|
|
36
|
+
);
|
|
32
37
|
});
|
|
33
38
|
}
|
|
@@ -36,6 +36,7 @@ import { hasTimelinePerformanceFixtureLease } from "../lib/timelinePerformanceFi
|
|
|
36
36
|
import { applyCachedSourceDurations, probeMissingSourceDurations } from "../lib/mediaProbe";
|
|
37
37
|
import { shouldResumeForwardPlaybackAfterSeek, shouldStopAfterSeek } from "../lib/playbackSeek";
|
|
38
38
|
import { applyPreviewVariablesToUrl } from "../../hooks/previewVariablesStore";
|
|
39
|
+
import { isStudioManualEditGestureLiveIn } from "../../components/editor/manualEditsDom";
|
|
39
40
|
import { createPreviewMessageHandler } from "./previewMessageRouter";
|
|
40
41
|
import { timelineElementsChanged } from "./timelinePlayerSync";
|
|
41
42
|
import { safeContentDocument } from "./timelineSyncHydration";
|
|
@@ -486,7 +487,10 @@ export function useTimelinePlayer({
|
|
|
486
487
|
// A newer edit, or anything replacing the live preview (a reload, a composition switch), wins.
|
|
487
488
|
const isCurrent = () => gen === refreshGenRef.current && slot === previewGeneration();
|
|
488
489
|
const swap = sceneSwapFor(iframe);
|
|
489
|
-
|
|
490
|
+
const swapWouldReplaceGestureNode =
|
|
491
|
+
!!iframe.contentDocument && isStudioManualEditGestureLiveIn(iframe.contentDocument);
|
|
492
|
+
if (!swap || isRefreshingRef.current || swapWouldReplaceGestureNode)
|
|
493
|
+
return reloadWholeFilm(url.toString());
|
|
490
494
|
swap(url.toString(), isCurrent, cancel.signal).catch((error: unknown) => {
|
|
491
495
|
if (!isCurrent()) return;
|
|
492
496
|
logReload("scene-swap-refused", { reason: String(error) });
|
|
@@ -219,6 +219,22 @@ describe("ThumbnailScheduler", () => {
|
|
|
219
219
|
expect(scheduler.getDiagnostics()).toMatchObject({ queued: 0, active: 0, leases: 0 });
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
it("frees an aborted job's slot at once, even when its loader ignores the abort", async () => {
|
|
223
|
+
const scheduler = new ThumbnailScheduler(
|
|
224
|
+
resolveTimelineViewportBudgets({ concurrentVideoDecodes: 1 }),
|
|
225
|
+
);
|
|
226
|
+
const stuck = deferred<ThumbnailLoadedResult>();
|
|
227
|
+
const lease = scheduler.acquire(
|
|
228
|
+
request("stuck", () => stuck.promise, "visible", { kind: "video" }),
|
|
229
|
+
vi.fn(),
|
|
230
|
+
);
|
|
231
|
+
lease.release();
|
|
232
|
+
const nextLoad = vi.fn(async () => result("next"));
|
|
233
|
+
scheduler.acquire(request("next", nextLoad, "visible", { kind: "video" }), vi.fn());
|
|
234
|
+
await flush();
|
|
235
|
+
expect(nextLoad).toHaveBeenCalledTimes(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
222
238
|
it("disposes a late result exactly once after its final lease releases", async () => {
|
|
223
239
|
const scheduler = new ThumbnailScheduler();
|
|
224
240
|
const pending = deferred<ThumbnailLoadedResult>();
|
|
@@ -310,6 +310,15 @@ export class ThumbnailScheduler {
|
|
|
310
310
|
entry.controller = controller;
|
|
311
311
|
this.activeByBucket[bucket]++;
|
|
312
312
|
this.activeByKind[entry.request.kind]++;
|
|
313
|
+
let slotFree = false;
|
|
314
|
+
const freeSlot = () => {
|
|
315
|
+
if (slotFree) return;
|
|
316
|
+
slotFree = true;
|
|
317
|
+
this.activeByBucket[bucket]--;
|
|
318
|
+
this.activeByKind[entry.request.kind]--;
|
|
319
|
+
this.pump();
|
|
320
|
+
};
|
|
321
|
+
controller.signal.addEventListener("abort", freeSlot, { once: true });
|
|
313
322
|
this.notify(entry);
|
|
314
323
|
|
|
315
324
|
const pending = this.loadWithTimeout(entry, controller);
|
|
@@ -331,8 +340,7 @@ export class ThumbnailScheduler {
|
|
|
331
340
|
})
|
|
332
341
|
.finally(() => {
|
|
333
342
|
if (entry.controller === controller) entry.controller = null;
|
|
334
|
-
|
|
335
|
-
this.activeByKind[entry.request.kind]--;
|
|
343
|
+
freeSlot();
|
|
336
344
|
this.pump();
|
|
337
345
|
});
|
|
338
346
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
4
|
+
import {
|
|
5
|
+
applyMediaMetadataFromElement,
|
|
6
|
+
getTimelineElementSelector,
|
|
7
|
+
isVideoAudible,
|
|
8
|
+
resolveMediaElement,
|
|
9
|
+
} from "./timelineElementHelpers";
|
|
10
|
+
import { readTimelineText, sameTimelineText } from "./timelineText";
|
|
3
11
|
|
|
4
12
|
describe("isVideoAudible — the compiler's data-has-audio rule", () => {
|
|
5
13
|
it("explicit data-has-audio wins", () => {
|
|
@@ -14,3 +22,81 @@ describe("isVideoAudible — the compiler's data-has-audio rule", () => {
|
|
|
14
22
|
expect(isVideoAudible({ tag: "img", hasAudioAttr: null, muted: false })).toBe(false);
|
|
15
23
|
});
|
|
16
24
|
});
|
|
25
|
+
|
|
26
|
+
describe("preview nodes built in another realm", () => {
|
|
27
|
+
// The Studio preview's body carries the editor window's prototypes, so its
|
|
28
|
+
// nodes fail `instanceof` against their own window's HTMLElement.
|
|
29
|
+
function foreignNode(html: string): Element {
|
|
30
|
+
const holder = document.createElement("div");
|
|
31
|
+
holder.innerHTML = html;
|
|
32
|
+
const node = holder.firstElementChild!;
|
|
33
|
+
document.body.appendChild(node);
|
|
34
|
+
Object.setPrototypeOf(node, Object.create(Element.prototype));
|
|
35
|
+
expect(node instanceof HTMLElement).toBe(false);
|
|
36
|
+
return node;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it("gives a row with an id its id selector", () => {
|
|
40
|
+
const el = foreignNode(`<div id="layer-00" class="clip layer"></div>`);
|
|
41
|
+
expect(getTimelineElementSelector(el)).toBe("#layer-00");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("reads media metadata off a foreign <video>", () => {
|
|
45
|
+
const el = foreignNode(`<video src="a.mp4" data-source-duration="12"></video>`);
|
|
46
|
+
expect(resolveMediaElement(el)).toBe(el);
|
|
47
|
+
const entry = { id: "v", tag: "div", start: 0, duration: 4, track: 0 } as TimelineElement;
|
|
48
|
+
applyMediaMetadataFromElement(entry, el);
|
|
49
|
+
expect(entry.sourceDuration).toBe(12);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("readTimelineText", () => {
|
|
54
|
+
function layer(html: string): Element {
|
|
55
|
+
document.body.innerHTML = html;
|
|
56
|
+
return document.body.firstElementChild!;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
it("reads a text layer's words, collapsed to one line, with its font and colour", () => {
|
|
60
|
+
const text = readTimelineText(
|
|
61
|
+
layer(`<h1 style="font-family: Georgia; font-weight: 700; color: rgb(255, 0, 0)">
|
|
62
|
+
Ship <span>it</span>
|
|
63
|
+
</h1>`),
|
|
64
|
+
);
|
|
65
|
+
expect(text).toMatchObject({ value: "Ship it", fontWeight: "700", color: "rgb(255, 0, 0)" });
|
|
66
|
+
expect(text?.fontFamily).toContain("Georgia");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("leaves a layer that holds anything but text, or no words, to its picture", () => {
|
|
70
|
+
expect(
|
|
71
|
+
readTimelineText(layer(`<div><div>Title</div><div><i></i></div></div>`)),
|
|
72
|
+
).toBeUndefined();
|
|
73
|
+
expect(readTimelineText(layer(`<div class="glyph"></div>`))).toBeUndefined();
|
|
74
|
+
expect(readTimelineText(layer(`<section>Words</section>`))).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("keeps a layer that paints an image as a picture, and carries a flat background colour", () => {
|
|
78
|
+
expect(
|
|
79
|
+
readTimelineText(
|
|
80
|
+
layer(`<div style="background-image: linear-gradient(red, blue)">L01</div>`),
|
|
81
|
+
),
|
|
82
|
+
).toBeUndefined();
|
|
83
|
+
expect(
|
|
84
|
+
readTimelineText(layer(`<div style="background-color: rgb(230, 57, 70)">Go</div>`)),
|
|
85
|
+
).toMatchObject({
|
|
86
|
+
value: "Go",
|
|
87
|
+
background: "rgb(230, 57, 70)",
|
|
88
|
+
});
|
|
89
|
+
expect(readTimelineText(layer(`<div>Go</div>`))?.background).toBeUndefined();
|
|
90
|
+
expect(
|
|
91
|
+
readTimelineText(layer(`<div style="background-color: rgba(255, 255, 255, 0.01)">Go</div>`))
|
|
92
|
+
?.background,
|
|
93
|
+
).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("tells a text change apart from an identical re-read", () => {
|
|
97
|
+
const text = { value: "Old", color: "rgb(0, 0, 0)" };
|
|
98
|
+
expect(sameTimelineText(text, { ...text })).toBe(true);
|
|
99
|
+
expect(sameTimelineText(text, { ...text, value: "New" })).toBe(false);
|
|
100
|
+
expect(sameTimelineText(text, undefined)).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { TimelineElement } from "../store/playerStore";
|
|
11
|
+
import { readTimelineText } from "./timelineText";
|
|
11
12
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
12
13
|
import { isFinitePositive } from "./playbackAdapter";
|
|
13
14
|
import { getSourceScopedSelectorIndex } from "../../utils/sourceScopedSelectorIndex";
|
|
14
15
|
import { HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
|
|
15
16
|
import { readElementFades } from "@hyperframes/core/audio-fade";
|
|
17
|
+
import { isHtmlElement, isImageElement, isMediaElement } from "@hyperframes/core/runtime/dom-realm";
|
|
16
18
|
import { elementVolume } from "./storedVolume";
|
|
17
19
|
import {
|
|
18
20
|
type AttrReader,
|
|
@@ -143,11 +145,6 @@ export function furthestClipEndFromSource(source: string): number {
|
|
|
143
145
|
// DOM element type guards
|
|
144
146
|
// ---------------------------------------------------------------------------
|
|
145
147
|
|
|
146
|
-
function isHtmlElement(el: Element): el is HTMLElement {
|
|
147
|
-
const HtmlElementCtor = el.ownerDocument.defaultView?.HTMLElement ?? globalThis.HTMLElement;
|
|
148
|
-
return typeof HtmlElementCtor !== "undefined" && el instanceof HtmlElementCtor;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
148
|
function isCompositionHost(el: Element): boolean {
|
|
152
149
|
return (
|
|
153
150
|
el.hasAttribute("data-composition-id") ||
|
|
@@ -157,16 +154,11 @@ function isCompositionHost(el: Element): boolean {
|
|
|
157
154
|
}
|
|
158
155
|
|
|
159
156
|
export function resolveMediaElement(el: Element): HTMLMediaElement | HTMLImageElement | null {
|
|
160
|
-
|
|
161
|
-
const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;
|
|
162
|
-
const ImageElementCtor = win.HTMLImageElement ?? globalThis.HTMLImageElement;
|
|
163
|
-
if (el instanceof MediaElementCtor || el instanceof ImageElementCtor) return el;
|
|
157
|
+
if (isMediaElement(el) || isImageElement(el)) return el;
|
|
164
158
|
// A composition's media belongs to its own timeline, not the clip's: its length would cap a trim.
|
|
165
159
|
if (isCompositionHost(el)) return null;
|
|
166
160
|
const candidate = el.querySelector("video, audio, img");
|
|
167
|
-
return candidate
|
|
168
|
-
? candidate
|
|
169
|
-
: null;
|
|
161
|
+
return isMediaElement(candidate) || isImageElement(candidate) ? candidate : null;
|
|
170
162
|
}
|
|
171
163
|
|
|
172
164
|
/** The in-point as playback reads it, and the attribute holding it; empty when neither is authored. */
|
|
@@ -255,6 +247,7 @@ function applyFadeMetadataFromElement(entry: TimelineElement, el: Element): void
|
|
|
255
247
|
}
|
|
256
248
|
|
|
257
249
|
export function applyMediaMetadataFromElement(entry: TimelineElement, el: Element): void {
|
|
250
|
+
setOptional(entry, "text", readTimelineText(el));
|
|
258
251
|
applyPlaybackMetadataFromElement(entry, el);
|
|
259
252
|
applyAudioMetadataFromElement(entry, el);
|
|
260
253
|
applyFadeMetadataFromElement(entry, el);
|
|
@@ -266,9 +259,7 @@ export function applyMediaMetadataFromElement(entry: TimelineElement, el: Elemen
|
|
|
266
259
|
const src = mediaEl.getAttribute("src");
|
|
267
260
|
if (src) entry.src = src;
|
|
268
261
|
|
|
269
|
-
|
|
270
|
-
const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;
|
|
271
|
-
if (typeof MediaElementCtor === "undefined" || !(mediaEl instanceof MediaElementCtor)) return;
|
|
262
|
+
if (!isMediaElement(mediaEl)) return;
|
|
272
263
|
|
|
273
264
|
const sourceDurationAttr =
|
|
274
265
|
el.getAttribute("data-source-duration") ?? mediaEl.getAttribute("data-source-duration");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isHtmlElement } from "@hyperframes/core/runtime/dom-realm";
|
|
2
|
+
import { parseCssColor } from "../../components/editor/colorValue";
|
|
3
|
+
import type { TimelineText } from "../store/timelineElement";
|
|
4
|
+
import {
|
|
5
|
+
getCuratedComputedStyles,
|
|
6
|
+
isEditableTextLeaf,
|
|
7
|
+
isTextBearingTag,
|
|
8
|
+
} from "../../components/editor/domEditingDom";
|
|
9
|
+
|
|
10
|
+
/** A layer that paints only text and a flat colour (a text tag whose children are all text leaves,
|
|
11
|
+
* no background image): its words and look. Anything painted beyond that keeps its picture. */
|
|
12
|
+
export function readTimelineText(el: Element): TimelineText | undefined {
|
|
13
|
+
if (!isHtmlElement(el) || !isTextBearingTag(el.localName)) return undefined;
|
|
14
|
+
const children = Array.from(el.children);
|
|
15
|
+
if (!children.every((child) => isHtmlElement(child) && isEditableTextLeaf(child)))
|
|
16
|
+
return undefined;
|
|
17
|
+
const value = el.textContent?.replace(/\s+/g, " ").trim();
|
|
18
|
+
if (!value) return undefined;
|
|
19
|
+
const style = getCuratedComputedStyles(el);
|
|
20
|
+
if ((style["background-image"] ?? "none") !== "none") return undefined;
|
|
21
|
+
const background = style["background-color"];
|
|
22
|
+
return {
|
|
23
|
+
value,
|
|
24
|
+
fontFamily: style["font-family"],
|
|
25
|
+
fontWeight: style["font-weight"],
|
|
26
|
+
color: style.color,
|
|
27
|
+
background: background && parseCssColor(background)?.alpha === 1 ? background : undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function sameTimelineText(a?: TimelineText, b?: TimelineText): boolean {
|
|
32
|
+
if (!a || !b) return a === b;
|
|
33
|
+
return (
|
|
34
|
+
a.value === b.value &&
|
|
35
|
+
a.fontFamily === b.fontFamily &&
|
|
36
|
+
a.fontWeight === b.fontWeight &&
|
|
37
|
+
a.color === b.color &&
|
|
38
|
+
a.background === b.background
|
|
39
|
+
);
|
|
40
|
+
}
|
package/src/player/sceneSwap.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
1
2
|
type SwapWindow = Window & {
|
|
2
3
|
__hfSwapScenes?: (html: string, signal?: AbortSignal) => Promise<void>;
|
|
3
4
|
};
|
|
@@ -72,7 +73,7 @@ export function sceneSwapFor(
|
|
|
72
73
|
);
|
|
73
74
|
cancel?.addEventListener("abort", () => deadline.abort(cancel.reason), { once: true });
|
|
74
75
|
const swapping = (async () => {
|
|
75
|
-
const response = await
|
|
76
|
+
const response = await studioApiFetch(url, { signal: deadline.signal });
|
|
76
77
|
if (!response.ok) throw new Error(`preview request failed with ${response.status}`);
|
|
77
78
|
const html = await response.text();
|
|
78
79
|
if (!isCurrent()) throw new Error("superseded by a newer reload");
|
|
@@ -95,6 +95,17 @@ export interface TimelineElement {
|
|
|
95
95
|
authoredStartIsMasterTime?: boolean;
|
|
96
96
|
/** Legacy marker for an inline sub-composition child; current rows never set it. */
|
|
97
97
|
expandedHostKey?: string;
|
|
98
|
+
/** A text layer's words and look, which its row draws live instead of a captured picture. */
|
|
99
|
+
text?: TimelineText;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TimelineText {
|
|
103
|
+
value: string;
|
|
104
|
+
fontFamily?: string;
|
|
105
|
+
fontWeight?: string;
|
|
106
|
+
color?: string;
|
|
107
|
+
/** The layer's own opaque background colour, when it paints one. */
|
|
108
|
+
background?: string;
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
type RowClock = Pick<
|
|
@@ -156,6 +167,7 @@ export type TimelineElementPatch = Partial<
|
|
|
156
167
|
| "audioGroupHidden"
|
|
157
168
|
| "audioGroupFxChain"
|
|
158
169
|
| "audioGroupAutomation"
|
|
170
|
+
| "text"
|
|
159
171
|
>
|
|
160
172
|
>;
|
|
161
173
|
|
package/src/styles/theme.css
CHANGED
|
@@ -146,6 +146,9 @@
|
|
|
146
146
|
transparent 100%
|
|
147
147
|
);
|
|
148
148
|
--timeline-thumbnail-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
|
|
149
|
+
/* A text clip's strip behind its live words: dark for light text, light for dark text. */
|
|
150
|
+
--timeline-text-clip-dark-bg: #1c2028;
|
|
151
|
+
--timeline-text-clip-light-bg: #e9e6df;
|
|
149
152
|
--timeline-waveform-bar-rgb: 245, 237, 255;
|
|
150
153
|
--timeline-waveform-muted-rgb: 236, 238, 242;
|
|
151
154
|
--timeline-waveform-baseline-rgb: 226, 216, 238;
|
package/src/telemetry/client.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { browserTelemetryAllowed } from "./policy";
|
|
|
9
9
|
import { getBrowserSystemMeta } from "./system";
|
|
10
10
|
import { canaryEventProperties } from "./canary";
|
|
11
11
|
import { recordBreadcrumb } from "./breadcrumbs";
|
|
12
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
12
13
|
|
|
13
14
|
// Write-only PostHog project key, safe to embed in client code.
|
|
14
15
|
const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
|
|
@@ -83,7 +84,7 @@ function send(url: string, payload: string): void {
|
|
|
83
84
|
// Prefer fetch with keepalive (survives page navigation). sendBeacon is a
|
|
84
85
|
// fallback for older runtimes where fetch isn't available.
|
|
85
86
|
try {
|
|
86
|
-
void
|
|
87
|
+
void studioApiFetch(url, {
|
|
87
88
|
method: "POST",
|
|
88
89
|
headers: { "Content-Type": "application/json" },
|
|
89
90
|
body: payload,
|
|
@@ -12,6 +12,7 @@ import { saveProjectFilesWithHistory } from "./studioFileHistory";
|
|
|
12
12
|
import { serializeStudioFileMutation } from "./studioFileMutationCoordinator";
|
|
13
13
|
import { extendRootDurationInSource } from "./rootDuration";
|
|
14
14
|
import { deriveTimelineStoreKeyForDomId } from "../player/lib/timelineElementHelpers";
|
|
15
|
+
import { studioApiFetch } from "./studioApiFetch";
|
|
15
16
|
|
|
16
17
|
function getMaxZIndexFromIframe(iframe: HTMLIFrameElement | null): number {
|
|
17
18
|
try {
|
|
@@ -65,7 +66,7 @@ async function installRegistryItem({
|
|
|
65
66
|
block: RegistryItem;
|
|
66
67
|
compositionFile: string;
|
|
67
68
|
} | null> {
|
|
68
|
-
const response = await
|
|
69
|
+
const response = await studioApiFetch(buildProjectApiPath(projectId, `/registry/install`), {
|
|
69
70
|
method: "POST",
|
|
70
71
|
headers: { "Content-Type": "application/json" },
|
|
71
72
|
body: JSON.stringify({ blockName }),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { elementTargets } from "./elementGsap";
|
|
4
|
+
|
|
5
|
+
it("keeps only the targets GSAP styles: not a plain object, not an XML-namespace element", () => {
|
|
6
|
+
const box = document.createElement("div");
|
|
7
|
+
const item = document.createElementNS("urn:x", "item");
|
|
8
|
+
expect(elementTargets({ targets: () => [{}, item, box, null] })).toEqual([box]);
|
|
9
|
+
});
|
package/src/utils/elementGsap.ts
CHANGED
|
@@ -43,3 +43,10 @@ export function readElementGsapNumber(element: HTMLElement, prop: string): numbe
|
|
|
43
43
|
const value = Number(gsapOf(element)?.getProperty?.(element, prop));
|
|
44
44
|
return Number.isFinite(value) ? value : null;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** The targets CSSPlugin styles: not plain objects (the runtime's filler) or XML-namespace elements. */
|
|
48
|
+
export function elementTargets(tween: { targets?: () => unknown[] }): Element[] {
|
|
49
|
+
return (tween.targets?.() ?? []).filter((t): t is Element =>
|
|
50
|
+
Boolean((t as HTMLElement | null)?.style && (t as Node).nodeType),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -209,7 +209,6 @@ export interface SoftReloadOptions {
|
|
|
209
209
|
currentTimeOverride?: number;
|
|
210
210
|
/** After-write file HTML — the primary source for the authored opacity and transform restore. */
|
|
211
211
|
authoredHtml?: string;
|
|
212
|
-
reparse?: Element[];
|
|
213
212
|
/** Other composition files a reset element is written in, by path; null when one could not be read. */
|
|
214
213
|
nestedFiles?: Map<string, string> | null;
|
|
215
214
|
}
|
|
@@ -274,7 +273,7 @@ export function applySoftReload(
|
|
|
274
273
|
scriptText: string,
|
|
275
274
|
options: SoftReloadOptions = {},
|
|
276
275
|
): SoftReloadResult {
|
|
277
|
-
const { onAsyncFailure, currentTimeOverride, authoredHtml
|
|
276
|
+
const { onAsyncFailure, currentTimeOverride, authoredHtml } = options;
|
|
278
277
|
if (!iframe || !scriptText) return "cannot-soft-reload";
|
|
279
278
|
|
|
280
279
|
const win = iframe.contentWindow as IframeWindow | null;
|
|
@@ -347,7 +346,7 @@ export function applySoftReload(
|
|
|
347
346
|
// fallow-ignore-next-line complexity
|
|
348
347
|
const doReload = () => {
|
|
349
348
|
const timelines = win.__timelines;
|
|
350
|
-
const targets = collectResetTargets(win, doc, targetKeys
|
|
349
|
+
const targets = collectResetTargets(win, doc, targetKeys);
|
|
351
350
|
|
|
352
351
|
// Kill ONLY the target composition's timeline(s) — leaving every other
|
|
353
352
|
// composition's timeline (and its children on the global timeline) intact.
|