@hyperframes/studio 0.7.75 → 0.7.77
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-BrfzYqX3.js → hyperframes-player-CEggaaxR.js} +1 -1
- package/dist/assets/{index-BvGKYVrH.js → index-B1INQNmj.js} +1 -1
- package/dist/assets/{index-CjGZkNxS.js → index-B1Tjjdse.js} +1 -1
- package/dist/assets/index-Bp4jAYZG.js +428 -0
- package/dist/assets/index-CBAfprvx.css +1 -0
- package/dist/index.d.ts +64 -17
- package/dist/index.html +2 -2
- package/dist/index.js +18040 -16293
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +16 -17
- package/src/components/EditorShell.tsx +3 -0
- package/src/components/TimelineToolbar.test.tsx +48 -3
- package/src/components/TimelineToolbar.tsx +89 -36
- package/src/components/editor/AnimationCard.test.tsx +18 -75
- package/src/components/editor/AnimationCard.tsx +26 -2
- package/src/components/editor/GsapAddAnimationControl.tsx +68 -0
- package/src/components/editor/GsapAnimationSection.tsx +18 -157
- package/src/components/editor/KeyframeEaseList.tsx +5 -1
- package/src/components/editor/KeyframeNavigation.tsx +46 -14
- package/src/components/editor/KeyframeNavigationDiamond.test.tsx +59 -0
- package/src/components/editor/MotionPathOverlay.tsx +32 -10
- package/src/components/editor/PropertyPanel.tsx +15 -5
- package/src/components/editor/PropertyPanelFlat.tsx +48 -23
- package/src/components/editor/gsapAnimationCallbacks.test.ts +122 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +108 -4
- package/src/components/editor/keyframeRetime.test.ts +25 -22
- package/src/components/editor/keyframeRetime.ts +1 -2
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +25 -139
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +32 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +21 -4
- package/src/components/nle/PreviewOverlays.tsx +3 -1
- package/src/components/nle/useTimelineEditCallbacks.test.ts +30 -0
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +693 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +197 -57
- package/src/contexts/TimelineEditContext.tsx +1 -1
- package/src/hooks/deleteSelectedKeyframes.ts +24 -8
- package/src/hooks/gsapDragCommit.ts +2 -2
- package/src/hooks/gsapDragPositionCommit.ts +84 -5
- package/src/hooks/gsapKeyframeCacheHelpers.test.ts +176 -1
- package/src/hooks/gsapKeyframeCacheHelpers.ts +93 -52
- package/src/hooks/gsapRuntimeBridge.test.ts +100 -0
- package/src/hooks/gsapScriptCommitHelpers.ts +3 -2
- package/src/hooks/gsapScriptCommitTypes.ts +2 -0
- package/src/hooks/gsapShared.test.ts +109 -1
- package/src/hooks/gsapShared.ts +142 -17
- package/src/hooks/gsapTweenSynth.test.ts +30 -1
- package/src/hooks/gsapTweenSynth.ts +61 -14
- package/src/hooks/keyframeCacheAstLoad.ts +181 -0
- package/src/hooks/timelineEditingHelpers.test.ts +80 -0
- package/src/hooks/timelineMoveAdapter.ts +9 -2
- package/src/hooks/useDomEditWiring.ts +2 -2
- package/src/hooks/useDomSelection.ts +4 -0
- package/src/hooks/useEnableKeyframes.test.ts +122 -0
- package/src/hooks/useEnableKeyframes.ts +116 -56
- package/src/hooks/useGestureCommit.ts +2 -2
- package/src/hooks/useGsapAnimationFetchFallback.ts +1 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +92 -13
- package/src/hooks/useGsapKeyframeOps.ts +30 -13
- package/src/hooks/useGsapScriptCommits.test.tsx +21 -0
- package/src/hooks/useGsapScriptCommits.ts +2 -0
- package/src/hooks/useGsapSelectionHandlers.test.tsx +69 -3
- package/src/hooks/useGsapSelectionHandlers.ts +90 -65
- package/src/hooks/useGsapTweenCache.test.ts +16 -0
- package/src/hooks/useGsapTweenCache.ts +64 -178
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +68 -0
- package/src/hooks/useStudioContextValue.test.ts +93 -0
- package/src/hooks/useStudioContextValue.ts +17 -3
- package/src/hooks/useStudioTestHooks.ts +63 -0
- package/src/hooks/useTimelineEditing.test.tsx +187 -22
- package/src/hooks/useTimelineEditing.ts +43 -32
- package/src/hooks/useTimelineEditingTypes.ts +2 -0
- package/src/hooks/useTimelineGroupEditing.ts +106 -82
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +69 -0
- package/src/player/components/KeyframeDiamondContextMenu.tsx +19 -8
- package/src/player/components/LayerDisclosureRow.tsx +76 -0
- package/src/player/components/Timeline.test.ts +420 -90
- package/src/player/components/Timeline.tsx +80 -78
- package/src/player/components/TimelineCanvas.tsx +33 -21
- package/src/player/components/TimelineClip.tsx +3 -1
- package/src/player/components/TimelineClipDiamonds.test.tsx +540 -16
- package/src/player/components/TimelineClipDiamonds.tsx +320 -117
- package/src/player/components/TimelineDiamondConnectors.tsx +145 -0
- package/src/player/components/TimelineLanes.tsx +153 -79
- package/src/player/components/TimelineOverlays.tsx +4 -18
- package/src/player/components/TimelinePropertyLanes.test.tsx +450 -0
- package/src/player/components/TimelinePropertyLanes.tsx +191 -0
- package/src/player/components/TimelineRazorInteraction.tsx +60 -0
- package/src/player/components/TimelineRuler.tsx +11 -21
- package/src/player/components/TimelineTrackHeader.test.tsx +346 -0
- package/src/player/components/TimelineTrackHeader.tsx +363 -0
- package/src/player/components/TrackClipCount.tsx +17 -0
- package/src/player/components/timelineCallbacks.ts +21 -6
- package/src/player/components/timelineClipDragPreview.test.ts +50 -3
- package/src/player/components/timelineClipDragPreview.ts +33 -15
- package/src/player/components/timelineCollision.ts +4 -13
- package/src/player/components/timelineDiamondTypes.ts +120 -0
- package/src/player/components/timelineDragDrop.ts +8 -3
- package/src/player/components/timelineEditing.test.ts +19 -27
- package/src/player/components/timelineEditing.ts +7 -8
- package/src/player/components/timelineKeyframeIdentity.test.ts +49 -0
- package/src/player/components/timelineKeyframeIdentity.ts +66 -0
- package/src/player/components/timelineLayout.test.ts +162 -9
- package/src/player/components/timelineLayout.ts +198 -38
- package/src/player/components/timelineMarquee.test.ts +76 -21
- package/src/player/components/timelineMarquee.ts +17 -14
- package/src/player/components/trackHeaderLaneState.ts +121 -0
- package/src/player/components/trackHeaderLaneValues.ts +139 -0
- package/src/player/components/useAutoExpandKeyframedClips.test.tsx +75 -0
- package/src/player/components/useAutoExpandKeyframedClips.ts +50 -0
- package/src/player/components/useTimelineClipDrag.ts +4 -1
- package/src/player/components/useTimelineGeometry.ts +4 -1
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +108 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +50 -15
- package/src/player/components/useTimelinePlayhead.ts +21 -12
- package/src/player/components/useTimelineRangeSelection.ts +35 -14
- package/src/player/components/useTimelineTrackLayout.test.ts +53 -0
- package/src/player/components/useTimelineTrackLayout.ts +162 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +21 -12
- package/src/player/lib/timelineElementHelpers.ts +26 -11
- package/src/player/store/keyframeSlice.ts +122 -0
- package/src/player/store/playerStore.test.ts +25 -0
- package/src/player/store/playerStore.ts +7 -43
- package/src/telemetry/events.test.ts +9 -3
- package/src/telemetry/events.ts +16 -7
- package/src/utils/globalTimeCompiler.test.ts +10 -0
- package/src/utils/globalTimeCompiler.ts +3 -3
- package/dist/assets/index-B37rWXo4.css +0 -1
- package/dist/assets/index-DzDajONI.js +0 -428
- package/src/player/components/TimelineDragGhost.tsx +0 -59
- package/src/utils/keyframeSelection.test.ts +0 -45
- package/src/utils/keyframeSelection.ts +0 -29
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The diamond row's data + prop contract, split out of TimelineClipDiamonds so
|
|
3
|
+
* that file stays under the 600-line studio gate. Types and the shared
|
|
4
|
+
* keyframe-identity helper live here; the rendering lives there.
|
|
5
|
+
*/
|
|
6
|
+
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
|
|
7
|
+
|
|
8
|
+
export interface TimelineDiamondKeyframe {
|
|
9
|
+
percentage: number;
|
|
10
|
+
/** Tween-relative percentage (the retime mutation keys on this, not clip %). */
|
|
11
|
+
tweenPercentage?: number;
|
|
12
|
+
propertyGroup?: string;
|
|
13
|
+
animationId?: string;
|
|
14
|
+
properties: Record<string, number | string>;
|
|
15
|
+
ease?: string;
|
|
16
|
+
/** Set when 2+ source animations collide at this percentage (a single inline
|
|
17
|
+
* ease button can't target one): the collapsed row hides the button here. */
|
|
18
|
+
easeAmbiguous?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface KeyframeCacheEntry {
|
|
22
|
+
format: string;
|
|
23
|
+
keyframes: TimelineDiamondKeyframe[];
|
|
24
|
+
ease?: string;
|
|
25
|
+
easeEach?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TimelineClipDiamondsProps {
|
|
29
|
+
keyframesData: KeyframeCacheEntry;
|
|
30
|
+
clipWidthPx: number;
|
|
31
|
+
clipHeightPx: number;
|
|
32
|
+
/** Beat-dot strip is shown on this track → shrink diamonds + drop them into
|
|
33
|
+
* the bottom half so they clear the strip at the top. */
|
|
34
|
+
beatsActive?: boolean;
|
|
35
|
+
accentColor: string;
|
|
36
|
+
isSelected: boolean;
|
|
37
|
+
currentPercentage: number;
|
|
38
|
+
elementId: string;
|
|
39
|
+
selectedKeyframes: ReadonlySet<string>;
|
|
40
|
+
onClickKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
|
41
|
+
onShiftClickKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
|
42
|
+
onContextMenuKeyframe?: (
|
|
43
|
+
e: React.MouseEvent,
|
|
44
|
+
elementId: string,
|
|
45
|
+
keyframe: TimelineKeyframeTarget,
|
|
46
|
+
) => void;
|
|
47
|
+
/** Drag-to-retime: move a keyframe to a new time, preserving its value + ease.
|
|
48
|
+
* `keyframe` identifies the dragged keyframe (clip-relative percentage plus
|
|
49
|
+
* whatever animation identity the row carries); `toClipPercentage` is the
|
|
50
|
+
* neighbour-clamped drop position, also clip-relative. The handler decides
|
|
51
|
+
* move (within the tween) vs resize (past its boundary). */
|
|
52
|
+
onMoveKeyframe?: (
|
|
53
|
+
elementId: string,
|
|
54
|
+
keyframe: TimelineKeyframeTarget,
|
|
55
|
+
toClipPercentage: number,
|
|
56
|
+
) => Promise<boolean>;
|
|
57
|
+
/** Open the segment ease editor for the hovered mid-point button — available on
|
|
58
|
+
* the inline clip row too, not just the expanded lanes. */
|
|
59
|
+
onSelectSegment?: (elementId: string, target: TimelineKeyframeTarget) => void;
|
|
60
|
+
/** Set while resolving a diamond press so the ancestor clip's onClick (which
|
|
61
|
+
* toggles selection off when already selected) ignores the native "click"
|
|
62
|
+
* the browser auto-synthesizes after this button's pointerdown+pointerup. */
|
|
63
|
+
suppressClickRef?: React.RefObject<boolean>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TimelineDiamondLaneProps extends Omit<
|
|
67
|
+
TimelineClipDiamondsProps,
|
|
68
|
+
| "onClickKeyframe"
|
|
69
|
+
| "onShiftClickKeyframe"
|
|
70
|
+
| "onContextMenuKeyframe"
|
|
71
|
+
| "onMoveKeyframe"
|
|
72
|
+
| "onSelectSegment"
|
|
73
|
+
> {
|
|
74
|
+
groupAware?: boolean;
|
|
75
|
+
globalEase?: string;
|
|
76
|
+
onSelectSegment?: (target: TimelineKeyframeTarget) => void;
|
|
77
|
+
onClickKeyframe?: (target: TimelineKeyframeTarget) => void;
|
|
78
|
+
onShiftClickKeyframe?: (target: TimelineKeyframeTarget) => void;
|
|
79
|
+
onContextMenuKeyframe?: (e: React.MouseEvent, target: TimelineKeyframeTarget) => void;
|
|
80
|
+
onMoveKeyframe?: (target: TimelineKeyframeTarget, toClipPercentage: number) => Promise<boolean>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const DIAMOND_RATIO = 0.8;
|
|
84
|
+
// Percentage tolerance for rendering keyframes near clip boundaries. Keyframes
|
|
85
|
+
// slightly outside [0, 100] (from rounding or stale cache during the async
|
|
86
|
+
// persist → reload cycle) are still rendered (the clip is overflow-visible) at
|
|
87
|
+
// their true position rather than hidden.
|
|
88
|
+
export const KF_MIN_PCT = -5;
|
|
89
|
+
export const KF_MAX_PCT = 105;
|
|
90
|
+
|
|
91
|
+
export type DragState = {
|
|
92
|
+
kfKey: string;
|
|
93
|
+
startX: number;
|
|
94
|
+
fromClipPct: number;
|
|
95
|
+
moved: boolean;
|
|
96
|
+
/** Latest pointer x, flushed to the preview once per frame. */
|
|
97
|
+
lastX: number;
|
|
98
|
+
/** Index in the sorted row, needed by the neighbour clamp off the render path. */
|
|
99
|
+
index: number;
|
|
100
|
+
/** Escape was pressed: the drag is dead, and the pointerup that follows is
|
|
101
|
+
* swallowed rather than falling through to the click branch. */
|
|
102
|
+
cancelled?: boolean;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The full identity of a diamond, used by every callback and by the selection
|
|
107
|
+
* key. Collapsed clip rows and expanded property lanes read the same cache, so
|
|
108
|
+
* they must hash a shared keyframe to the same key: dropping the group here for
|
|
109
|
+
* the collapsed row would leave a diamond selected in one view and unselected in
|
|
110
|
+
* the other, and would strip the animation id the retime/delete mutations use to
|
|
111
|
+
* pick between two animations that collide at one percentage.
|
|
112
|
+
*/
|
|
113
|
+
export function keyframeTarget(keyframe: TimelineDiamondKeyframe): TimelineKeyframeTarget {
|
|
114
|
+
return {
|
|
115
|
+
percentage: keyframe.percentage,
|
|
116
|
+
tweenPercentage: keyframe.tweenPercentage,
|
|
117
|
+
propertyGroup: keyframe.propertyGroup,
|
|
118
|
+
animationId: keyframe.animationId,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
TIMELINE_COMPOSITION_MIME,
|
|
6
6
|
} from "../../utils/timelineCompositionDrop";
|
|
7
7
|
import { usePlayerStore } from "../store/playerStore";
|
|
8
|
-
import {
|
|
8
|
+
import { resolveTimelineAssetDrop } from "./timelineLayout";
|
|
9
9
|
import type { TimelineDropCallbacks } from "./timelineCallbacks";
|
|
10
10
|
|
|
11
11
|
interface UseTimelineAssetDropOptions extends TimelineDropCallbacks {
|
|
@@ -13,6 +13,8 @@ interface UseTimelineAssetDropOptions extends TimelineDropCallbacks {
|
|
|
13
13
|
ppsRef: RefObject<number>;
|
|
14
14
|
durationRef: RefObject<number>;
|
|
15
15
|
trackOrderRef: RefObject<number[]>;
|
|
16
|
+
rowHeightsRef: RefObject<readonly number[]>;
|
|
17
|
+
contentOrigin: number;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
type TimelinePlacement = { start: number; track: number };
|
|
@@ -54,6 +56,8 @@ export function useTimelineAssetDrop({
|
|
|
54
56
|
ppsRef,
|
|
55
57
|
durationRef,
|
|
56
58
|
trackOrderRef,
|
|
59
|
+
rowHeightsRef,
|
|
60
|
+
contentOrigin,
|
|
57
61
|
onFileDrop,
|
|
58
62
|
onAssetDrop,
|
|
59
63
|
onBlockDrop,
|
|
@@ -85,10 +89,11 @@ export function useTimelineAssetDrop({
|
|
|
85
89
|
rectTop: rect?.top ?? 0,
|
|
86
90
|
scrollLeft: scroll?.scrollLeft ?? 0,
|
|
87
91
|
scrollTop: scroll?.scrollTop ?? 0,
|
|
92
|
+
contentOrigin,
|
|
88
93
|
pixelsPerSecond: ppsRef.current,
|
|
89
94
|
duration: durationRef.current,
|
|
90
95
|
clampStartToDuration: !usePointerStart,
|
|
91
|
-
|
|
96
|
+
rowHeights: rowHeightsRef.current,
|
|
92
97
|
trackOrder: trackOrderRef.current,
|
|
93
98
|
},
|
|
94
99
|
clientX,
|
|
@@ -99,7 +104,7 @@ export function useTimelineAssetDrop({
|
|
|
99
104
|
track: pointer.track,
|
|
100
105
|
};
|
|
101
106
|
},
|
|
102
|
-
[scrollRef, ppsRef, durationRef, trackOrderRef],
|
|
107
|
+
[scrollRef, ppsRef, durationRef, trackOrderRef, rowHeightsRef, contentOrigin],
|
|
103
108
|
);
|
|
104
109
|
|
|
105
110
|
const handleAssetDrop = useCallback(
|
|
@@ -27,14 +27,13 @@ describe("resolveTimelineMove", () => {
|
|
|
27
27
|
track: 2,
|
|
28
28
|
duration: 2,
|
|
29
29
|
originClientX: 100,
|
|
30
|
-
|
|
30
|
+
originRow: 0,
|
|
31
31
|
pixelsPerSecond: 100,
|
|
32
|
-
trackHeight: 72,
|
|
33
32
|
maxStart: 8,
|
|
34
33
|
trackOrder: [0, 1, 2, 3, 4],
|
|
35
34
|
},
|
|
36
35
|
245,
|
|
37
|
-
|
|
36
|
+
0,
|
|
38
37
|
),
|
|
39
38
|
).toEqual({ start: 2.7, track: 2 });
|
|
40
39
|
});
|
|
@@ -47,14 +46,13 @@ describe("resolveTimelineMove", () => {
|
|
|
47
46
|
track: 1,
|
|
48
47
|
duration: 3,
|
|
49
48
|
originClientX: 200,
|
|
50
|
-
|
|
49
|
+
originRow: 0,
|
|
51
50
|
pixelsPerSecond: 100,
|
|
52
|
-
trackHeight: 72,
|
|
53
51
|
maxStart: 10,
|
|
54
52
|
trackOrder: [0, 1, 5, 9],
|
|
55
53
|
},
|
|
56
54
|
150,
|
|
57
|
-
|
|
55
|
+
190 / 72,
|
|
58
56
|
),
|
|
59
57
|
).toEqual({ start: 1.5, track: 9 });
|
|
60
58
|
});
|
|
@@ -67,14 +65,13 @@ describe("resolveTimelineMove", () => {
|
|
|
67
65
|
track: 0,
|
|
68
66
|
duration: 4,
|
|
69
67
|
originClientX: 300,
|
|
70
|
-
|
|
68
|
+
originRow: 0,
|
|
71
69
|
pixelsPerSecond: 100,
|
|
72
|
-
trackHeight: 72,
|
|
73
70
|
maxStart: 6,
|
|
74
71
|
trackOrder: [0, 10, 20],
|
|
75
72
|
},
|
|
76
73
|
-100,
|
|
77
|
-
-
|
|
74
|
+
-400 / 72,
|
|
78
75
|
),
|
|
79
76
|
).toEqual({ start: 0, track: -1 });
|
|
80
77
|
|
|
@@ -85,14 +82,13 @@ describe("resolveTimelineMove", () => {
|
|
|
85
82
|
track: 10,
|
|
86
83
|
duration: 4,
|
|
87
84
|
originClientX: 300,
|
|
88
|
-
|
|
85
|
+
originRow: 0,
|
|
89
86
|
pixelsPerSecond: 100,
|
|
90
|
-
trackHeight: 72,
|
|
91
87
|
maxStart: 6,
|
|
92
88
|
trackOrder: [0, 10, 20],
|
|
93
89
|
},
|
|
94
90
|
500,
|
|
95
|
-
|
|
91
|
+
0,
|
|
96
92
|
),
|
|
97
93
|
).toEqual({ start: 6, track: 10 });
|
|
98
94
|
});
|
|
@@ -105,14 +101,13 @@ describe("resolveTimelineMove", () => {
|
|
|
105
101
|
track: 0,
|
|
106
102
|
duration: 2,
|
|
107
103
|
originClientX: 100,
|
|
108
|
-
|
|
104
|
+
originRow: 0,
|
|
109
105
|
pixelsPerSecond: 100,
|
|
110
|
-
trackHeight: 72,
|
|
111
106
|
maxStart: 8,
|
|
112
107
|
trackOrder: [0, 10, 20],
|
|
113
108
|
},
|
|
114
109
|
100,
|
|
115
|
-
|
|
110
|
+
-50 / 72,
|
|
116
111
|
),
|
|
117
112
|
).toEqual({ start: 1, track: -1 });
|
|
118
113
|
});
|
|
@@ -125,19 +120,18 @@ describe("resolveTimelineMove", () => {
|
|
|
125
120
|
track: 20,
|
|
126
121
|
duration: 2,
|
|
127
122
|
originClientX: 100,
|
|
128
|
-
|
|
123
|
+
originRow: 0,
|
|
129
124
|
pixelsPerSecond: 100,
|
|
130
|
-
trackHeight: 72,
|
|
131
125
|
maxStart: 8,
|
|
132
126
|
trackOrder: [0, 10, 20],
|
|
133
127
|
},
|
|
134
128
|
100,
|
|
135
|
-
|
|
129
|
+
50 / 72,
|
|
136
130
|
),
|
|
137
131
|
).toEqual({ start: 1, track: 21 });
|
|
138
132
|
});
|
|
139
133
|
|
|
140
|
-
it("accounts for scroll displacement while dragging", () => {
|
|
134
|
+
it("accounts for horizontal scroll displacement while dragging", () => {
|
|
141
135
|
expect(
|
|
142
136
|
resolveTimelineMove(
|
|
143
137
|
{
|
|
@@ -145,18 +139,17 @@ describe("resolveTimelineMove", () => {
|
|
|
145
139
|
track: 0,
|
|
146
140
|
duration: 2,
|
|
147
141
|
originClientX: 100,
|
|
148
|
-
|
|
142
|
+
originRow: 0,
|
|
149
143
|
originScrollLeft: 0,
|
|
150
|
-
originScrollTop: 0,
|
|
151
144
|
currentScrollLeft: 100,
|
|
152
|
-
currentScrollTop: 144,
|
|
153
145
|
pixelsPerSecond: 100,
|
|
154
|
-
trackHeight: 72,
|
|
155
146
|
maxStart: 8,
|
|
147
|
+
// Vertical scroll never reaches here: the drag preview folds it into the
|
|
148
|
+
// row index it passes, because rows no longer share one pixel height.
|
|
156
149
|
trackOrder: [0, 1, 2, 3],
|
|
157
150
|
},
|
|
158
151
|
100,
|
|
159
|
-
|
|
152
|
+
2,
|
|
160
153
|
),
|
|
161
154
|
).toEqual({ start: 2, track: 2 });
|
|
162
155
|
});
|
|
@@ -195,9 +188,8 @@ describe("resolveTimelineMove", () => {
|
|
|
195
188
|
track: 1,
|
|
196
189
|
duration: 2,
|
|
197
190
|
originClientX: 0,
|
|
198
|
-
|
|
191
|
+
originRow: 0,
|
|
199
192
|
pixelsPerSecond: 100,
|
|
200
|
-
trackHeight: 72,
|
|
201
193
|
maxStart: 8,
|
|
202
194
|
trackOrder: [0, 1],
|
|
203
195
|
layerOrder: layers.map((layer) => layer.id),
|
|
@@ -206,7 +198,7 @@ describe("resolveTimelineMove", () => {
|
|
|
206
198
|
stackingElements,
|
|
207
199
|
},
|
|
208
200
|
0,
|
|
209
|
-
-
|
|
201
|
+
-1,
|
|
210
202
|
);
|
|
211
203
|
|
|
212
204
|
expect(result).toEqual({
|
|
@@ -46,13 +46,11 @@ export interface TimelineMoveInput {
|
|
|
46
46
|
track: number;
|
|
47
47
|
duration: number;
|
|
48
48
|
originClientX: number;
|
|
49
|
-
|
|
49
|
+
/** Vertical position as a track-row index, not pixels: rows vary in height. */
|
|
50
|
+
originRow: number;
|
|
50
51
|
originScrollLeft?: number;
|
|
51
|
-
originScrollTop?: number;
|
|
52
52
|
currentScrollLeft?: number;
|
|
53
|
-
currentScrollTop?: number;
|
|
54
53
|
pixelsPerSecond: number;
|
|
55
|
-
trackHeight: number;
|
|
56
54
|
maxStart: number;
|
|
57
55
|
trackOrder: number[];
|
|
58
56
|
layerOrder?: TimelineLayerId[];
|
|
@@ -108,7 +106,7 @@ export function resolveTimelineAutoScroll(
|
|
|
108
106
|
export function resolveTimelineMove(
|
|
109
107
|
input: TimelineMoveInput,
|
|
110
108
|
clientX: number,
|
|
111
|
-
|
|
109
|
+
currentRow: number,
|
|
112
110
|
): {
|
|
113
111
|
start: number;
|
|
114
112
|
track: number;
|
|
@@ -117,11 +115,12 @@ export function resolveTimelineMove(
|
|
|
117
115
|
stackingReorder?: TimelineStackingReorderIntent | null;
|
|
118
116
|
} {
|
|
119
117
|
const scrollDeltaX = (input.currentScrollLeft ?? 0) - (input.originScrollLeft ?? 0);
|
|
120
|
-
const scrollDeltaY = (input.currentScrollTop ?? 0) - (input.originScrollTop ?? 0);
|
|
121
118
|
const deltaTime =
|
|
122
119
|
(clientX - input.originClientX + scrollDeltaX) / Math.max(input.pixelsPerSecond, 1);
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
// Rows, so vertical scroll and per-row heights are the caller's problem: rows
|
|
121
|
+
// have varied in height since lanes expand, and a single trackHeight can't
|
|
122
|
+
// describe them.
|
|
123
|
+
const trackDeltaRaw = currentRow - input.originRow;
|
|
125
124
|
const deltaTrack = Math.round(trackDeltaRaw);
|
|
126
125
|
const nextStart = clamp(
|
|
127
126
|
roundToCentiseconds(input.start + deltaTime),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
timelineKeyframeSelectionKey,
|
|
4
|
+
timelineKeyframeTargetFromSelectionKey,
|
|
5
|
+
} from "./timelineKeyframeIdentity";
|
|
6
|
+
|
|
7
|
+
describe("timeline keyframe selection identity", () => {
|
|
8
|
+
it("round-trips an expanded lane with colon-bearing identities", () => {
|
|
9
|
+
const key = timelineKeyframeSelectionKey("comp#a:child", {
|
|
10
|
+
percentage: 75,
|
|
11
|
+
tweenPercentage: 40,
|
|
12
|
+
propertyGroup: "position",
|
|
13
|
+
animationId: "child:position",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(timelineKeyframeTargetFromSelectionKey("comp#a:child", key)).toEqual({
|
|
17
|
+
percentage: 75,
|
|
18
|
+
tweenPercentage: 40,
|
|
19
|
+
propertyGroup: "position",
|
|
20
|
+
animationId: "child:position",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("does not confuse an expanded lane whose element id extends the active id", () => {
|
|
25
|
+
const key = timelineKeyframeSelectionKey("comp#a:child", {
|
|
26
|
+
percentage: 75,
|
|
27
|
+
tweenPercentage: 40,
|
|
28
|
+
propertyGroup: "position",
|
|
29
|
+
animationId: "child-position",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(timelineKeyframeTargetFromSelectionKey("comp#a", key)).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Timeline.tsx still writes the collapsed form for clip-lane shift-clicks, and
|
|
36
|
+
// an element id can itself contain a colon — the split has to be the LAST one.
|
|
37
|
+
it("splits the collapsed key at the last colon so a colon-bearing id survives", () => {
|
|
38
|
+
expect(timelineKeyframeTargetFromSelectionKey("a:b", "a:b:40")).toEqual({ percentage: 40 });
|
|
39
|
+
expect(timelineKeyframeTargetFromSelectionKey("a", "a:b:40")).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("retains the collapsed key fallback and rejects malformed percentages", () => {
|
|
43
|
+
expect(timelineKeyframeTargetFromSelectionKey("comp#a", "comp#a:30")).toEqual({
|
|
44
|
+
percentage: 30,
|
|
45
|
+
});
|
|
46
|
+
expect(timelineKeyframeTargetFromSelectionKey("comp#a", "comp#a:NaN")).toBeNull();
|
|
47
|
+
expect(timelineKeyframeTargetFromSelectionKey("comp#a", "comp#b:30")).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface TimelineKeyframeTarget {
|
|
2
|
+
percentage: number;
|
|
3
|
+
tweenPercentage?: number;
|
|
4
|
+
propertyGroup?: string;
|
|
5
|
+
animationId?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Note the asymmetry: `tweenPercentage` is optional here and defaults to
|
|
10
|
+
* `percentage`, but the reader treats both slots as authoritative. Callers must
|
|
11
|
+
* therefore keep the pair consistent — writing a new `tweenPercentage` without
|
|
12
|
+
* the matching `percentage` hashes two logically-equal selections to different
|
|
13
|
+
* keys.
|
|
14
|
+
*/
|
|
15
|
+
export function timelineKeyframeSelectionKey(
|
|
16
|
+
elementId: string,
|
|
17
|
+
target: TimelineKeyframeTarget,
|
|
18
|
+
): string {
|
|
19
|
+
if (!target.propertyGroup) return `${elementId}:${target.percentage}`;
|
|
20
|
+
return JSON.stringify([
|
|
21
|
+
elementId,
|
|
22
|
+
target.propertyGroup,
|
|
23
|
+
target.animationId ?? "",
|
|
24
|
+
target.percentage,
|
|
25
|
+
target.tweenPercentage ?? target.percentage,
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function timelineKeyframeTargetFromSelectionKey(
|
|
30
|
+
elementId: string,
|
|
31
|
+
key: string,
|
|
32
|
+
): TimelineKeyframeTarget | null {
|
|
33
|
+
if (key.startsWith("[")) {
|
|
34
|
+
let decoded: unknown;
|
|
35
|
+
try {
|
|
36
|
+
decoded = JSON.parse(key);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(decoded) || decoded.length !== 5) return null;
|
|
41
|
+
const [selectedElementId, propertyGroup, animationId, percentage, tweenPercentage] = decoded;
|
|
42
|
+
if (
|
|
43
|
+
selectedElementId !== elementId ||
|
|
44
|
+
typeof propertyGroup !== "string" ||
|
|
45
|
+
propertyGroup.length === 0 ||
|
|
46
|
+
typeof animationId !== "string" ||
|
|
47
|
+
typeof percentage !== "number" ||
|
|
48
|
+
!Number.isFinite(percentage) ||
|
|
49
|
+
typeof tweenPercentage !== "number" ||
|
|
50
|
+
!Number.isFinite(tweenPercentage)
|
|
51
|
+
) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
propertyGroup,
|
|
56
|
+
animationId: animationId || undefined,
|
|
57
|
+
percentage,
|
|
58
|
+
tweenPercentage,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const separator = key.lastIndexOf(":");
|
|
63
|
+
if (separator < 0 || key.slice(0, separator) !== elementId) return null;
|
|
64
|
+
const percentage = Number(key.slice(separator + 1));
|
|
65
|
+
return Number.isFinite(percentage) ? { percentage } : null;
|
|
66
|
+
}
|
|
@@ -1,17 +1,99 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
CLIP_Y,
|
|
4
|
+
INSERT_BOUNDARY_BAND,
|
|
5
|
+
getTimelineInsertBoundaryBand,
|
|
3
6
|
RULER_H,
|
|
4
7
|
TRACK_H,
|
|
8
|
+
LANE_H,
|
|
5
9
|
TRACKS_TOP_PAD,
|
|
6
10
|
TRACKS_BOTTOM_PAD,
|
|
7
11
|
GUTTER,
|
|
8
12
|
TRACKS_LEFT_PAD,
|
|
9
13
|
getTimelineRowTop,
|
|
14
|
+
getTimelineScrubTime,
|
|
10
15
|
getTimelineRowFromY,
|
|
16
|
+
getTimelineRowOffsets,
|
|
11
17
|
getTimelineCanvasHeight,
|
|
18
|
+
trackHeights,
|
|
12
19
|
resolveTimelineAssetDrop,
|
|
13
20
|
} from "./timelineLayout";
|
|
14
21
|
|
|
22
|
+
/** N collapsed rows, the shape every caller passes when nothing is expanded. */
|
|
23
|
+
const baseRows = (count: number) => Array.from({ length: count }, () => TRACK_H);
|
|
24
|
+
|
|
25
|
+
describe("variable timeline row geometry", () => {
|
|
26
|
+
const tracks = [
|
|
27
|
+
[{ clipId: "a", laneCount: 0 }],
|
|
28
|
+
[{ clipId: "b", laneCount: 2 }],
|
|
29
|
+
[{ clipId: "c", laneCount: 1 }],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
it("resolves every row to the base height when no clip is expanded", () => {
|
|
33
|
+
expect(trackHeights(tracks)).toEqual([TRACK_H, TRACK_H, TRACK_H]);
|
|
34
|
+
expect(trackHeights([[], [], []])).toEqual([TRACK_H, TRACK_H, TRACK_H]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("adds one lane height per lane on an expanded clip", () => {
|
|
38
|
+
expect(trackHeights(tracks, new Set(["b"]))).toEqual([TRACK_H, TRACK_H + 2 * LANE_H, TRACK_H]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("derives row tops from cumulative offsets", () => {
|
|
42
|
+
const heights = trackHeights(tracks, new Set(["b"]));
|
|
43
|
+
expect(getTimelineRowOffsets(heights)).toEqual([
|
|
44
|
+
0,
|
|
45
|
+
TRACK_H,
|
|
46
|
+
2 * TRACK_H + 2 * LANE_H,
|
|
47
|
+
3 * TRACK_H + 2 * LANE_H,
|
|
48
|
+
]);
|
|
49
|
+
expect(getTimelineRowTop(2, heights)).toBe(RULER_H + TRACKS_TOP_PAD + 2 * TRACK_H + 2 * LANE_H);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("maps y inside an expanded lane region back to the expanded track", () => {
|
|
53
|
+
const heights = trackHeights(tracks, new Set(["b"]));
|
|
54
|
+
const yInSecondExpandedLane = getTimelineRowTop(1, heights) + TRACK_H + LANE_H * 1.5;
|
|
55
|
+
const row = getTimelineRowFromY(yInSecondExpandedLane, heights);
|
|
56
|
+
expect(Math.floor(row)).toBe(1);
|
|
57
|
+
expect(row).toBeGreaterThan(1.5);
|
|
58
|
+
expect(row).toBeLessThan(2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("sums resolved row heights into the canvas height", () => {
|
|
62
|
+
const heights = trackHeights(tracks, new Set(["b"]));
|
|
63
|
+
expect(getTimelineCanvasHeight(heights)).toBe(
|
|
64
|
+
RULER_H + TRACKS_TOP_PAD + 3 * TRACK_H + 2 * LANE_H + TRACKS_BOTTOM_PAD,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("collapsed timeline row geometry characterization", () => {
|
|
70
|
+
it.each([
|
|
71
|
+
[0, 74],
|
|
72
|
+
[1, 122],
|
|
73
|
+
[4, 266],
|
|
74
|
+
])("keeps row %i at content y=%i", (row, expectedTop) => {
|
|
75
|
+
expect(getTimelineRowTop(row)).toBe(expectedTop);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it.each([
|
|
79
|
+
[74, 0],
|
|
80
|
+
[86, 0.25],
|
|
81
|
+
[146, 1.5],
|
|
82
|
+
[290, 4.5],
|
|
83
|
+
])("maps content y=%i to fractional row %f", (contentY, expectedRow) => {
|
|
84
|
+
expect(getTimelineRowFromY(contentY)).toBe(expectedRow);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it.each([
|
|
88
|
+
[0, 146],
|
|
89
|
+
[1, 194],
|
|
90
|
+
[3, 290],
|
|
91
|
+
[5, 386],
|
|
92
|
+
])("keeps the %i-track canvas height at %i", (trackCount, expectedHeight) => {
|
|
93
|
+
expect(getTimelineCanvasHeight(baseRows(trackCount))).toBe(expectedHeight);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
15
97
|
describe("track-area breathing pad y-math", () => {
|
|
16
98
|
describe("getTimelineRowTop", () => {
|
|
17
99
|
it("offsets the first lane below the ruler by the top pad", () => {
|
|
@@ -53,20 +135,16 @@ describe("track-area breathing pad y-math", () => {
|
|
|
53
135
|
|
|
54
136
|
describe("getTimelineCanvasHeight", () => {
|
|
55
137
|
it("reserves ruler + top pad + lanes + bottom pad", () => {
|
|
56
|
-
expect(getTimelineCanvasHeight(
|
|
57
|
-
expect(getTimelineCanvasHeight(3)).toBe(
|
|
138
|
+
expect(getTimelineCanvasHeight([])).toBe(RULER_H + TRACKS_TOP_PAD + TRACKS_BOTTOM_PAD);
|
|
139
|
+
expect(getTimelineCanvasHeight(baseRows(3))).toBe(
|
|
58
140
|
RULER_H + TRACKS_TOP_PAD + 3 * TRACK_H + TRACKS_BOTTOM_PAD,
|
|
59
141
|
);
|
|
60
142
|
});
|
|
61
143
|
|
|
62
|
-
it("clamps a negative track count to zero lanes", () => {
|
|
63
|
-
expect(getTimelineCanvasHeight(-4)).toBe(RULER_H + TRACKS_TOP_PAD + TRACKS_BOTTOM_PAD);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
144
|
it("leaves room below the last lane for a drag-into-void new track", () => {
|
|
67
145
|
// The gap below the final lane must be at least a full track height so a
|
|
68
146
|
// clip can be dropped there to create a new bottom track.
|
|
69
|
-
const oneLane = getTimelineCanvasHeight(1);
|
|
147
|
+
const oneLane = getTimelineCanvasHeight(baseRows(1));
|
|
70
148
|
const lastLaneBottom = getTimelineRowTop(0) + TRACK_H;
|
|
71
149
|
expect(oneLane - lastLaneBottom).toBeGreaterThanOrEqual(TRACK_H);
|
|
72
150
|
});
|
|
@@ -78,15 +156,16 @@ describe("track-area breathing pad y-math", () => {
|
|
|
78
156
|
rectTop: 0,
|
|
79
157
|
scrollLeft: 0,
|
|
80
158
|
scrollTop: 0,
|
|
159
|
+
contentOrigin: GUTTER,
|
|
81
160
|
pixelsPerSecond: 100,
|
|
82
161
|
duration: 60,
|
|
83
|
-
|
|
162
|
+
rowHeights: baseRows(3),
|
|
84
163
|
trackOrder: [0, 1, 2],
|
|
85
164
|
};
|
|
86
165
|
|
|
87
166
|
it("drops onto lane 0 when the pointer is in the middle of the first lane", () => {
|
|
88
167
|
const clientY = getTimelineRowTop(0) + TRACK_H / 2;
|
|
89
|
-
const clientX = GUTTER +
|
|
168
|
+
const clientX = GUTTER + 100; // t = 1s (contentOrigin = GUTTER)
|
|
90
169
|
const { start, track } = resolveTimelineAssetDrop(base, clientX, clientY);
|
|
91
170
|
expect(track).toBe(0);
|
|
92
171
|
expect(start).toBe(1);
|
|
@@ -103,5 +182,79 @@ describe("track-area breathing pad y-math", () => {
|
|
|
103
182
|
const { track } = resolveTimelineAssetDrop(base, GUTTER, clientY);
|
|
104
183
|
expect(track).toBe(3); // max(trackOrder)+1
|
|
105
184
|
});
|
|
185
|
+
|
|
186
|
+
it("keeps a drop in an expanded lane region on that track", () => {
|
|
187
|
+
const rowHeights = [TRACK_H + 2 * LANE_H, TRACK_H, TRACK_H];
|
|
188
|
+
const clientY = getTimelineRowTop(0, rowHeights) + TRACK_H + LANE_H;
|
|
189
|
+
const { track } = resolveTimelineAssetDrop({ ...base, rowHeights }, GUTTER, clientY);
|
|
190
|
+
expect(track).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("getTimelineScrubTime", () => {
|
|
196
|
+
const at = (clientX: number, duration = 10) =>
|
|
197
|
+
getTimelineScrubTime({
|
|
198
|
+
clientX,
|
|
199
|
+
viewportLeft: 0,
|
|
200
|
+
scrollLeft: 0,
|
|
201
|
+
contentOrigin: GUTTER + TRACKS_LEFT_PAD,
|
|
202
|
+
pixelsPerSecond: 100,
|
|
203
|
+
duration,
|
|
204
|
+
});
|
|
205
|
+
const origin = GUTTER + TRACKS_LEFT_PAD;
|
|
206
|
+
|
|
207
|
+
it("maps the content origin to t=0", () => {
|
|
208
|
+
expect(at(origin)).toBe(0);
|
|
209
|
+
expect(at(origin + 250)).toBe(2.5);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// The bug: a pointer left of the origin used to abort the scrub instead of
|
|
213
|
+
// clamping, so dragging the playhead to the start only worked if a sample
|
|
214
|
+
// happened to land in the few px before t=0.
|
|
215
|
+
it("clamps a pointer left of the origin to 0 instead of dropping the scrub", () => {
|
|
216
|
+
expect(at(origin - 1)).toBe(0);
|
|
217
|
+
expect(at(origin - 500)).toBe(0);
|
|
218
|
+
expect(at(0)).toBe(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("clamps past the end to the duration", () => {
|
|
222
|
+
expect(at(origin + 5000)).toBe(10);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("returns 0 for a degenerate zoom or duration", () => {
|
|
226
|
+
expect(
|
|
227
|
+
getTimelineScrubTime({
|
|
228
|
+
clientX: 500,
|
|
229
|
+
viewportLeft: 0,
|
|
230
|
+
scrollLeft: 0,
|
|
231
|
+
contentOrigin: GUTTER + TRACKS_LEFT_PAD,
|
|
232
|
+
pixelsPerSecond: 0,
|
|
233
|
+
duration: 10,
|
|
234
|
+
}),
|
|
235
|
+
).toBe(0);
|
|
236
|
+
expect(at(origin + 250, Number.NaN)).toBe(0);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// The only production hook keeping resolveInsertRow's band aligned with the
|
|
241
|
+
// rendered clip inset once rows can be taller than TRACK_H. Pinned directly so a
|
|
242
|
+
// change to CLIP_Y or the invalid-height fallback can't silently drift it.
|
|
243
|
+
describe("getTimelineInsertBoundaryBand", () => {
|
|
244
|
+
it("matches the fixed band for a plain track row", () => {
|
|
245
|
+
expect(getTimelineInsertBoundaryBand(TRACK_H)).toBe(INSERT_BOUNDARY_BAND);
|
|
246
|
+
expect(getTimelineInsertBoundaryBand(TRACK_H)).toBe(CLIP_Y / TRACK_H);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("shrinks as the row grows, so the band stays CLIP_Y pixels tall", () => {
|
|
250
|
+
const expanded = TRACK_H + 2 * LANE_H;
|
|
251
|
+
expect(getTimelineInsertBoundaryBand(expanded)).toBeCloseTo(CLIP_Y / expanded, 10);
|
|
252
|
+
expect(getTimelineInsertBoundaryBand(expanded)).toBeLessThan(INSERT_BOUNDARY_BAND);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("falls back to the plain-track band for a height that is not usable", () => {
|
|
256
|
+
for (const height of [0, -10, Number.NaN]) {
|
|
257
|
+
expect(getTimelineInsertBoundaryBand(height)).toBe(INSERT_BOUNDARY_BAND);
|
|
258
|
+
}
|
|
106
259
|
});
|
|
107
260
|
});
|