@hyperframes/studio 0.7.89 → 0.7.92
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-DVTnBvSO.js → hyperframes-player-9qOa08Tv.js} +1 -1
- package/dist/assets/{index-Qvx7boWH.js → index-Bw4lNdtO.js} +1 -1
- package/dist/assets/{index-ulQEdWKo.js → index-CDTN2ZVa.js} +1 -1
- package/dist/assets/index-DIntkrQl.js +428 -0
- package/dist/index.d.ts +20 -16
- package/dist/index.html +1 -1
- package/dist/index.js +6495 -5429
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanel.tsx +1 -1
- package/src/components/editor/GsapAnimationList.tsx +84 -0
- package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
- package/src/components/editor/GsapAnimationSection.tsx +11 -41
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/PropertyPanelFlat.tsx +29 -26
- package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
- package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
- package/src/components/editor/propertyPanelTypes.ts +1 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
- package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
- package/src/components/renders/useRenderQueue.ts +16 -3
- package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- package/src/player/components/BeatStrip.test.tsx +374 -0
- package/src/player/components/BeatStrip.tsx +295 -37
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
- package/src/player/components/Timeline.test.ts +2 -3
- package/src/player/components/Timeline.tsx +47 -48
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.tsx +6 -3
- package/src/player/components/TimelineClipDiamonds.test.tsx +442 -18
- package/src/player/components/TimelineClipDiamonds.tsx +66 -211
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +0 -1
- package/src/player/components/TimelineLanes.tsx +3 -2
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- package/src/player/components/timelineCallbacks.ts +1 -1
- package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
- package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
- package/src/player/components/timelineClipDragPreview.test.ts +33 -0
- package/src/player/components/timelineClipDragPreview.ts +18 -12
- package/src/player/components/timelineClipDragTypes.ts +12 -0
- package/src/player/components/timelineDiamondTypes.ts +5 -15
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineLaneProps.ts +0 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- package/src/player/components/timelineTestViewport.ts +13 -0
- package/src/player/components/timelineViewModel.ts +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
- package/src/player/components/useTimelineClipDrag.ts +112 -70
- package/src/player/components/useTimelineClipRenderWindow.ts +4 -4
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
- package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
- package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
- package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
- package/src/player/components/useTimelineRangeSelection.ts +202 -69
- package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
- package/src/player/components/useTimelineRowVirtualization.ts +11 -4
- package/src/player/components/useTrackGapMenu.test.tsx +64 -0
- package/src/player/components/useTrackGapMenu.ts +32 -9
- package/src/player/store/keyframeSlice.ts +59 -18
- package/src/player/store/playerStore.test.ts +76 -0
- package/src/player/store/playerStore.ts +19 -11
- package/src/telemetry/canary.test.ts +325 -0
- package/src/telemetry/canary.ts +268 -0
- package/src/telemetry/client.test.ts +8 -3
- package/src/telemetry/client.ts +13 -43
- package/src/telemetry/config.ts +16 -2
- package/src/telemetry/distinctId.test.ts +35 -0
- package/src/telemetry/distinctId.ts +11 -2
- package/src/telemetry/policy.test.ts +81 -0
- package/src/telemetry/policy.ts +104 -0
- package/src/utils/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-CddKwfQ1.js +0 -428
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act, createElement } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
6
|
+
import { usePlayerStore } from "../store/playerStore";
|
|
7
|
+
import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu";
|
|
8
|
+
import { TimelineOverlays, resolveTimelineContextElement } from "./TimelineOverlays";
|
|
9
|
+
import { defaultTimelineTheme } from "./timelineTheme";
|
|
10
|
+
|
|
11
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
|
+
|
|
13
|
+
const roots: Root[] = [];
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
for (const root of roots.splice(0)) act(() => root.unmount());
|
|
16
|
+
document.body.innerHTML = "";
|
|
17
|
+
usePlayerStore.setState({ selectedElementId: null, timelineSessionEpoch: 0 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const captured: TimelineElement = {
|
|
21
|
+
id: "child",
|
|
22
|
+
key: "parent::child",
|
|
23
|
+
tag: "div",
|
|
24
|
+
start: 1,
|
|
25
|
+
duration: 2,
|
|
26
|
+
track: 3,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("resolveTimelineContextElement", () => {
|
|
30
|
+
it("returns the current expanded model instead of the captured snapshot", () => {
|
|
31
|
+
const current = { ...captured, start: 4, track: 7 };
|
|
32
|
+
|
|
33
|
+
expect(
|
|
34
|
+
resolveTimelineContextElement({
|
|
35
|
+
capturedElement: captured,
|
|
36
|
+
targetSessionEpoch: 2,
|
|
37
|
+
sessionEpoch: 2,
|
|
38
|
+
selectedElementId: "parent::child",
|
|
39
|
+
elements: [current],
|
|
40
|
+
}),
|
|
41
|
+
).toBe(current);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("resolves synthetic expanded children that are absent from raw store elements", () => {
|
|
45
|
+
expect(
|
|
46
|
+
resolveTimelineContextElement({
|
|
47
|
+
capturedElement: captured,
|
|
48
|
+
targetSessionEpoch: 2,
|
|
49
|
+
sessionEpoch: 2,
|
|
50
|
+
selectedElementId: "parent::child",
|
|
51
|
+
elements: [captured],
|
|
52
|
+
}),
|
|
53
|
+
).toBe(captured);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("rejects stale sessions, changed selection, and removed elements", () => {
|
|
57
|
+
const input = {
|
|
58
|
+
capturedElement: captured,
|
|
59
|
+
targetSessionEpoch: 2,
|
|
60
|
+
sessionEpoch: 2,
|
|
61
|
+
selectedElementId: "parent::child",
|
|
62
|
+
elements: [captured],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
expect(resolveTimelineContextElement({ ...input, sessionEpoch: 3 })).toBeNull();
|
|
66
|
+
expect(resolveTimelineContextElement({ ...input, selectedElementId: "other" })).toBeNull();
|
|
67
|
+
expect(resolveTimelineContextElement({ ...input, elements: [] })).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function renderKeyframeOverlay(options: {
|
|
72
|
+
capturedElement: TimelineElement;
|
|
73
|
+
currentElement: TimelineElement;
|
|
74
|
+
setKfContextMenu?: ReturnType<typeof vi.fn>;
|
|
75
|
+
onDeleteAllKeyframes?: ReturnType<typeof vi.fn>;
|
|
76
|
+
}) {
|
|
77
|
+
const container = document.createElement("div");
|
|
78
|
+
document.body.appendChild(container);
|
|
79
|
+
const root = createRoot(container);
|
|
80
|
+
roots.push(root);
|
|
81
|
+
const elements = [options.currentElement];
|
|
82
|
+
const setKfContextMenu = options.setKfContextMenu ?? vi.fn();
|
|
83
|
+
const onDeleteAllKeyframes = options.onDeleteAllKeyframes ?? vi.fn();
|
|
84
|
+
const menu: KeyframeDiamondContextMenuState = {
|
|
85
|
+
x: 10,
|
|
86
|
+
y: 10,
|
|
87
|
+
sessionEpoch: 2,
|
|
88
|
+
element: options.capturedElement,
|
|
89
|
+
elementId: options.capturedElement.key ?? options.capturedElement.id,
|
|
90
|
+
percentage: 50,
|
|
91
|
+
animationId: "child-position",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
act(() => {
|
|
95
|
+
usePlayerStore.setState({
|
|
96
|
+
selectedElementId: options.capturedElement.key ?? options.capturedElement.id,
|
|
97
|
+
timelineSessionEpoch: 2,
|
|
98
|
+
});
|
|
99
|
+
root.render(
|
|
100
|
+
createElement(TimelineOverlays, {
|
|
101
|
+
elements,
|
|
102
|
+
elementsRef: { current: elements },
|
|
103
|
+
theme: defaultTimelineTheme,
|
|
104
|
+
showShortcutHint: false,
|
|
105
|
+
showPopover: false,
|
|
106
|
+
rangeSelection: null,
|
|
107
|
+
setShowPopover: vi.fn(),
|
|
108
|
+
setRangeSelection: vi.fn(),
|
|
109
|
+
kfContextMenu: menu,
|
|
110
|
+
setKfContextMenu,
|
|
111
|
+
onDeleteKeyframe: vi.fn(),
|
|
112
|
+
onDeleteAllKeyframes,
|
|
113
|
+
onMoveKeyframeToPlayhead: vi.fn(),
|
|
114
|
+
clipContextMenu: null,
|
|
115
|
+
setClipContextMenu: vi.fn(),
|
|
116
|
+
currentTime: 0,
|
|
117
|
+
onSplitElement: vi.fn(),
|
|
118
|
+
pinZoomBeforeEdit: vi.fn(),
|
|
119
|
+
onDeleteElement: vi.fn(),
|
|
120
|
+
gapContextMenu: null,
|
|
121
|
+
onDismissGapContextMenu: vi.fn(),
|
|
122
|
+
onCloseTrackGap: vi.fn(),
|
|
123
|
+
onCloseAllTrackGaps: vi.fn(),
|
|
124
|
+
onHoverGapAction: vi.fn(),
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
return { setKfContextMenu, onDeleteAllKeyframes };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe("TimelineOverlays context lifecycle", () => {
|
|
132
|
+
it("dismisses a keyframe menu when its selected target becomes stale", () => {
|
|
133
|
+
const setKfContextMenu = vi.fn();
|
|
134
|
+
renderKeyframeOverlay({
|
|
135
|
+
capturedElement: captured,
|
|
136
|
+
currentElement: captured,
|
|
137
|
+
setKfContextMenu,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
act(() => usePlayerStore.setState({ selectedElementId: "other" }));
|
|
141
|
+
|
|
142
|
+
expect(setKfContextMenu).toHaveBeenCalledExactlyOnceWith(null);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("dispatches a menu action with the current model element", () => {
|
|
146
|
+
const current = { ...captured, start: 4, track: 7 };
|
|
147
|
+
const onDeleteAllKeyframes = vi.fn();
|
|
148
|
+
renderKeyframeOverlay({
|
|
149
|
+
capturedElement: captured,
|
|
150
|
+
currentElement: current,
|
|
151
|
+
onDeleteAllKeyframes,
|
|
152
|
+
});
|
|
153
|
+
const button = Array.from(document.body.querySelectorAll("button")).find(
|
|
154
|
+
(candidate) => candidate.textContent === "Delete All Keyframes",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
act(() => button?.click());
|
|
158
|
+
|
|
159
|
+
expect(onDeleteAllKeyframes).toHaveBeenCalledExactlyOnceWith(current, "child-position");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { useEffect, type MutableRefObject } from "react";
|
|
1
2
|
import type { TimelineElement } from "../store/playerStore";
|
|
3
|
+
import { usePlayerStore } from "../store/playerStore";
|
|
2
4
|
import type { TimelineTheme } from "./timelineTheme";
|
|
3
5
|
import type { TimelineRangeSelection } from "./timelineEditing";
|
|
4
6
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
@@ -11,10 +13,11 @@ import { ClipContextMenu } from "./ClipContextMenu";
|
|
|
11
13
|
import { TrackGapContextMenu } from "./TrackGapContextMenu";
|
|
12
14
|
import { TimelineShortcutHint } from "./TimelineShortcutHint";
|
|
13
15
|
|
|
14
|
-
interface ClipContextMenuState {
|
|
16
|
+
export interface ClipContextMenuState {
|
|
15
17
|
x: number;
|
|
16
18
|
y: number;
|
|
17
19
|
element: TimelineElement;
|
|
20
|
+
sessionEpoch: number;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/** Resolved model for the empty-lane-space (track gap) context menu. */
|
|
@@ -28,6 +31,8 @@ interface TrackGapContextMenuState {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
interface TimelineOverlaysProps {
|
|
34
|
+
elements: readonly TimelineElement[];
|
|
35
|
+
elementsRef: MutableRefObject<readonly TimelineElement[]>;
|
|
31
36
|
theme: TimelineTheme;
|
|
32
37
|
showShortcutHint: boolean;
|
|
33
38
|
showPopover: boolean;
|
|
@@ -52,10 +57,49 @@ interface TimelineOverlaysProps {
|
|
|
52
57
|
onHoverGapAction: (action: "close-gap" | "close-all" | null) => void;
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
interface TimelineContextTargetInput {
|
|
61
|
+
capturedElement: TimelineElement;
|
|
62
|
+
targetSessionEpoch: number | undefined;
|
|
63
|
+
sessionEpoch: number;
|
|
64
|
+
selectedElementId: string | null;
|
|
65
|
+
elements: readonly TimelineElement[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The captured project session and current selection jointly own a context target. */
|
|
69
|
+
export function resolveTimelineContextElement({
|
|
70
|
+
capturedElement,
|
|
71
|
+
targetSessionEpoch,
|
|
72
|
+
sessionEpoch,
|
|
73
|
+
selectedElementId,
|
|
74
|
+
elements,
|
|
75
|
+
}: TimelineContextTargetInput): TimelineElement | null {
|
|
76
|
+
const identity = capturedElement.key ?? capturedElement.id;
|
|
77
|
+
if (targetSessionEpoch !== sessionEpoch) return null;
|
|
78
|
+
if (selectedElementId !== identity) return null;
|
|
79
|
+
return elements.find((element) => (element.key ?? element.id) === identity) ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readTimelineContextElement(
|
|
83
|
+
capturedElement: TimelineElement,
|
|
84
|
+
targetSessionEpoch: number | undefined,
|
|
85
|
+
elements: readonly TimelineElement[],
|
|
86
|
+
): TimelineElement | null {
|
|
87
|
+
const state = usePlayerStore.getState();
|
|
88
|
+
return resolveTimelineContextElement({
|
|
89
|
+
capturedElement,
|
|
90
|
+
targetSessionEpoch,
|
|
91
|
+
sessionEpoch: state.timelineSessionEpoch,
|
|
92
|
+
selectedElementId: state.selectedElementId,
|
|
93
|
+
elements,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
55
97
|
// The timeline's floating overlays, rendered as siblings above the scroll area:
|
|
56
98
|
// the shortcut hint, the range-edit popover, the keyframe-diamond context menu,
|
|
57
99
|
// and the clip context menu.
|
|
58
100
|
export function TimelineOverlays({
|
|
101
|
+
elements,
|
|
102
|
+
elementsRef,
|
|
59
103
|
theme,
|
|
60
104
|
showShortcutHint,
|
|
61
105
|
showPopover,
|
|
@@ -79,6 +123,39 @@ export function TimelineOverlays({
|
|
|
79
123
|
onCloseAllTrackGaps,
|
|
80
124
|
onHoverGapAction,
|
|
81
125
|
}: TimelineOverlaysProps) {
|
|
126
|
+
const selectedElementId = usePlayerStore((state) => state.selectedElementId);
|
|
127
|
+
const sessionEpoch = usePlayerStore((state) => state.timelineSessionEpoch);
|
|
128
|
+
const kfTargetSessionEpoch = kfContextMenu?.sessionEpoch;
|
|
129
|
+
const clipTargetSessionEpoch = clipContextMenu?.sessionEpoch;
|
|
130
|
+
const keyframeElement = kfContextMenu
|
|
131
|
+
? resolveTimelineContextElement({
|
|
132
|
+
capturedElement: kfContextMenu.element,
|
|
133
|
+
targetSessionEpoch: kfTargetSessionEpoch,
|
|
134
|
+
sessionEpoch,
|
|
135
|
+
selectedElementId,
|
|
136
|
+
elements,
|
|
137
|
+
})
|
|
138
|
+
: null;
|
|
139
|
+
const clipElement = clipContextMenu
|
|
140
|
+
? resolveTimelineContextElement({
|
|
141
|
+
capturedElement: clipContextMenu.element,
|
|
142
|
+
targetSessionEpoch: clipTargetSessionEpoch,
|
|
143
|
+
sessionEpoch,
|
|
144
|
+
selectedElementId,
|
|
145
|
+
elements,
|
|
146
|
+
})
|
|
147
|
+
: null;
|
|
148
|
+
const readCurrentElement = (element: TimelineElement, targetSessionEpoch: number | undefined) =>
|
|
149
|
+
readTimelineContextElement(element, targetSessionEpoch, elementsRef.current);
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (kfContextMenu && !keyframeElement) setKfContextMenu(null);
|
|
153
|
+
}, [keyframeElement, kfContextMenu, setKfContextMenu]);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (clipContextMenu && !clipElement) setClipContextMenu(null);
|
|
157
|
+
}, [clipContextMenu, clipElement, setClipContextMenu]);
|
|
158
|
+
|
|
82
159
|
return (
|
|
83
160
|
<>
|
|
84
161
|
{showShortcutHint && !showPopover && !rangeSelection && (
|
|
@@ -98,29 +175,45 @@ export function TimelineOverlays({
|
|
|
98
175
|
/>
|
|
99
176
|
)}
|
|
100
177
|
|
|
101
|
-
{kfContextMenu && (
|
|
178
|
+
{kfContextMenu && keyframeElement && (
|
|
102
179
|
<KeyframeDiamondContextMenu
|
|
103
|
-
state={kfContextMenu}
|
|
180
|
+
state={{ ...kfContextMenu, element: keyframeElement }}
|
|
104
181
|
onClose={() => setKfContextMenu(null)}
|
|
105
|
-
onDelete={(
|
|
106
|
-
|
|
182
|
+
onDelete={(...args) => {
|
|
183
|
+
if (!readCurrentElement(keyframeElement, kfTargetSessionEpoch)) return;
|
|
184
|
+
onDeleteKeyframe?.(...args);
|
|
185
|
+
}}
|
|
186
|
+
onDeleteAll={(_element, animationId) => {
|
|
187
|
+
const element = readCurrentElement(keyframeElement, kfTargetSessionEpoch);
|
|
188
|
+
if (element) onDeleteAllKeyframes?.(element, animationId);
|
|
189
|
+
}}
|
|
107
190
|
onMoveToPlayhead={
|
|
108
|
-
onMoveKeyframeToPlayhead
|
|
191
|
+
onMoveKeyframeToPlayhead
|
|
192
|
+
? (_element, ...args) => {
|
|
193
|
+
const element = readCurrentElement(keyframeElement, kfTargetSessionEpoch);
|
|
194
|
+
if (element) onMoveKeyframeToPlayhead(element, ...args);
|
|
195
|
+
}
|
|
196
|
+
: undefined
|
|
109
197
|
}
|
|
110
198
|
/>
|
|
111
199
|
)}
|
|
112
200
|
|
|
113
|
-
{clipContextMenu && (
|
|
201
|
+
{clipContextMenu && clipElement && (
|
|
114
202
|
<ClipContextMenu
|
|
115
203
|
x={clipContextMenu.x}
|
|
116
204
|
y={clipContextMenu.y}
|
|
117
|
-
element={
|
|
205
|
+
element={clipElement}
|
|
118
206
|
currentTime={currentTime}
|
|
119
207
|
onClose={() => setClipContextMenu(null)}
|
|
120
|
-
onSplit={(
|
|
121
|
-
|
|
208
|
+
onSplit={(_element, time) => {
|
|
209
|
+
const element = readCurrentElement(clipElement, clipTargetSessionEpoch);
|
|
210
|
+
if (element) onSplitElement?.(element, time);
|
|
211
|
+
}}
|
|
212
|
+
onDelete={() => {
|
|
213
|
+
const element = readCurrentElement(clipElement, clipTargetSessionEpoch);
|
|
214
|
+
if (!element) return;
|
|
122
215
|
pinZoomBeforeEdit();
|
|
123
|
-
onDeleteElement?.(
|
|
216
|
+
onDeleteElement?.(element);
|
|
124
217
|
}}
|
|
125
218
|
/>
|
|
126
219
|
)}
|
|
@@ -73,7 +73,7 @@ export interface TimelineEditCallbacks {
|
|
|
73
73
|
onRazorSplit?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
|
74
74
|
onRazorSplitAll?: (splitTime: number) => Promise<void> | void;
|
|
75
75
|
onDeleteKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
|
76
|
-
onDeleteAllKeyframes?: (element: TimelineElement) => void;
|
|
76
|
+
onDeleteAllKeyframes?: (element: TimelineElement, animationId?: string) => void;
|
|
77
77
|
onMoveKeyframeToPlayhead?: (element: TimelineElement, keyframe: TimelineKeyframeTarget) => void;
|
|
78
78
|
/** Drag-to-retime: `keyframe` identifies the dragged keyframe (its percentage
|
|
79
79
|
* is clip-relative), `toClipPercentage` is the neighbour-clamped drop. */
|
|
@@ -21,6 +21,7 @@ describe("timeline clip drag gesture lifecycle", () => {
|
|
|
21
21
|
track: 0,
|
|
22
22
|
};
|
|
23
23
|
const drag: DraggedClipState = {
|
|
24
|
+
pointerId: 0,
|
|
24
25
|
element,
|
|
25
26
|
originClientX: 0,
|
|
26
27
|
originClientY: 0,
|
|
@@ -49,23 +50,36 @@ describe("timeline clip drag gesture lifecycle", () => {
|
|
|
49
50
|
}));
|
|
50
51
|
const onMoveElement = vi.fn();
|
|
51
52
|
const stopAutoScroll = vi.fn();
|
|
53
|
+
const cancelGestureRef = { current: () => false };
|
|
52
54
|
const dispose = mountTimelineClipDragGestureLifecycle({
|
|
55
|
+
lifecycleRef: {
|
|
56
|
+
current: {
|
|
57
|
+
kind: "drag",
|
|
58
|
+
phase: "active",
|
|
59
|
+
pointerId: null,
|
|
60
|
+
sessionEpoch: 0,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
sessionEpochRef: { current: 0 },
|
|
64
|
+
cancelGestureRef,
|
|
65
|
+
scrollRef: { current: null },
|
|
53
66
|
draggedClipRef,
|
|
54
67
|
resizingClipRef,
|
|
55
68
|
blockedClipRef: { current: null },
|
|
56
69
|
groupResizeRef: { current: null },
|
|
57
70
|
suppressClickRef: { current: false },
|
|
71
|
+
gestureSelectedKeysRef: { current: new Set() },
|
|
58
72
|
elementsRef: { current: [element] },
|
|
59
73
|
trackOrderRef: { current: [0] },
|
|
60
|
-
setDraggedClip,
|
|
61
|
-
|
|
74
|
+
setDraggedClipState: setDraggedClip,
|
|
75
|
+
setResizingClipState: () => {},
|
|
62
76
|
setShowPopover: () => {},
|
|
63
77
|
setRangeSelectionRef: { current: null },
|
|
64
78
|
applyResizePointerRef: { current: () => {} },
|
|
65
79
|
syncClipDragAutoScrollRef: { current: () => {} },
|
|
66
80
|
stopClipDragAutoScrollRef: { current: stopAutoScroll },
|
|
67
81
|
updateDraggedClipPreviewRef: { current: updateDraggedClipPreview },
|
|
68
|
-
|
|
82
|
+
publishDraggedClip: setDraggedClip,
|
|
69
83
|
updateElement: vi.fn(),
|
|
70
84
|
onMoveElementRef: { current: onMoveElement },
|
|
71
85
|
onMoveElementsRef: { current: undefined },
|
|
@@ -93,5 +107,6 @@ describe("timeline clip drag gesture lifecycle", () => {
|
|
|
93
107
|
expect(stopAutoScroll).toHaveBeenCalledTimes(1);
|
|
94
108
|
|
|
95
109
|
dispose();
|
|
110
|
+
expect(cancelGestureRef.current()).toBe(false);
|
|
96
111
|
});
|
|
97
112
|
});
|