@hyperframes/studio 0.7.107 → 0.7.108
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-BLrfwq5o.js → hyperframes-player-QqavTWRK.js} +1 -1
- package/dist/assets/{index-BA9yhzfR.js → index-BQjXA1pz.js} +1 -1
- package/dist/assets/{index-BSBk5srs.js → index-BhHHMprP.js} +1 -1
- package/dist/assets/index-DNkh9mbV.css +1 -0
- package/dist/assets/index-GqONLcOl.js +428 -0
- package/dist/index.d.ts +85 -2
- package/dist/index.html +2 -2
- package/dist/index.js +17696 -12512
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanel.tsx +18 -1
- package/src/components/TimelineToolbar.test.tsx +46 -0
- package/src/components/TimelineToolbar.tsx +17 -0
- package/src/components/editor/PropertyPanel.test.tsx +5 -1
- package/src/components/editor/PropertyPanel.tsx +7 -23
- package/src/components/editor/PropertyPanelFlat.tsx +25 -0
- package/src/components/editor/audioFxSummary.test.ts +75 -0
- package/src/components/editor/audioFxSummary.ts +36 -0
- package/src/components/editor/audioFxTelemetry.test.ts +129 -0
- package/src/components/editor/audioFxTelemetry.ts +230 -0
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +1830 -0
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +299 -0
- package/src/components/editor/propertyPanelAudioFxGroupUtils.ts +21 -0
- package/src/components/editor/propertyPanelAutomation.test.ts +60 -0
- package/src/components/editor/propertyPanelAutomation.ts +97 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +37 -9
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFxAddMenu.tsx +209 -0
- package/src/components/editor/propertyPanelFxBandRuler.tsx +72 -0
- package/src/components/editor/propertyPanelFxCarveModule.tsx +382 -0
- package/src/components/editor/propertyPanelFxControls.test.tsx +115 -0
- package/src/components/editor/propertyPanelFxControls.tsx +356 -0
- package/src/components/editor/propertyPanelFxEqModule.tsx +196 -0
- package/src/components/editor/propertyPanelFxFamily.ts +83 -0
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +269 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +112 -0
- package/src/components/editor/propertyPanelFxNodeRow.tsx +339 -0
- package/src/components/editor/propertyPanelFxPresetMenu.tsx +159 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +215 -0
- package/src/components/editor/propertyPanelFxPresetStyle.test.ts +142 -0
- package/src/components/editor/propertyPanelFxPresetStyle.ts +216 -0
- package/src/components/editor/propertyPanelFxRackChain.tsx +202 -0
- package/src/components/editor/propertyPanelFxSection.test.tsx +1747 -0
- package/src/components/editor/propertyPanelFxSection.tsx +550 -0
- package/src/components/editor/propertyPanelFxSectionTypes.ts +91 -0
- package/src/components/editor/propertyPanelTypes.ts +5 -0
- package/src/components/editor/useFxAudition.ts +95 -0
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxChainObserved.ts +83 -0
- package/src/components/editor/useFxLevelling.ts +235 -0
- package/src/components/editor/useVolumeAutomation.test.tsx +122 -0
- package/src/components/editor/useVolumeAutomation.ts +54 -0
- package/src/contexts/DomEditContext.tsx +11 -0
- package/src/hooks/useAppHotkeys.test.ts +198 -0
- package/src/hooks/useAppHotkeys.ts +30 -3
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +429 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +371 -0
- package/src/hooks/useDomEditAttributeCommits.ts +90 -10
- package/src/hooks/useDomEditCommits.test.tsx +38 -0
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -0
- package/src/hooks/useDomEditTextCommits.ts +2 -0
- package/src/hooks/useElementLifecycleOps.ts +3 -0
- package/src/hooks/useLivePlayheadTime.ts +49 -0
- package/src/hooks/usePreviewPersistence.ts +7 -0
- package/src/hooks/useTimelineEditing.ts +3 -0
- package/src/player/components/AutomationSelectionMenu.tsx +76 -0
- package/src/player/components/AutomationValueInput.tsx +51 -0
- package/src/player/components/LayerDisclosureRow.tsx +5 -4
- package/src/player/components/Timeline.test.ts +84 -0
- package/src/player/components/TimelineAutomationLane.test.tsx +1720 -0
- package/src/player/components/TimelineAutomationLane.tsx +674 -0
- package/src/player/components/TimelineAutomationLaneSlot.test.tsx +193 -0
- package/src/player/components/TimelineLanes.test.tsx +24 -5
- package/src/player/components/TimelineLanes.tsx +142 -154
- package/src/player/components/TimelineTrackHeader.test.tsx +212 -1
- package/src/player/components/TimelineTrackHeader.tsx +166 -6
- package/src/player/components/TimelineTrackRow.tsx +8 -1
- package/src/player/components/automationClipboard.test.ts +94 -0
- package/src/player/components/automationClipboard.ts +136 -0
- package/src/player/components/automationGestureKeys.test.ts +49 -0
- package/src/player/components/automationGestureKeys.ts +49 -0
- package/src/player/components/automationLaneData.test.ts +270 -0
- package/src/player/components/automationLaneData.ts +258 -0
- package/src/player/components/automationLaneDragMath.ts +194 -0
- package/src/player/components/automationLaneGeometry.test.ts +239 -0
- package/src/player/components/automationLaneGeometry.ts +287 -0
- package/src/player/components/automationLaneHeight.ts +15 -0
- package/src/player/components/automationLanePointer.ts +20 -0
- package/src/player/components/automationLaneSelection.test.ts +183 -0
- package/src/player/components/automationLaneSelection.ts +135 -0
- package/src/player/components/automationShapes.test.ts +82 -0
- package/src/player/components/automationShapes.ts +70 -0
- package/src/player/components/automationSimplify.test.ts +48 -0
- package/src/player/components/automationSimplify.ts +51 -0
- package/src/player/components/timelineClipGestureHandlers.ts +223 -0
- package/src/player/components/timelineLaneProps.ts +19 -0
- package/src/player/components/timelineLayout.ts +11 -5
- package/src/player/components/useAutomationEdgeStretch.test.ts +27 -0
- package/src/player/components/useAutomationEdgeStretch.ts +269 -0
- package/src/player/components/useAutomationLaneGestures.ts +537 -0
- package/src/player/components/useAutomationLanes.test.tsx +112 -0
- package/src/player/components/useAutomationLanes.ts +170 -0
- package/src/player/components/useAutomationRangeDrag.ts +114 -0
- package/src/player/components/useTimelineTrackLayout.test.ts +107 -1
- package/src/player/components/useTimelineTrackLayout.ts +83 -7
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
- package/src/player/hooks/useTimelinePlayer.ts +13 -1
- package/src/player/lib/automationStoreSync.test.ts +83 -0
- package/src/player/lib/automationStoreSync.ts +56 -0
- package/src/player/lib/timelineDOM.test.ts +33 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/store/automationSelectionSlice.test.ts +30 -0
- package/src/player/store/automationSelectionSlice.ts +54 -0
- package/src/player/store/playerStore.test.ts +15 -0
- package/src/player/store/playerStore.ts +54 -65
- package/src/player/store/timelineElement.ts +78 -0
- package/src/styles/studio.css +89 -0
- package/src/telemetry/agentRuntime.test.ts +45 -0
- package/src/telemetry/agentRuntime.ts +62 -0
- package/src/telemetry/system.ts +8 -0
- package/src/utils/studioFileHistory.test.ts +49 -0
- package/src/utils/studioFileHistory.ts +17 -1
- package/src/utils/studioTelemetry.ts +16 -10
- package/dist/assets/index-dF-CmLZu.js +0 -428
- package/dist/assets/index-zQ4JFwwB.css +0 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stretching an automation selection by one of its edges.
|
|
3
|
+
*
|
|
4
|
+
* Its own module because edge-stretch is a fifth mutually-exclusive gesture on a
|
|
5
|
+
* lane that already sits near the studio's file ceiling, and because it has to
|
|
6
|
+
* follow the same three-part contract the other gestures do — a movement
|
|
7
|
+
* threshold before anything is written, a live preview the user can see, and a
|
|
8
|
+
* revert when the browser abandons the gesture. It was first written without
|
|
9
|
+
* them, and every one of its bugs came from that: a bare click near an edge
|
|
10
|
+
* pushed an undo entry that changed nothing, the highlight stayed frozen at the
|
|
11
|
+
* pre-drag bounds so the handle was invisible while it moved, and a
|
|
12
|
+
* `pointercancel` persisted whatever partial retime it had reached.
|
|
13
|
+
*
|
|
14
|
+
* `retimeRange` is a RELATIVE transform — it scales a lane's own current point
|
|
15
|
+
* positions by newSpan/oldSpan — so it must always run against the snapshot
|
|
16
|
+
* taken when the drag armed, never the live draft, or the scale factor compounds
|
|
17
|
+
* on every pointermove.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useCallback, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
|
21
|
+
import type {
|
|
22
|
+
AutomationRange,
|
|
23
|
+
HfAutomationLane,
|
|
24
|
+
HfAutomationPoint,
|
|
25
|
+
} from "@hyperframes/core/audio-automation";
|
|
26
|
+
import { capturePointer } from "./automationLanePointer";
|
|
27
|
+
import { retimeRange } from "./automationLaneSelection";
|
|
28
|
+
|
|
29
|
+
/** Hit radius for grabbing a selection's edge, in screen px — independent of a
|
|
30
|
+
* point's own grab radius so the two zones can be reasoned about on their own. */
|
|
31
|
+
const EDGE_GRAB_PX = 8;
|
|
32
|
+
|
|
33
|
+
/** Screen px a press has to travel before it counts as a stretch rather than a
|
|
34
|
+
* click. The sibling range-drag uses the same 3px, and for the same reason:
|
|
35
|
+
* below it, a press is the "clear the selection" escape, and writing anything
|
|
36
|
+
* would put a no-op entry in undo. */
|
|
37
|
+
const EDGE_DRAG_PX = 3;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Narrowest selection a stretch can leave behind, in clip seconds. Its own
|
|
41
|
+
* constant rather than a borrowed `POINT_MERGE_SEC`: that one is about when two
|
|
42
|
+
* breakpoints are the same breakpoint, which is a different question from how
|
|
43
|
+
* thin a time selection may get and still be grabbable.
|
|
44
|
+
*/
|
|
45
|
+
const MIN_SELECTION_SEC = 0.02;
|
|
46
|
+
|
|
47
|
+
export interface UseAutomationEdgeStretchInput {
|
|
48
|
+
getBox(): DOMRect | null;
|
|
49
|
+
lane: HfAutomationLane;
|
|
50
|
+
range: AutomationRange;
|
|
51
|
+
/** Pointer position as a clip-local time and a parameter value. */
|
|
52
|
+
pointAt(clientX: number, clientY: number): { t: number; v: number };
|
|
53
|
+
xOf(t: number): number;
|
|
54
|
+
commitPoints(points: HfAutomationLane["points"], persist: boolean): void;
|
|
55
|
+
/** Clamp bound for the dragged edge. */
|
|
56
|
+
duration: number;
|
|
57
|
+
readOnly?: boolean | undefined;
|
|
58
|
+
/** Active selection on this lane, so its edges have something to grab. */
|
|
59
|
+
rangeSelection?: { t0: number; t1: number; v0: number; v1: number } | null | undefined;
|
|
60
|
+
/** Whether the pointer is over a point the current selection contains. Such a
|
|
61
|
+
* press is a group drag, not a stretch — see `arm`. */
|
|
62
|
+
pointInSelectionAt?: ((clientX: number, clientY: number) => boolean) | undefined;
|
|
63
|
+
onRangeSelect?: ((t0: number, t1: number, v0: number, v1: number) => void) | undefined;
|
|
64
|
+
onRangeClear?: (() => void) | undefined;
|
|
65
|
+
/** Value readout owned by the lane's gesture hook. */
|
|
66
|
+
onHint(text: string | null): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface UseAutomationEdgeStretchResult {
|
|
70
|
+
/** Edge being stretched, for the cursor. Null when no stretch is live. */
|
|
71
|
+
edge: "t0" | "t1" | null;
|
|
72
|
+
/** Pointer sits over a handle with no gesture live — the col-resize hint. */
|
|
73
|
+
hover: boolean;
|
|
74
|
+
/** Take the press as a stretch, or decline it so another gesture can have it. */
|
|
75
|
+
arm(e: ReactPointerEvent<SVGSVGElement>): boolean;
|
|
76
|
+
move(e: ReactPointerEvent<SVGSVGElement>): void;
|
|
77
|
+
/** Pointer released: persist the stretch, or clear the selection when the
|
|
78
|
+
* press never travelled far enough to be one. */
|
|
79
|
+
finish(): void;
|
|
80
|
+
/** The browser abandoned the gesture: put the envelope and the selection back
|
|
81
|
+
* the way they were, with nothing persisted. */
|
|
82
|
+
cancel(): void;
|
|
83
|
+
updateHover(e: ReactPointerEvent<SVGSVGElement>): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The dragged edge, clamped inside the clip AND clear of its partner. The
|
|
87
|
+
* 0/duration bound is applied LAST so a selection thinner than
|
|
88
|
+
* `MIN_SELECTION_SEC` can never push its own t0 negative — core's `cleanPoint`
|
|
89
|
+
* collapses negative times onto a duplicate t=0 on the next serialize.
|
|
90
|
+
* Exported for that ordering's own test: reaching it through the pointer needs
|
|
91
|
+
* a selection so thin that no drag can clear the movement threshold. */
|
|
92
|
+
export function clampEdge(
|
|
93
|
+
edge: "t0" | "t1",
|
|
94
|
+
raw: number,
|
|
95
|
+
origin: { t0: number; t1: number; v0: number; v1: number },
|
|
96
|
+
duration: number,
|
|
97
|
+
): number {
|
|
98
|
+
if (edge === "t0") {
|
|
99
|
+
return Math.max(0, Math.min(raw, origin.t1 - MIN_SELECTION_SEC));
|
|
100
|
+
}
|
|
101
|
+
return Math.min(duration, Math.max(raw, origin.t0 + MIN_SELECTION_SEC));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function useAutomationEdgeStretch({
|
|
105
|
+
getBox,
|
|
106
|
+
lane,
|
|
107
|
+
range,
|
|
108
|
+
pointAt,
|
|
109
|
+
xOf,
|
|
110
|
+
commitPoints,
|
|
111
|
+
duration,
|
|
112
|
+
readOnly,
|
|
113
|
+
rangeSelection,
|
|
114
|
+
pointInSelectionAt,
|
|
115
|
+
onRangeSelect,
|
|
116
|
+
onRangeClear,
|
|
117
|
+
onHint,
|
|
118
|
+
}: UseAutomationEdgeStretchInput): UseAutomationEdgeStretchResult {
|
|
119
|
+
/** The live stretch: which edge, the selection it started from (fixed, as the
|
|
120
|
+
* retime's untouched anchor), the edge's own live position, and the lane's
|
|
121
|
+
* points as they stood at arm time. */
|
|
122
|
+
const [drag, setDrag] = useState<{
|
|
123
|
+
edge: "t0" | "t1";
|
|
124
|
+
origin: { t0: number; t1: number; v0: number; v1: number };
|
|
125
|
+
current: number;
|
|
126
|
+
points: HfAutomationPoint[];
|
|
127
|
+
} | null>(null);
|
|
128
|
+
/** Whether the drag has travelled far enough to write anything at all. */
|
|
129
|
+
const crossed = useRef(false);
|
|
130
|
+
const [hover, setHover] = useState(false);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Which edge of the active selection is within grab range of the pointer's
|
|
134
|
+
* screen x, over the full lane height — the handle spans the rect.
|
|
135
|
+
*
|
|
136
|
+
* A selection narrower than two halos has both edges under one press. The
|
|
137
|
+
* midpoint split says which one wins — the same rule a nearest-distance
|
|
138
|
+
* comparison expresses, written so that is legible rather than inferred, since
|
|
139
|
+
* on a narrow selection it is the ONLY thing deciding the gesture. Pressing a
|
|
140
|
+
* handle without moving clears the selection (see `finish`), which is what
|
|
141
|
+
* gets a user out of a halo too small to aim inside.
|
|
142
|
+
*/
|
|
143
|
+
const edgeAt = useCallback(
|
|
144
|
+
(clientX: number): "t0" | "t1" | null => {
|
|
145
|
+
if (!rangeSelection) return null;
|
|
146
|
+
const box = getBox();
|
|
147
|
+
if (!box) return null;
|
|
148
|
+
const px = clientX - box.left;
|
|
149
|
+
const x0 = xOf(rangeSelection.t0);
|
|
150
|
+
const x1 = xOf(rangeSelection.t1);
|
|
151
|
+
const near0 = Math.abs(x0 - px) <= EDGE_GRAB_PX;
|
|
152
|
+
const near1 = Math.abs(x1 - px) <= EDGE_GRAB_PX;
|
|
153
|
+
if (near0 && near1) return px <= (x0 + x1) / 2 ? "t0" : "t1";
|
|
154
|
+
if (near0) return "t0";
|
|
155
|
+
return near1 ? "t1" : null;
|
|
156
|
+
},
|
|
157
|
+
[rangeSelection, getBox, xOf],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const arm = useCallback(
|
|
161
|
+
(e: ReactPointerEvent<SVGSVGElement>): boolean => {
|
|
162
|
+
if (readOnly || !rangeSelection) return false;
|
|
163
|
+
const edge = edgeAt(e.clientX);
|
|
164
|
+
if (!edge) return false;
|
|
165
|
+
// Selected CONTENT outranks the edge it sits on.
|
|
166
|
+
//
|
|
167
|
+
// #3207 had the opposite rule, and it was right for a time-only selection:
|
|
168
|
+
// every range operation leaves a breakpoint exactly on the edge it created,
|
|
169
|
+
// so a point-first rule made the second stretch of an edge resolve to a
|
|
170
|
+
// point-drag. A box selection changes the question — a point on the edge is
|
|
171
|
+
// now visibly INSIDE the selection, with the value axis saying so, and
|
|
172
|
+
// dragging selected content has to move it or the box means nothing.
|
|
173
|
+
//
|
|
174
|
+
// So the edge still stretches everywhere it is not also selected content,
|
|
175
|
+
// which is most of its length.
|
|
176
|
+
if (pointInSelectionAt?.(e.clientX, e.clientY)) return false;
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
capturePointer(e);
|
|
179
|
+
setHover(false);
|
|
180
|
+
crossed.current = false;
|
|
181
|
+
setDrag({
|
|
182
|
+
edge,
|
|
183
|
+
origin: rangeSelection,
|
|
184
|
+
current: edge === "t0" ? rangeSelection.t0 : rangeSelection.t1,
|
|
185
|
+
points: lane.points,
|
|
186
|
+
});
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
[readOnly, rangeSelection, edgeAt, lane, pointInSelectionAt],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
/** Preview the stretch: the partner edge stays put as the retime's anchor, and
|
|
193
|
+
* the selection itself follows the pointer so the handle being dragged is
|
|
194
|
+
* visible — the same live `onRangeSelect` the marquee drag fires. */
|
|
195
|
+
const move = useCallback(
|
|
196
|
+
(e: ReactPointerEvent<SVGSVGElement>): void => {
|
|
197
|
+
if (drag === null) return;
|
|
198
|
+
const { edge, origin, points } = drag;
|
|
199
|
+
const current = clampEdge(edge, pointAt(e.clientX, e.clientY).t, origin, duration);
|
|
200
|
+
setDrag({ edge, origin, current, points });
|
|
201
|
+
if (!crossed.current) {
|
|
202
|
+
const from = edge === "t0" ? origin.t0 : origin.t1;
|
|
203
|
+
if (Math.abs(xOf(current) - xOf(from)) <= EDGE_DRAG_PX) return;
|
|
204
|
+
crossed.current = true;
|
|
205
|
+
}
|
|
206
|
+
const newT0 = edge === "t0" ? current : origin.t0;
|
|
207
|
+
const newT1 = edge === "t1" ? current : origin.t1;
|
|
208
|
+
onHint(`${newT0.toFixed(2)}s → ${newT1.toFixed(2)}s`);
|
|
209
|
+
onRangeSelect?.(newT0, newT1, origin.v0, origin.v1);
|
|
210
|
+
commitPoints(
|
|
211
|
+
retimeRange({
|
|
212
|
+
lane: { target: lane.target, points },
|
|
213
|
+
range,
|
|
214
|
+
t0: origin.t0,
|
|
215
|
+
t1: origin.t1,
|
|
216
|
+
newT0,
|
|
217
|
+
newT1,
|
|
218
|
+
}),
|
|
219
|
+
false,
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
[drag, pointAt, duration, xOf, onHint, onRangeSelect, commitPoints, lane.target, range],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const finish = useCallback((): void => {
|
|
226
|
+
if (drag === null) return;
|
|
227
|
+
const { edge, origin, current } = drag;
|
|
228
|
+
setDrag(null);
|
|
229
|
+
onHint(null);
|
|
230
|
+
// A press that never travelled is the "clear the selection" click, exactly
|
|
231
|
+
// as it is anywhere else on the background. Persisting here instead pushed
|
|
232
|
+
// an undo entry that changed nothing AND made that escape unreachable
|
|
233
|
+
// within a halo of either edge.
|
|
234
|
+
if (!crossed.current) {
|
|
235
|
+
onRangeClear?.();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
crossed.current = false;
|
|
239
|
+
commitPoints(lane.points, true);
|
|
240
|
+
onRangeSelect?.(
|
|
241
|
+
edge === "t0" ? current : origin.t0,
|
|
242
|
+
edge === "t1" ? current : origin.t1,
|
|
243
|
+
origin.v0,
|
|
244
|
+
origin.v1,
|
|
245
|
+
);
|
|
246
|
+
}, [drag, onHint, onRangeClear, commitPoints, lane, onRangeSelect]);
|
|
247
|
+
|
|
248
|
+
const cancel = useCallback((): void => {
|
|
249
|
+
if (drag === null) return;
|
|
250
|
+
const { origin, points } = drag;
|
|
251
|
+
setDrag(null);
|
|
252
|
+
onHint(null);
|
|
253
|
+
if (!crossed.current) return;
|
|
254
|
+
crossed.current = false;
|
|
255
|
+
// A live write is a preview, so putting the snapshot back through the same
|
|
256
|
+
// channel is the whole revert — there is nothing persisted to undo.
|
|
257
|
+
commitPoints(points, false);
|
|
258
|
+
onRangeSelect?.(origin.t0, origin.t1, origin.v0, origin.v1);
|
|
259
|
+
}, [drag, onHint, commitPoints, onRangeSelect]);
|
|
260
|
+
|
|
261
|
+
const updateHover = useCallback(
|
|
262
|
+
(e: ReactPointerEvent<SVGSVGElement>): void => {
|
|
263
|
+
if (!readOnly) setHover(edgeAt(e.clientX) !== null);
|
|
264
|
+
},
|
|
265
|
+
[readOnly, edgeAt],
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
return { edge: drag?.edge ?? null, hover, arm, move, finish, cancel, updateHover };
|
|
269
|
+
}
|