@weasel-js/routing 1.5.0
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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/chunk-Y5FHAA4U.js +1504 -0
- package/dist/chunk-Y5FHAA4U.js.map +1 -0
- package/dist/index-BOlkWrA-.d.ts +2446 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +405 -0
- package/dist/react.js +1304 -0
- package/dist/react.js.map +1 -0
- package/package.json +59 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
import { createDispatcher, buildDepsFromRequires, validateActionId, validateActionDefaultBinding, scopeBindings, liveScope, offhandKeyFor, makeToolOffhandAction, buildToolOffhandBindings, isDev, reportRouteConflicts, dlog, isEditableTarget, clientToCanvasRect, openPointerSession, pointerGestureId, itemsFromDataTransfer, itemsFromClipboardData } from './chunk-Y5FHAA4U.js';
|
|
2
|
+
import { createContext, useState, useCallback, useMemo, useContext, useRef, useEffect } from 'react';
|
|
3
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
import { resolveCursorTier } from '@weasel-js/cursor';
|
|
5
|
+
|
|
6
|
+
var ActiveToolContext = createContext(null);
|
|
7
|
+
function ActiveToolContextProvider({
|
|
8
|
+
children,
|
|
9
|
+
initialActive = "select"
|
|
10
|
+
}) {
|
|
11
|
+
const [active, setActiveState] = useState(initialActive);
|
|
12
|
+
const [hotkeyStack, setHotkeyStack] = useState([]);
|
|
13
|
+
const setActive = useCallback((id) => {
|
|
14
|
+
setActiveState(id);
|
|
15
|
+
}, []);
|
|
16
|
+
const pushHotkey = useCallback((id) => {
|
|
17
|
+
setHotkeyStack((s) => [...s, id]);
|
|
18
|
+
}, []);
|
|
19
|
+
const popHotkey = useCallback((id) => {
|
|
20
|
+
setHotkeyStack((s) => {
|
|
21
|
+
if (s.length === 0) return s;
|
|
22
|
+
if (id === void 0) return s.slice(0, -1);
|
|
23
|
+
const i = s.lastIndexOf(id);
|
|
24
|
+
return i === -1 ? s : [...s.slice(0, i), ...s.slice(i + 1)];
|
|
25
|
+
});
|
|
26
|
+
}, []);
|
|
27
|
+
const value = useMemo(
|
|
28
|
+
() => ({ active, hotkeyStack, setActive, pushHotkey, popHotkey }),
|
|
29
|
+
[active, hotkeyStack, setActive, pushHotkey, popHotkey]
|
|
30
|
+
);
|
|
31
|
+
return /* @__PURE__ */ jsx(ActiveToolContext.Provider, { value, children });
|
|
32
|
+
}
|
|
33
|
+
function useActiveToolContext() {
|
|
34
|
+
const value = useContext(ActiveToolContext);
|
|
35
|
+
if (value === null) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"useActiveToolContext: no ActiveToolContextProvider in scope. Wrap your tree with <ActiveToolContextProvider> (typically inside <SceneCanvas>)."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function useOptionalActiveToolContext() {
|
|
43
|
+
return useContext(ActiveToolContext);
|
|
44
|
+
}
|
|
45
|
+
function ActiveToolContextProviderIfRoot({
|
|
46
|
+
children
|
|
47
|
+
}) {
|
|
48
|
+
const parent = useContext(ActiveToolContext);
|
|
49
|
+
if (parent !== null) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
50
|
+
return /* @__PURE__ */ jsx(ActiveToolContextProvider, { children });
|
|
51
|
+
}
|
|
52
|
+
var DepRegistryContext = createContext(null);
|
|
53
|
+
function DepRegistryProvider({ children }) {
|
|
54
|
+
const sourcesRef = useRef(/* @__PURE__ */ new Map());
|
|
55
|
+
const registry = useMemo(() => ({
|
|
56
|
+
register: (name, source) => {
|
|
57
|
+
const key = name;
|
|
58
|
+
const entry = source;
|
|
59
|
+
const stack = sourcesRef.current.get(key);
|
|
60
|
+
if (stack) stack.push(entry);
|
|
61
|
+
else sourcesRef.current.set(key, [entry]);
|
|
62
|
+
let released = false;
|
|
63
|
+
return () => {
|
|
64
|
+
if (released) return;
|
|
65
|
+
released = true;
|
|
66
|
+
const cur = sourcesRef.current.get(key);
|
|
67
|
+
if (!cur) return;
|
|
68
|
+
const i = cur.lastIndexOf(entry);
|
|
69
|
+
if (i === -1) return;
|
|
70
|
+
cur.splice(i, 1);
|
|
71
|
+
if (cur.length === 0) sourcesRef.current.delete(key);
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
get: (name) => sourcesRef.current.get(name)?.at(-1)?.()
|
|
75
|
+
}), []);
|
|
76
|
+
return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry, children });
|
|
77
|
+
}
|
|
78
|
+
function useDepRegistry() {
|
|
79
|
+
const r = useContext(DepRegistryContext);
|
|
80
|
+
if (r === null) {
|
|
81
|
+
throw new Error("useDepRegistry: no DepRegistryProvider in scope. Wrap your tree with <DepRegistryProvider> (typically inside <SceneCanvas>).");
|
|
82
|
+
}
|
|
83
|
+
return r;
|
|
84
|
+
}
|
|
85
|
+
function useOptionalDepRegistry() {
|
|
86
|
+
return useContext(DepRegistryContext);
|
|
87
|
+
}
|
|
88
|
+
function useDepSource(name, source) {
|
|
89
|
+
const registry = useDepRegistry();
|
|
90
|
+
const sourceRef = useRef(source);
|
|
91
|
+
sourceRef.current = source;
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
return registry.register(name, () => sourceRef.current());
|
|
94
|
+
}, [name, registry]);
|
|
95
|
+
}
|
|
96
|
+
var DROPOVER_CLASS = "weasel-dropover";
|
|
97
|
+
function stylusOf(e) {
|
|
98
|
+
return {
|
|
99
|
+
...typeof e.pressure === "number" ? { pressure: e.pressure } : {},
|
|
100
|
+
...typeof e.tiltX === "number" ? { tiltX: e.tiltX } : {},
|
|
101
|
+
...typeof e.tiltY === "number" ? { tiltY: e.tiltY } : {}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
var IS_MAC = typeof navigator !== "undefined" && /mac/i.test(
|
|
105
|
+
// `||`, not `??`: jsdom (and some embedders) report an empty-string
|
|
106
|
+
// platform, which is not nullish and would swallow the UA fallback.
|
|
107
|
+
navigator.platform || navigator.userAgent
|
|
108
|
+
);
|
|
109
|
+
function withViewDeps(base, overlay) {
|
|
110
|
+
return {
|
|
111
|
+
register: (name, source) => base().register(name, source),
|
|
112
|
+
get: (name) => {
|
|
113
|
+
const own = overlay()?.[name];
|
|
114
|
+
if (own !== void 0) return own;
|
|
115
|
+
return base().get(name);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function computeMultiTouchGeometry(positions) {
|
|
120
|
+
const pts = [...positions];
|
|
121
|
+
if (pts.length < 2) {
|
|
122
|
+
return { centroid: { x: 0, y: 0 }, spread: 0 };
|
|
123
|
+
}
|
|
124
|
+
let sumX = 0;
|
|
125
|
+
let sumY = 0;
|
|
126
|
+
for (const p of pts) {
|
|
127
|
+
sumX += p.x;
|
|
128
|
+
sumY += p.y;
|
|
129
|
+
}
|
|
130
|
+
const centroid = { x: sumX / pts.length, y: sumY / pts.length };
|
|
131
|
+
const dx = pts[1].x - pts[0].x;
|
|
132
|
+
const dy = pts[1].y - pts[0].y;
|
|
133
|
+
const spread = Math.sqrt(dx * dx + dy * dy);
|
|
134
|
+
return { centroid, spread };
|
|
135
|
+
}
|
|
136
|
+
function useGestureDispatcher(opts) {
|
|
137
|
+
const { canvasRef, actions, toolsById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld, requestRedraw, paintedCursor, getRuleCtx, onDoubleClick, views } = opts;
|
|
138
|
+
const onDoubleClickRef = useRef(onDoubleClick);
|
|
139
|
+
onDoubleClickRef.current = onDoubleClick;
|
|
140
|
+
const activeTool = useActiveToolContext();
|
|
141
|
+
const depRegistry = useDepRegistry();
|
|
142
|
+
const dispatcherRef = useRef(null);
|
|
143
|
+
if (!dispatcherRef.current) {
|
|
144
|
+
dispatcherRef.current = dispatcherOpt ?? createDispatcher();
|
|
145
|
+
}
|
|
146
|
+
const ctxRef = useRef({
|
|
147
|
+
actions,
|
|
148
|
+
depRegistry,
|
|
149
|
+
activeToolId: activeTool.active,
|
|
150
|
+
hotkeyStack: activeTool.hotkeyStack,
|
|
151
|
+
toolsById,
|
|
152
|
+
isMac: IS_MAC
|
|
153
|
+
});
|
|
154
|
+
ctxRef.current = {
|
|
155
|
+
actions,
|
|
156
|
+
depRegistry,
|
|
157
|
+
activeToolId: activeTool.active,
|
|
158
|
+
hotkeyStack: activeTool.hotkeyStack,
|
|
159
|
+
toolsById,
|
|
160
|
+
isMac: IS_MAC
|
|
161
|
+
};
|
|
162
|
+
const rootTargetRef = useRef({
|
|
163
|
+
id: null,
|
|
164
|
+
dispatcher: dispatcherRef.current,
|
|
165
|
+
affordanceAt,
|
|
166
|
+
classifyTarget,
|
|
167
|
+
clientToWorld,
|
|
168
|
+
getRuleCtx
|
|
169
|
+
});
|
|
170
|
+
rootTargetRef.current = {
|
|
171
|
+
id: null,
|
|
172
|
+
dispatcher: dispatcherRef.current,
|
|
173
|
+
affordanceAt,
|
|
174
|
+
classifyTarget,
|
|
175
|
+
clientToWorld,
|
|
176
|
+
getRuleCtx
|
|
177
|
+
};
|
|
178
|
+
const viewsRef = useRef(views);
|
|
179
|
+
viewsRef.current = views;
|
|
180
|
+
const allDispatchers = () => [
|
|
181
|
+
rootTargetRef.current.dispatcher,
|
|
182
|
+
...(viewsRef.current?.targets() ?? []).map((t) => t.dispatcher)
|
|
183
|
+
];
|
|
184
|
+
const requestRedrawRef = useRef(requestRedraw);
|
|
185
|
+
requestRedrawRef.current = requestRedraw;
|
|
186
|
+
const paintedCursorRef = useRef(paintedCursor);
|
|
187
|
+
paintedCursorRef.current = paintedCursor;
|
|
188
|
+
const lastClickRef = useRef(null);
|
|
189
|
+
const prevActiveRef = useRef(activeTool.active);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (prevActiveRef.current !== activeTool.active) {
|
|
192
|
+
const prevTool = toolsById.get(prevActiveRef.current);
|
|
193
|
+
if (prevTool?.onDeactivate) {
|
|
194
|
+
const scratch = prevTool.initScratch?.();
|
|
195
|
+
try {
|
|
196
|
+
prevTool.onDeactivate({ scratch });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error(
|
|
199
|
+
`weasel: tool "${prevActiveRef.current}".onDeactivate threw`,
|
|
200
|
+
err
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const d of allDispatchers()) d.cancelAll("cancel");
|
|
205
|
+
}
|
|
206
|
+
prevActiveRef.current = activeTool.active;
|
|
207
|
+
});
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!enabled) return;
|
|
210
|
+
let disposed = false;
|
|
211
|
+
let routedId = null;
|
|
212
|
+
const target = () => {
|
|
213
|
+
if (routedId === null) return rootTargetRef.current;
|
|
214
|
+
return viewsRef.current?.targets().find((t) => t.id === routedId) ?? rootTargetRef.current;
|
|
215
|
+
};
|
|
216
|
+
const routeAt = (pointerId, clientX, clientY) => {
|
|
217
|
+
routedId = viewsRef.current?.resolver.at(pointerId, clientX, clientY).id ?? null;
|
|
218
|
+
};
|
|
219
|
+
const routeDown = (pointerId, clientX, clientY) => {
|
|
220
|
+
routedId = viewsRef.current?.resolver.begin(pointerId, clientX, clientY).id ?? null;
|
|
221
|
+
};
|
|
222
|
+
const routeRelease = (pointerId) => {
|
|
223
|
+
viewsRef.current?.resolver.end(pointerId);
|
|
224
|
+
};
|
|
225
|
+
const dispatcherNow = () => target().dispatcher;
|
|
226
|
+
const routedDeps = withViewDeps(() => ctxRef.current.depRegistry, () => target().deps?.());
|
|
227
|
+
const routedRuleCtx = () => target().getRuleCtx?.();
|
|
228
|
+
const ctxNow = () => ({
|
|
229
|
+
...ctxRef.current,
|
|
230
|
+
depRegistry: routedDeps,
|
|
231
|
+
getRuleCtx: routedRuleCtx
|
|
232
|
+
});
|
|
233
|
+
const canvas = canvasRef.current;
|
|
234
|
+
const toWorld = (cx, cy) => {
|
|
235
|
+
const fn = target().clientToWorld;
|
|
236
|
+
return fn ? fn(cx, cy) : { x: cx, y: cy };
|
|
237
|
+
};
|
|
238
|
+
const dispatch = (ev, on) => {
|
|
239
|
+
const d = on ?? dispatcherNow();
|
|
240
|
+
const before = d.inFlight().size;
|
|
241
|
+
const result = d.handleInput(ev, ctxNow());
|
|
242
|
+
const after = d.inFlight().size;
|
|
243
|
+
if (before > 0 || after > 0) requestRedrawRef.current?.();
|
|
244
|
+
return result;
|
|
245
|
+
};
|
|
246
|
+
const heldKeys = /* @__PURE__ */ new Set();
|
|
247
|
+
const held = /* @__PURE__ */ new Map();
|
|
248
|
+
const toCanvasLocal = (p) => {
|
|
249
|
+
const rect = canvas?.getBoundingClientRect();
|
|
250
|
+
if (!rect) return p;
|
|
251
|
+
const [x, y] = clientToCanvasRect(rect, p.x, p.y);
|
|
252
|
+
return { x, y };
|
|
253
|
+
};
|
|
254
|
+
const heldGeometry = () => {
|
|
255
|
+
const { centroid, spread } = computeMultiTouchGeometry(
|
|
256
|
+
[...held.values()].map((p) => p.pos)
|
|
257
|
+
);
|
|
258
|
+
return { centroid: toCanvasLocal(centroid), spread };
|
|
259
|
+
};
|
|
260
|
+
let multiTouchStart = null;
|
|
261
|
+
const TAP_THRESHOLD_PX = 8;
|
|
262
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
263
|
+
const LONG_PRESS_MS = 500;
|
|
264
|
+
const longPressTimers = /* @__PURE__ */ new Map();
|
|
265
|
+
const cancelLongPress = (pointerId) => {
|
|
266
|
+
const t = longPressTimers.get(pointerId);
|
|
267
|
+
if (t !== void 0) {
|
|
268
|
+
clearTimeout(t);
|
|
269
|
+
longPressTimers.delete(pointerId);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const cancelAllLongPress = () => {
|
|
273
|
+
for (const t of longPressTimers.values()) clearTimeout(t);
|
|
274
|
+
longPressTimers.clear();
|
|
275
|
+
};
|
|
276
|
+
const fireLongPress = (pointerId) => {
|
|
277
|
+
const down = held.get(pointerId)?.down;
|
|
278
|
+
if (!down) return;
|
|
279
|
+
const shared = {
|
|
280
|
+
target: down.target,
|
|
281
|
+
altKey: down.altKey,
|
|
282
|
+
ctrlKey: down.ctrlKey,
|
|
283
|
+
metaKey: down.metaKey,
|
|
284
|
+
shiftKey: down.shiftKey,
|
|
285
|
+
...down.affordance !== void 0 ? { affordance: down.affordance } : {},
|
|
286
|
+
...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
|
|
287
|
+
...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
|
|
288
|
+
};
|
|
289
|
+
const result = dispatch({
|
|
290
|
+
kind: "longpress",
|
|
291
|
+
x: down.worldX,
|
|
292
|
+
y: down.worldY,
|
|
293
|
+
clientX: down.clientX,
|
|
294
|
+
clientY: down.clientY,
|
|
295
|
+
...shared
|
|
296
|
+
});
|
|
297
|
+
if (result === "unhandled") {
|
|
298
|
+
dispatch({
|
|
299
|
+
kind: "contextmenu",
|
|
300
|
+
worldX: down.worldX,
|
|
301
|
+
worldY: down.worldY,
|
|
302
|
+
...shared
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
const DOUBLE_CLICK_MAX_MS = 600;
|
|
307
|
+
const DOUBLE_CLICK_MAX_PX = 8;
|
|
308
|
+
const onKeyDown = (e) => {
|
|
309
|
+
if (isEditableTarget(e.target)) return;
|
|
310
|
+
if (e.repeat && heldKeys.has(e.key)) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const keyEv = {
|
|
315
|
+
kind: "key",
|
|
316
|
+
key: e.key,
|
|
317
|
+
altKey: e.altKey,
|
|
318
|
+
ctrlKey: e.ctrlKey,
|
|
319
|
+
metaKey: e.metaKey,
|
|
320
|
+
shiftKey: e.shiftKey,
|
|
321
|
+
repeat: e.repeat
|
|
322
|
+
};
|
|
323
|
+
const keyResult = dispatch(keyEv);
|
|
324
|
+
const heldEv = {
|
|
325
|
+
kind: "key-held",
|
|
326
|
+
key: e.key,
|
|
327
|
+
phase: "down",
|
|
328
|
+
altKey: e.altKey,
|
|
329
|
+
ctrlKey: e.ctrlKey,
|
|
330
|
+
metaKey: e.metaKey,
|
|
331
|
+
shiftKey: e.shiftKey
|
|
332
|
+
};
|
|
333
|
+
const heldResult = dispatch(heldEv);
|
|
334
|
+
if (heldResult === "handled") {
|
|
335
|
+
heldKeys.add(e.key);
|
|
336
|
+
}
|
|
337
|
+
if (keyResult === "handled" || heldResult === "handled") {
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const onKeyUp = (e) => {
|
|
342
|
+
if (!heldKeys.has(e.key)) return;
|
|
343
|
+
const ev = {
|
|
344
|
+
kind: "key-held",
|
|
345
|
+
key: e.key,
|
|
346
|
+
phase: "up",
|
|
347
|
+
altKey: e.altKey,
|
|
348
|
+
ctrlKey: e.ctrlKey,
|
|
349
|
+
metaKey: e.metaKey,
|
|
350
|
+
shiftKey: e.shiftKey
|
|
351
|
+
};
|
|
352
|
+
dispatch(ev);
|
|
353
|
+
heldKeys.delete(e.key);
|
|
354
|
+
};
|
|
355
|
+
const onWindowBlur = () => {
|
|
356
|
+
for (const p of [...held.values()]) p.session.cancel();
|
|
357
|
+
for (const key of heldKeys) {
|
|
358
|
+
const ev = {
|
|
359
|
+
kind: "key-held",
|
|
360
|
+
key,
|
|
361
|
+
phase: "up",
|
|
362
|
+
altKey: false,
|
|
363
|
+
ctrlKey: false,
|
|
364
|
+
metaKey: false,
|
|
365
|
+
shiftKey: false
|
|
366
|
+
};
|
|
367
|
+
dispatch(ev);
|
|
368
|
+
}
|
|
369
|
+
heldKeys.clear();
|
|
370
|
+
};
|
|
371
|
+
const onWheel = (e) => {
|
|
372
|
+
routeAt(null, e.clientX, e.clientY);
|
|
373
|
+
const rect = canvas?.getBoundingClientRect();
|
|
374
|
+
const [localX, localY] = rect ? clientToCanvasRect(rect, e.clientX, e.clientY) : [e.clientX, e.clientY];
|
|
375
|
+
const hitPoint = toWorld(e.clientX, e.clientY);
|
|
376
|
+
const affordance = target().affordanceAt?.(hitPoint) ?? void 0;
|
|
377
|
+
const body = target().classifyTarget?.(hitPoint);
|
|
378
|
+
const ev = {
|
|
379
|
+
kind: "wheel",
|
|
380
|
+
altKey: e.altKey,
|
|
381
|
+
ctrlKey: e.ctrlKey,
|
|
382
|
+
metaKey: e.metaKey,
|
|
383
|
+
shiftKey: e.shiftKey,
|
|
384
|
+
deltaX: e.deltaX,
|
|
385
|
+
deltaY: e.deltaY,
|
|
386
|
+
clientX: localX,
|
|
387
|
+
clientY: localY,
|
|
388
|
+
...affordance !== void 0 ? { affordance } : {},
|
|
389
|
+
...body?.body !== void 0 ? { bodyTarget: body.body } : {},
|
|
390
|
+
...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
|
|
391
|
+
};
|
|
392
|
+
const result = dispatch(ev);
|
|
393
|
+
if (result === "handled") {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
e.stopPropagation();
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const onPointerDown = (e) => {
|
|
399
|
+
if ((e.button ?? 0) !== 0) return;
|
|
400
|
+
if (!canvas) return;
|
|
401
|
+
routeDown(e.pointerId, e.clientX, e.clientY);
|
|
402
|
+
const w = toWorld(e.clientX, e.clientY);
|
|
403
|
+
const affordance = target().affordanceAt?.(w) ?? void 0;
|
|
404
|
+
const body = target().classifyTarget?.(w);
|
|
405
|
+
const bodyTarget = body?.body;
|
|
406
|
+
const bodyKind = body?.kind;
|
|
407
|
+
const ev = {
|
|
408
|
+
kind: "pointerdown",
|
|
409
|
+
target: e.target,
|
|
410
|
+
pointerId: e.pointerId,
|
|
411
|
+
x: w.x,
|
|
412
|
+
y: w.y,
|
|
413
|
+
clientX: e.clientX,
|
|
414
|
+
clientY: e.clientY,
|
|
415
|
+
altKey: e.altKey,
|
|
416
|
+
ctrlKey: e.ctrlKey,
|
|
417
|
+
metaKey: e.metaKey,
|
|
418
|
+
shiftKey: e.shiftKey,
|
|
419
|
+
...stylusOf(e),
|
|
420
|
+
...affordance !== void 0 ? { affordance } : {},
|
|
421
|
+
...bodyTarget !== void 0 ? { bodyTarget } : {},
|
|
422
|
+
...bodyKind !== void 0 ? { bodyKind } : {}
|
|
423
|
+
};
|
|
424
|
+
held.set(e.pointerId, {
|
|
425
|
+
session: openPointerSession(canvas, e, {
|
|
426
|
+
onMove: onPointerMove,
|
|
427
|
+
onEnd: onPointerUp,
|
|
428
|
+
onCancel: () => {
|
|
429
|
+
onPointerCancel(e.pointerId);
|
|
430
|
+
}
|
|
431
|
+
}),
|
|
432
|
+
pos: { x: e.clientX, y: e.clientY },
|
|
433
|
+
// Deferred until the first past-threshold pointermove: a bare
|
|
434
|
+
// press-and-release should fire `click`, not start a drag action.
|
|
435
|
+
buffered: { ev, clientX: e.clientX, clientY: e.clientY },
|
|
436
|
+
down: {
|
|
437
|
+
target: e.target,
|
|
438
|
+
clientX: e.clientX,
|
|
439
|
+
clientY: e.clientY,
|
|
440
|
+
worldX: w.x,
|
|
441
|
+
worldY: w.y,
|
|
442
|
+
affordance,
|
|
443
|
+
bodyTarget,
|
|
444
|
+
bodyKind,
|
|
445
|
+
altKey: e.altKey,
|
|
446
|
+
ctrlKey: e.ctrlKey,
|
|
447
|
+
metaKey: e.metaKey,
|
|
448
|
+
shiftKey: e.shiftKey
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
dispatch({ ...ev, stage: "press" });
|
|
452
|
+
if ((e.pointerType === "touch" || e.pointerType === "pen") && held.size === 1) {
|
|
453
|
+
cancelLongPress(e.pointerId);
|
|
454
|
+
longPressTimers.set(
|
|
455
|
+
e.pointerId,
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
longPressTimers.delete(e.pointerId);
|
|
458
|
+
if (disposed) return;
|
|
459
|
+
fireLongPress(e.pointerId);
|
|
460
|
+
}, LONG_PRESS_MS)
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
if (held.size >= 2) {
|
|
464
|
+
cancelAllLongPress();
|
|
465
|
+
for (const p of held.values()) p.buffered = null;
|
|
466
|
+
const { centroid, spread } = heldGeometry();
|
|
467
|
+
if (!multiTouchStart) {
|
|
468
|
+
multiTouchStart = { fingers: held.size, centroid: { ...centroid } };
|
|
469
|
+
} else {
|
|
470
|
+
multiTouchStart.fingers = held.size;
|
|
471
|
+
}
|
|
472
|
+
const mt = {
|
|
473
|
+
kind: "multitouch",
|
|
474
|
+
fingers: held.size,
|
|
475
|
+
altKey: e.altKey,
|
|
476
|
+
ctrlKey: e.ctrlKey,
|
|
477
|
+
metaKey: e.metaKey,
|
|
478
|
+
shiftKey: e.shiftKey,
|
|
479
|
+
centroid,
|
|
480
|
+
spread
|
|
481
|
+
};
|
|
482
|
+
dispatch(mt);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
const onPointerMove = (e) => {
|
|
486
|
+
routeAt(e.pointerId, e.clientX, e.clientY);
|
|
487
|
+
const rec = held.get(e.pointerId);
|
|
488
|
+
if (rec) rec.pos = { x: e.clientX, y: e.clientY };
|
|
489
|
+
const buffered = rec?.buffered;
|
|
490
|
+
if (rec && buffered) {
|
|
491
|
+
const dx = e.clientX - buffered.clientX;
|
|
492
|
+
const dy = e.clientY - buffered.clientY;
|
|
493
|
+
if (Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
|
|
494
|
+
rec.buffered = null;
|
|
495
|
+
cancelLongPress(e.pointerId);
|
|
496
|
+
dispatch(buffered.ev);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const mtGestureId = `multitouch-${held.size}`;
|
|
500
|
+
if (held.size >= 2 && dispatcherNow().inFlight().has(mtGestureId)) {
|
|
501
|
+
const { centroid, spread } = heldGeometry();
|
|
502
|
+
const mtEv = {
|
|
503
|
+
kind: "multitouch",
|
|
504
|
+
fingers: held.size,
|
|
505
|
+
altKey: e.altKey,
|
|
506
|
+
ctrlKey: e.ctrlKey,
|
|
507
|
+
metaKey: e.metaKey,
|
|
508
|
+
shiftKey: e.shiftKey,
|
|
509
|
+
centroid,
|
|
510
|
+
spread
|
|
511
|
+
};
|
|
512
|
+
dispatch(mtEv);
|
|
513
|
+
}
|
|
514
|
+
const w = toWorld(e.clientX, e.clientY);
|
|
515
|
+
const ev = {
|
|
516
|
+
kind: "pointermove",
|
|
517
|
+
pointerId: e.pointerId,
|
|
518
|
+
x: w.x,
|
|
519
|
+
y: w.y,
|
|
520
|
+
clientX: e.clientX,
|
|
521
|
+
clientY: e.clientY,
|
|
522
|
+
altKey: e.altKey,
|
|
523
|
+
ctrlKey: e.ctrlKey,
|
|
524
|
+
metaKey: e.metaKey,
|
|
525
|
+
shiftKey: e.shiftKey,
|
|
526
|
+
...stylusOf(e)
|
|
527
|
+
};
|
|
528
|
+
dispatch(ev);
|
|
529
|
+
lastHover = {
|
|
530
|
+
clientX: e.clientX,
|
|
531
|
+
clientY: e.clientY,
|
|
532
|
+
altKey: e.altKey,
|
|
533
|
+
ctrlKey: e.ctrlKey,
|
|
534
|
+
metaKey: e.metaKey,
|
|
535
|
+
shiftKey: e.shiftKey
|
|
536
|
+
};
|
|
537
|
+
refreshHoverCursor();
|
|
538
|
+
};
|
|
539
|
+
let lastHover = null;
|
|
540
|
+
let cursorOverridden = false;
|
|
541
|
+
const clearHoverCursor = () => {
|
|
542
|
+
paintedCursorRef.current?.()?.setOverride(null);
|
|
543
|
+
if (cursorOverridden && canvas) {
|
|
544
|
+
canvas.style.cursor = "";
|
|
545
|
+
cursorOverridden = false;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const applyHoverCursor = (spec) => {
|
|
549
|
+
const tier = spec === null ? null : resolveCursorTier(spec);
|
|
550
|
+
if (!tier || !canvas) {
|
|
551
|
+
clearHoverCursor();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const painted = tier.kind === "painted" ? tier : null;
|
|
555
|
+
paintedCursorRef.current?.()?.setOverride(painted);
|
|
556
|
+
const cursor = tier.kind === "css" ? tier.css : "none";
|
|
557
|
+
if (!cursor) {
|
|
558
|
+
clearHoverCursor();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
canvas.style.cursor = cursor;
|
|
562
|
+
cursorOverridden = true;
|
|
563
|
+
};
|
|
564
|
+
function pushPaintedPointer() {
|
|
565
|
+
const painted = paintedCursorRef.current?.();
|
|
566
|
+
if (!painted?.active() || !canvas || !lastHover) return;
|
|
567
|
+
const r = canvas.getBoundingClientRect();
|
|
568
|
+
painted.setPointer(lastHover.clientX - r.left, lastHover.clientY - r.top);
|
|
569
|
+
}
|
|
570
|
+
function refreshHoverCursor() {
|
|
571
|
+
resolveHoverCursor();
|
|
572
|
+
pushPaintedPointer();
|
|
573
|
+
}
|
|
574
|
+
function resolveHoverCursor() {
|
|
575
|
+
if (!canvas || !lastHover) return;
|
|
576
|
+
if (held.size > 0 || dispatcherNow().inFlight().size > 0) {
|
|
577
|
+
applyHoverCursor(dispatcherNow().inFlightCursor());
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const h = lastHover;
|
|
581
|
+
routeAt(null, h.clientX, h.clientY);
|
|
582
|
+
const w = toWorld(h.clientX, h.clientY);
|
|
583
|
+
const affordance = target().affordanceAt?.(w) ?? void 0;
|
|
584
|
+
if (affordance?.cursor) {
|
|
585
|
+
applyHoverCursor(affordance.cursor);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const hoverBody = target().classifyTarget?.(w);
|
|
589
|
+
const bodyTarget = hoverBody?.body;
|
|
590
|
+
const bodyKind = hoverBody?.kind;
|
|
591
|
+
const predicted = dispatcherNow().resolveOnly(
|
|
592
|
+
{
|
|
593
|
+
kind: "pointerdown",
|
|
594
|
+
x: w.x,
|
|
595
|
+
y: w.y,
|
|
596
|
+
clientX: h.clientX,
|
|
597
|
+
clientY: h.clientY,
|
|
598
|
+
altKey: h.altKey,
|
|
599
|
+
ctrlKey: h.ctrlKey,
|
|
600
|
+
metaKey: h.metaKey,
|
|
601
|
+
shiftKey: h.shiftKey,
|
|
602
|
+
...affordance !== void 0 ? { affordance } : {},
|
|
603
|
+
...bodyTarget !== void 0 ? { bodyTarget } : {},
|
|
604
|
+
...bodyKind !== void 0 ? { bodyKind } : {}
|
|
605
|
+
},
|
|
606
|
+
ctxNow()
|
|
607
|
+
);
|
|
608
|
+
applyHoverCursor(predicted?.action.cursor ?? null);
|
|
609
|
+
}
|
|
610
|
+
const scheduleHoverCursorRefresh = (e) => {
|
|
611
|
+
if (!lastHover) return;
|
|
612
|
+
lastHover = {
|
|
613
|
+
...lastHover,
|
|
614
|
+
altKey: e.altKey,
|
|
615
|
+
ctrlKey: e.ctrlKey,
|
|
616
|
+
metaKey: e.metaKey,
|
|
617
|
+
shiftKey: e.shiftKey
|
|
618
|
+
};
|
|
619
|
+
setTimeout(() => {
|
|
620
|
+
if (!disposed) refreshHoverCursor();
|
|
621
|
+
}, 0);
|
|
622
|
+
};
|
|
623
|
+
const onHoverMove = (e) => {
|
|
624
|
+
if (held.has(e.pointerId)) return;
|
|
625
|
+
onPointerMove(e);
|
|
626
|
+
};
|
|
627
|
+
const onHoverPointerLeave = () => {
|
|
628
|
+
lastHover = null;
|
|
629
|
+
paintedCursorRef.current?.()?.clearPointer();
|
|
630
|
+
clearHoverCursor();
|
|
631
|
+
};
|
|
632
|
+
const onPointerUp = (e) => {
|
|
633
|
+
routeAt(e.pointerId, e.clientX, e.clientY);
|
|
634
|
+
cancelLongPress(e.pointerId);
|
|
635
|
+
const prevSize = held.size;
|
|
636
|
+
const rec = held.get(e.pointerId);
|
|
637
|
+
held.delete(e.pointerId);
|
|
638
|
+
const staysUnderThreshold = rec?.buffered != null;
|
|
639
|
+
if (prevSize >= 2 && held.size < 2) {
|
|
640
|
+
const mtGestureId = `multitouch-${prevSize}`;
|
|
641
|
+
if (dispatcherNow().inFlight().has(mtGestureId)) {
|
|
642
|
+
dispatcherNow().cancelAll("commit");
|
|
643
|
+
}
|
|
644
|
+
const start = multiTouchStart;
|
|
645
|
+
if (start) {
|
|
646
|
+
const centroid = toCanvasLocal(
|
|
647
|
+
computeMultiTouchGeometry([
|
|
648
|
+
...[...held.values()].map((p) => p.pos),
|
|
649
|
+
{ x: e.clientX, y: e.clientY }
|
|
650
|
+
]).centroid
|
|
651
|
+
);
|
|
652
|
+
const dx = centroid.x - start.centroid.x;
|
|
653
|
+
const dy = centroid.y - start.centroid.y;
|
|
654
|
+
if (Math.hypot(dx, dy) <= TAP_THRESHOLD_PX) {
|
|
655
|
+
const tap = {
|
|
656
|
+
kind: "multitouchtap",
|
|
657
|
+
fingers: start.fingers,
|
|
658
|
+
altKey: e.altKey,
|
|
659
|
+
ctrlKey: e.ctrlKey,
|
|
660
|
+
metaKey: e.metaKey,
|
|
661
|
+
shiftKey: e.shiftKey
|
|
662
|
+
};
|
|
663
|
+
dispatch(tap);
|
|
664
|
+
}
|
|
665
|
+
multiTouchStart = null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const movedDuringDrag = rec ? rec.down.clientX !== e.clientX || rec.down.clientY !== e.clientY : true;
|
|
669
|
+
const hadDragInFlight = dispatcherNow().inFlight().has(pointerGestureId(e.pointerId)) && movedDuringDrag;
|
|
670
|
+
const wUp = toWorld(e.clientX, e.clientY);
|
|
671
|
+
const ev = {
|
|
672
|
+
kind: "pointerup",
|
|
673
|
+
pointerId: e.pointerId,
|
|
674
|
+
x: wUp.x,
|
|
675
|
+
y: wUp.y,
|
|
676
|
+
clientX: e.clientX,
|
|
677
|
+
clientY: e.clientY,
|
|
678
|
+
altKey: e.altKey,
|
|
679
|
+
ctrlKey: e.ctrlKey,
|
|
680
|
+
metaKey: e.metaKey,
|
|
681
|
+
shiftKey: e.shiftKey
|
|
682
|
+
};
|
|
683
|
+
dispatch(ev);
|
|
684
|
+
const down = rec?.down;
|
|
685
|
+
if (!hadDragInFlight && staysUnderThreshold && down) {
|
|
686
|
+
const wClick = toWorld(e.clientX, e.clientY);
|
|
687
|
+
const clickEv = {
|
|
688
|
+
kind: "click",
|
|
689
|
+
target: e.target,
|
|
690
|
+
altKey: e.altKey,
|
|
691
|
+
ctrlKey: e.ctrlKey,
|
|
692
|
+
metaKey: e.metaKey,
|
|
693
|
+
shiftKey: e.shiftKey,
|
|
694
|
+
worldX: wClick.x,
|
|
695
|
+
worldY: wClick.y,
|
|
696
|
+
pressX: down.worldX,
|
|
697
|
+
pressY: down.worldY,
|
|
698
|
+
...down.affordance !== void 0 ? { affordance: down.affordance } : {},
|
|
699
|
+
...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
|
|
700
|
+
...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
|
|
701
|
+
};
|
|
702
|
+
dispatch(clickEv);
|
|
703
|
+
const now = Date.now();
|
|
704
|
+
const prev = lastClickRef.current;
|
|
705
|
+
const dx = e.clientX - (prev?.clientX ?? 0);
|
|
706
|
+
const dy = e.clientY - (prev?.clientY ?? 0);
|
|
707
|
+
if (prev && now - prev.t < DOUBLE_CLICK_MAX_MS && Math.hypot(dx, dy) < DOUBLE_CLICK_MAX_PX) {
|
|
708
|
+
const dblEv = {
|
|
709
|
+
kind: "doubleclick",
|
|
710
|
+
target: e.target,
|
|
711
|
+
altKey: e.altKey,
|
|
712
|
+
ctrlKey: e.ctrlKey,
|
|
713
|
+
metaKey: e.metaKey,
|
|
714
|
+
shiftKey: e.shiftKey,
|
|
715
|
+
worldX: wClick.x,
|
|
716
|
+
worldY: wClick.y,
|
|
717
|
+
...down.affordance !== void 0 ? { affordance: down.affordance } : {},
|
|
718
|
+
...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
|
|
719
|
+
...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
|
|
720
|
+
};
|
|
721
|
+
onDoubleClickRef.current?.({ x: wClick.x, y: wClick.y });
|
|
722
|
+
dispatch(dblEv);
|
|
723
|
+
lastClickRef.current = null;
|
|
724
|
+
} else {
|
|
725
|
+
lastClickRef.current = { t: now, clientX: e.clientX, clientY: e.clientY };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
routeRelease(e.pointerId);
|
|
729
|
+
lastHover = {
|
|
730
|
+
clientX: e.clientX,
|
|
731
|
+
clientY: e.clientY,
|
|
732
|
+
altKey: e.altKey,
|
|
733
|
+
ctrlKey: e.ctrlKey,
|
|
734
|
+
metaKey: e.metaKey,
|
|
735
|
+
shiftKey: e.shiftKey
|
|
736
|
+
};
|
|
737
|
+
setTimeout(() => {
|
|
738
|
+
if (!disposed) refreshHoverCursor();
|
|
739
|
+
}, 0);
|
|
740
|
+
};
|
|
741
|
+
const onPointerCancel = (pointerId) => {
|
|
742
|
+
if (disposed) return;
|
|
743
|
+
const rec = held.get(pointerId);
|
|
744
|
+
if (!rec) return;
|
|
745
|
+
held.delete(pointerId);
|
|
746
|
+
routeAt(pointerId, rec.pos.x, rec.pos.y);
|
|
747
|
+
cancelLongPress(pointerId);
|
|
748
|
+
multiTouchStart = null;
|
|
749
|
+
const ev = {
|
|
750
|
+
kind: "pointercancel",
|
|
751
|
+
pointerId,
|
|
752
|
+
altKey: rec.down.altKey,
|
|
753
|
+
ctrlKey: rec.down.ctrlKey,
|
|
754
|
+
metaKey: rec.down.metaKey,
|
|
755
|
+
shiftKey: rec.down.shiftKey
|
|
756
|
+
};
|
|
757
|
+
dispatch(ev);
|
|
758
|
+
routeRelease(pointerId);
|
|
759
|
+
};
|
|
760
|
+
const onContextMenu = (e) => {
|
|
761
|
+
e.preventDefault();
|
|
762
|
+
routeAt(null, e.clientX, e.clientY);
|
|
763
|
+
const w = toWorld(e.clientX, e.clientY);
|
|
764
|
+
const affordance = target().affordanceAt?.(w) ?? void 0;
|
|
765
|
+
const menuBody = target().classifyTarget?.(w);
|
|
766
|
+
const ev = {
|
|
767
|
+
kind: "contextmenu",
|
|
768
|
+
target: e.target,
|
|
769
|
+
altKey: e.altKey,
|
|
770
|
+
ctrlKey: e.ctrlKey,
|
|
771
|
+
metaKey: e.metaKey,
|
|
772
|
+
shiftKey: e.shiftKey,
|
|
773
|
+
worldX: w.x,
|
|
774
|
+
worldY: w.y,
|
|
775
|
+
...affordance !== void 0 ? { affordance } : {},
|
|
776
|
+
...menuBody?.body !== void 0 ? { bodyTarget: menuBody.body } : {},
|
|
777
|
+
...menuBody?.kind !== void 0 ? { bodyKind: menuBody.kind } : {}
|
|
778
|
+
};
|
|
779
|
+
dispatch(ev);
|
|
780
|
+
};
|
|
781
|
+
const onDragOver = (e) => {
|
|
782
|
+
if (!e.dataTransfer) return;
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
e.dataTransfer.dropEffect = "copy";
|
|
785
|
+
canvas?.classList.add(DROPOVER_CLASS);
|
|
786
|
+
};
|
|
787
|
+
const onDragLeave = () => {
|
|
788
|
+
canvas?.classList.remove(DROPOVER_CLASS);
|
|
789
|
+
};
|
|
790
|
+
const onDrop = (e) => {
|
|
791
|
+
canvas?.classList.remove(DROPOVER_CLASS);
|
|
792
|
+
const dt = e.dataTransfer;
|
|
793
|
+
if (!dt) return;
|
|
794
|
+
e.preventDefault();
|
|
795
|
+
routeAt(null, e.clientX, e.clientY);
|
|
796
|
+
const dropOn = target().dispatcher;
|
|
797
|
+
const w = toWorld(e.clientX, e.clientY);
|
|
798
|
+
const base = {
|
|
799
|
+
altKey: e.altKey,
|
|
800
|
+
ctrlKey: e.ctrlKey,
|
|
801
|
+
metaKey: e.metaKey,
|
|
802
|
+
shiftKey: e.shiftKey
|
|
803
|
+
};
|
|
804
|
+
void itemsFromDataTransfer(dt).then((items) => {
|
|
805
|
+
if (disposed) return;
|
|
806
|
+
if (items.length === 0) return;
|
|
807
|
+
dispatch({
|
|
808
|
+
kind: "drop",
|
|
809
|
+
items,
|
|
810
|
+
x: w.x,
|
|
811
|
+
y: w.y,
|
|
812
|
+
clientX: e.clientX,
|
|
813
|
+
clientY: e.clientY,
|
|
814
|
+
...base
|
|
815
|
+
}, dropOn);
|
|
816
|
+
});
|
|
817
|
+
};
|
|
818
|
+
const onPaste = (e) => {
|
|
819
|
+
if (isEditableTarget(e.target)) return;
|
|
820
|
+
const cd = e.clipboardData;
|
|
821
|
+
if (!cd) return;
|
|
822
|
+
const items = itemsFromClipboardData(cd);
|
|
823
|
+
if (items.length === 0) return;
|
|
824
|
+
const result = dispatch({
|
|
825
|
+
kind: "paste",
|
|
826
|
+
items,
|
|
827
|
+
// ClipboardEvent carries no modifier state.
|
|
828
|
+
altKey: false,
|
|
829
|
+
ctrlKey: false,
|
|
830
|
+
metaKey: false,
|
|
831
|
+
shiftKey: false
|
|
832
|
+
});
|
|
833
|
+
if (result === "handled") e.preventDefault();
|
|
834
|
+
};
|
|
835
|
+
if (keyboard) {
|
|
836
|
+
window.addEventListener("keydown", onKeyDown);
|
|
837
|
+
window.addEventListener("keyup", onKeyUp);
|
|
838
|
+
}
|
|
839
|
+
window.addEventListener("blur", onWindowBlur);
|
|
840
|
+
window.addEventListener("keydown", scheduleHoverCursorRefresh);
|
|
841
|
+
window.addEventListener("keyup", scheduleHoverCursorRefresh);
|
|
842
|
+
canvas?.addEventListener("pointerleave", onHoverPointerLeave);
|
|
843
|
+
canvas?.addEventListener("wheel", onWheel, { passive: false });
|
|
844
|
+
canvas?.addEventListener("pointerdown", onPointerDown);
|
|
845
|
+
canvas?.addEventListener("pointermove", onHoverMove);
|
|
846
|
+
canvas?.addEventListener("contextmenu", onContextMenu);
|
|
847
|
+
canvas?.addEventListener("dragenter", onDragOver);
|
|
848
|
+
canvas?.addEventListener("dragover", onDragOver);
|
|
849
|
+
canvas?.addEventListener("dragleave", onDragLeave);
|
|
850
|
+
canvas?.addEventListener("drop", onDrop);
|
|
851
|
+
window.addEventListener("paste", onPaste);
|
|
852
|
+
return () => {
|
|
853
|
+
if (keyboard) {
|
|
854
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
855
|
+
window.removeEventListener("keyup", onKeyUp);
|
|
856
|
+
}
|
|
857
|
+
window.removeEventListener("blur", onWindowBlur);
|
|
858
|
+
window.removeEventListener("keydown", scheduleHoverCursorRefresh);
|
|
859
|
+
window.removeEventListener("keyup", scheduleHoverCursorRefresh);
|
|
860
|
+
canvas?.removeEventListener("pointerleave", onHoverPointerLeave);
|
|
861
|
+
canvas?.removeEventListener("wheel", onWheel);
|
|
862
|
+
canvas?.removeEventListener("pointerdown", onPointerDown);
|
|
863
|
+
canvas?.removeEventListener("pointermove", onHoverMove);
|
|
864
|
+
canvas?.removeEventListener("contextmenu", onContextMenu);
|
|
865
|
+
canvas?.removeEventListener("dragenter", onDragOver);
|
|
866
|
+
canvas?.removeEventListener("dragover", onDragOver);
|
|
867
|
+
canvas?.removeEventListener("dragleave", onDragLeave);
|
|
868
|
+
canvas?.removeEventListener("drop", onDrop);
|
|
869
|
+
window.removeEventListener("paste", onPaste);
|
|
870
|
+
canvas?.classList.remove(DROPOVER_CLASS);
|
|
871
|
+
clearHoverCursor();
|
|
872
|
+
cancelAllLongPress();
|
|
873
|
+
disposed = true;
|
|
874
|
+
for (const p of held.values()) p.session.cancel();
|
|
875
|
+
held.clear();
|
|
876
|
+
for (const d of allDispatchers()) d.cancelAll("cancel");
|
|
877
|
+
};
|
|
878
|
+
}, [enabled, keyboard, canvasRef]);
|
|
879
|
+
}
|
|
880
|
+
var ActionsContext = createContext(null);
|
|
881
|
+
function useMuted(base) {
|
|
882
|
+
const mutedRef = useRef(/* @__PURE__ */ new Map());
|
|
883
|
+
const listenersRef = useRef(/* @__PURE__ */ new Set());
|
|
884
|
+
const cacheRef = useRef(null);
|
|
885
|
+
return useMemo(() => {
|
|
886
|
+
if (!base) return null;
|
|
887
|
+
const isMuted = (id) => mutedRef.current.has(id);
|
|
888
|
+
const notify = () => {
|
|
889
|
+
for (const l of listenersRef.current) {
|
|
890
|
+
try {
|
|
891
|
+
l();
|
|
892
|
+
} catch (err) {
|
|
893
|
+
console.error("weasel ActionsRegistry: subscriber threw", err);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
return {
|
|
898
|
+
...base,
|
|
899
|
+
list: () => {
|
|
900
|
+
const src = base.list();
|
|
901
|
+
if (mutedRef.current.size === 0) return src;
|
|
902
|
+
const cached = cacheRef.current;
|
|
903
|
+
if (cached && cached.src === src) return cached.out;
|
|
904
|
+
const out = Object.freeze(src.filter((a) => !mutedRef.current.has(a.id)));
|
|
905
|
+
cacheRef.current = { src, out };
|
|
906
|
+
return out;
|
|
907
|
+
},
|
|
908
|
+
trigger: (id, params) => isMuted(id) ? false : base.trigger(id, params),
|
|
909
|
+
begin: (id, params) => isMuted(id) ? null : base.begin(id, params),
|
|
910
|
+
subscribe: (listener) => {
|
|
911
|
+
const offBase = base.subscribe(listener);
|
|
912
|
+
listenersRef.current.add(listener);
|
|
913
|
+
return () => {
|
|
914
|
+
offBase();
|
|
915
|
+
listenersRef.current.delete(listener);
|
|
916
|
+
};
|
|
917
|
+
},
|
|
918
|
+
mute: (id) => {
|
|
919
|
+
mutedRef.current.set(id, (mutedRef.current.get(id) ?? 0) + 1);
|
|
920
|
+
cacheRef.current = null;
|
|
921
|
+
notify();
|
|
922
|
+
let released = false;
|
|
923
|
+
return () => {
|
|
924
|
+
if (released) return;
|
|
925
|
+
released = true;
|
|
926
|
+
const held = mutedRef.current.get(id);
|
|
927
|
+
if (held === void 0) return;
|
|
928
|
+
if (held > 1) mutedRef.current.set(id, held - 1);
|
|
929
|
+
else mutedRef.current.delete(id);
|
|
930
|
+
cacheRef.current = null;
|
|
931
|
+
notify();
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
}, [base]);
|
|
936
|
+
}
|
|
937
|
+
function ActionsScope({ children }) {
|
|
938
|
+
const parent = useActionsRegistry();
|
|
939
|
+
const scoped = useMuted(parent);
|
|
940
|
+
if (!scoped) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
941
|
+
return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: scoped, children });
|
|
942
|
+
}
|
|
943
|
+
function pushOwner(ref, value) {
|
|
944
|
+
if (value === null) return () => {
|
|
945
|
+
};
|
|
946
|
+
ref.current.push(value);
|
|
947
|
+
let released = false;
|
|
948
|
+
return () => {
|
|
949
|
+
if (released) return;
|
|
950
|
+
released = true;
|
|
951
|
+
const i = ref.current.lastIndexOf(value);
|
|
952
|
+
if (i !== -1) ref.current.splice(i, 1);
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function ActionsProvider({ children }) {
|
|
956
|
+
const actionsRef = useRef(/* @__PURE__ */ new Map());
|
|
957
|
+
const liveAction = (id) => actionsRef.current.get(id)?.at(-1);
|
|
958
|
+
const versionRef = useRef(0);
|
|
959
|
+
const cachedRef = useRef([]);
|
|
960
|
+
const cachedVerRef = useRef(-1);
|
|
961
|
+
const listenersRef = useRef(/* @__PURE__ */ new Set());
|
|
962
|
+
const depReg = useOptionalDepRegistry();
|
|
963
|
+
const depRegRef = useRef(depReg);
|
|
964
|
+
depRegRef.current = depReg;
|
|
965
|
+
const warnedRef = useRef(false);
|
|
966
|
+
const wiredDepRegRef = useRef([]);
|
|
967
|
+
const dispatcherRef = useRef([]);
|
|
968
|
+
const store = useMemo(() => {
|
|
969
|
+
const snapshot = () => {
|
|
970
|
+
const v = versionRef.current;
|
|
971
|
+
if (cachedVerRef.current === v) return cachedRef.current;
|
|
972
|
+
const out = Object.freeze(
|
|
973
|
+
Array.from(actionsRef.current.values(), (stack) => stack[stack.length - 1])
|
|
974
|
+
);
|
|
975
|
+
cachedRef.current = out;
|
|
976
|
+
cachedVerRef.current = v;
|
|
977
|
+
return out;
|
|
978
|
+
};
|
|
979
|
+
const notify = () => {
|
|
980
|
+
for (const l of listenersRef.current) {
|
|
981
|
+
try {
|
|
982
|
+
l();
|
|
983
|
+
} catch (err) {
|
|
984
|
+
console.error("weasel ActionsRegistry: subscriber threw", err);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
return {
|
|
989
|
+
register: (action) => {
|
|
990
|
+
validateActionId(action.id);
|
|
991
|
+
validateActionDefaultBinding(action);
|
|
992
|
+
const stack = actionsRef.current.get(action.id);
|
|
993
|
+
if (stack) stack.push(action);
|
|
994
|
+
else actionsRef.current.set(action.id, [action]);
|
|
995
|
+
versionRef.current++;
|
|
996
|
+
notify();
|
|
997
|
+
let released = false;
|
|
998
|
+
return () => {
|
|
999
|
+
if (released) return;
|
|
1000
|
+
released = true;
|
|
1001
|
+
const cur = actionsRef.current.get(action.id);
|
|
1002
|
+
if (!cur) return;
|
|
1003
|
+
const i = cur.lastIndexOf(action);
|
|
1004
|
+
if (i === -1) return;
|
|
1005
|
+
cur.splice(i, 1);
|
|
1006
|
+
if (cur.length === 0) actionsRef.current.delete(action.id);
|
|
1007
|
+
versionRef.current++;
|
|
1008
|
+
notify();
|
|
1009
|
+
};
|
|
1010
|
+
},
|
|
1011
|
+
unregister: (id) => {
|
|
1012
|
+
if (actionsRef.current.delete(id)) {
|
|
1013
|
+
versionRef.current++;
|
|
1014
|
+
notify();
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
list: () => snapshot(),
|
|
1018
|
+
trigger: (id, params) => {
|
|
1019
|
+
const a = liveAction(id);
|
|
1020
|
+
if (!a) return false;
|
|
1021
|
+
try {
|
|
1022
|
+
if (a.invoker && a.invoker.timing === "immediate") {
|
|
1023
|
+
const r = wiredDepRegRef.current.at(-1) ?? depRegRef.current;
|
|
1024
|
+
const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
|
|
1025
|
+
selection: r.get("selection"),
|
|
1026
|
+
scene: r.get("scene"),
|
|
1027
|
+
history: r.get("history"),
|
|
1028
|
+
view: r.get("view"),
|
|
1029
|
+
pointer: r.get("pointer"),
|
|
1030
|
+
activeTool: r.get("activeTool"),
|
|
1031
|
+
booleansAdapter: r.get("booleansAdapter")
|
|
1032
|
+
};
|
|
1033
|
+
a.invoker.run(deps, params);
|
|
1034
|
+
}
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
|
|
1037
|
+
}
|
|
1038
|
+
return true;
|
|
1039
|
+
},
|
|
1040
|
+
subscribe: (listener) => {
|
|
1041
|
+
listenersRef.current.add(listener);
|
|
1042
|
+
return () => {
|
|
1043
|
+
listenersRef.current.delete(listener);
|
|
1044
|
+
};
|
|
1045
|
+
},
|
|
1046
|
+
setDispatcher: (d) => {
|
|
1047
|
+
if (d && dispatcherRef.current.length > 0 && dispatcherRef.current.at(-1) !== d && !warnedRef.current) {
|
|
1048
|
+
warnedRef.current = true;
|
|
1049
|
+
warnSharedScope();
|
|
1050
|
+
}
|
|
1051
|
+
return pushOwner(dispatcherRef, d);
|
|
1052
|
+
},
|
|
1053
|
+
setDepRegistry: (r) => pushOwner(wiredDepRegRef, r),
|
|
1054
|
+
begin: (id, params) => {
|
|
1055
|
+
const disp = dispatcherRef.current.at(-1) ?? null;
|
|
1056
|
+
if (!disp) return null;
|
|
1057
|
+
const r = wiredDepRegRef.current.at(-1) ?? depRegRef.current;
|
|
1058
|
+
const a = liveAction(id);
|
|
1059
|
+
const deps = !r ? {} : a?.requires ? buildDepsFromRequires(a, r) : {
|
|
1060
|
+
selection: r.get("selection"),
|
|
1061
|
+
scene: r.get("scene"),
|
|
1062
|
+
history: r.get("history"),
|
|
1063
|
+
view: r.get("view"),
|
|
1064
|
+
pointer: r.get("pointer"),
|
|
1065
|
+
activeTool: r.get("activeTool"),
|
|
1066
|
+
booleansAdapter: r.get("booleansAdapter")
|
|
1067
|
+
};
|
|
1068
|
+
return disp.beginUiOngoing(id, deps, params);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}, []);
|
|
1072
|
+
const registry = useMuted(store);
|
|
1073
|
+
return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry, children });
|
|
1074
|
+
}
|
|
1075
|
+
function warnSharedScope() {
|
|
1076
|
+
if (!IS_DEV) return;
|
|
1077
|
+
console.warn(
|
|
1078
|
+
"weasel: a second dispatcher claimed this <ActionsProvider>, so only the newest <SceneCanvas> under it will respond to input. Give each canvas its own scope with <WeaselProvider isolate>."
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
var IS_DEV = (() => {
|
|
1082
|
+
try {
|
|
1083
|
+
return Boolean(import.meta.env?.DEV);
|
|
1084
|
+
} catch {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
})();
|
|
1088
|
+
function useActionsRegistry() {
|
|
1089
|
+
return useContext(ActionsContext);
|
|
1090
|
+
}
|
|
1091
|
+
function useAction(action) {
|
|
1092
|
+
const reg = useActionsRegistry();
|
|
1093
|
+
useEffect(() => {
|
|
1094
|
+
if (!reg) {
|
|
1095
|
+
if (IS_DEV) {
|
|
1096
|
+
console.warn(
|
|
1097
|
+
`useAction("${action.id}"): no <ActionsProvider> is in scope, so the action was not registered and its bindings will never fire.`
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
return reg.register(action);
|
|
1103
|
+
}, [reg, action]);
|
|
1104
|
+
}
|
|
1105
|
+
var NO_TRIGGERS = /* @__PURE__ */ new Set();
|
|
1106
|
+
function useContributions(opts) {
|
|
1107
|
+
const ctx = useActiveToolContext();
|
|
1108
|
+
const actionsRegistry = useActionsRegistry();
|
|
1109
|
+
const hasInitializedRef = useRef(false);
|
|
1110
|
+
const isFirstRender = !hasInitializedRef.current;
|
|
1111
|
+
const needsFirstMountSyncRef = useRef(false);
|
|
1112
|
+
if (isFirstRender) {
|
|
1113
|
+
hasInitializedRef.current = true;
|
|
1114
|
+
needsFirstMountSyncRef.current = ctx.active === "select" && opts.focused !== "select";
|
|
1115
|
+
}
|
|
1116
|
+
useEffect(() => {
|
|
1117
|
+
if (!needsFirstMountSyncRef.current) return;
|
|
1118
|
+
needsFirstMountSyncRef.current = false;
|
|
1119
|
+
ctx.setActive(opts.focused);
|
|
1120
|
+
}, []);
|
|
1121
|
+
const focused = isFirstRender && ctx.active === "select" && opts.focused !== "select" ? opts.focused : ctx.active;
|
|
1122
|
+
const entriesRef = useRef(opts.entries);
|
|
1123
|
+
entriesRef.current = opts.entries;
|
|
1124
|
+
const focusedRef = useRef(focused);
|
|
1125
|
+
focusedRef.current = focused;
|
|
1126
|
+
const engagedRef = useRef(ctx.hotkeyStack);
|
|
1127
|
+
engagedRef.current = ctx.hotkeyStack;
|
|
1128
|
+
const setFocused = useCallback((id) => {
|
|
1129
|
+
ctx.setActive(id);
|
|
1130
|
+
}, [ctx]);
|
|
1131
|
+
const snapshot = useCallback(() => {
|
|
1132
|
+
const engaged = engagedRef.current;
|
|
1133
|
+
const engagedIds = new Set(engaged);
|
|
1134
|
+
const ordered = [];
|
|
1135
|
+
for (const id of engaged) {
|
|
1136
|
+
const entry = entriesRef.current.find((e) => e.id === id);
|
|
1137
|
+
if (entry && !ordered.includes(entry)) ordered.push(entry);
|
|
1138
|
+
}
|
|
1139
|
+
for (const entry of entriesRef.current) {
|
|
1140
|
+
if (!ordered.includes(entry)) ordered.push(entry);
|
|
1141
|
+
}
|
|
1142
|
+
return {
|
|
1143
|
+
ordered,
|
|
1144
|
+
state: { focusedId: focusedRef.current, heldTriggers: NO_TRIGGERS, engagedIds }
|
|
1145
|
+
};
|
|
1146
|
+
}, []);
|
|
1147
|
+
const scopedBindings = useCallback(() => {
|
|
1148
|
+
const { ordered, state } = snapshot();
|
|
1149
|
+
return scopeBindings(ordered, state);
|
|
1150
|
+
}, [snapshot]);
|
|
1151
|
+
const overlays = useCallback((position = "top") => {
|
|
1152
|
+
const { ordered, state } = snapshot();
|
|
1153
|
+
const active = [];
|
|
1154
|
+
const hotkey = [];
|
|
1155
|
+
const ambient = [];
|
|
1156
|
+
for (const entry of ordered) {
|
|
1157
|
+
if (!entry.overlay) continue;
|
|
1158
|
+
if ((entry.overlayPosition ?? "top") !== position) continue;
|
|
1159
|
+
const layers = Array.isArray(entry.overlay) ? entry.overlay : [entry.overlay];
|
|
1160
|
+
const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
|
|
1161
|
+
if (scope === "active") active.push(...layers);
|
|
1162
|
+
else if (scope === "hotkey") hotkey.push(...layers);
|
|
1163
|
+
else if (scope === "ambient") ambient.push(...layers);
|
|
1164
|
+
}
|
|
1165
|
+
return [...active, ...hotkey, ...ambient];
|
|
1166
|
+
}, [snapshot]);
|
|
1167
|
+
const offhandSig = opts.entries.map((e) => e.eligibility?.offhand ? `${e.id}:${e.eligibility.offhand}` : "").filter(Boolean).join(",");
|
|
1168
|
+
useEffect(() => {
|
|
1169
|
+
if (!actionsRegistry || offhandSig === "") return;
|
|
1170
|
+
const specs = offhandSig.split(",").map((entry) => {
|
|
1171
|
+
const [toolId, trigger] = entry.split(":");
|
|
1172
|
+
return { toolId, key: offhandKeyFor(trigger) };
|
|
1173
|
+
});
|
|
1174
|
+
return actionsRegistry.register(makeToolOffhandAction(buildToolOffhandBindings(specs)));
|
|
1175
|
+
}, [actionsRegistry, offhandSig]);
|
|
1176
|
+
const lastConflictSigRef = useRef(null);
|
|
1177
|
+
const entrySig = opts.entries.map((e) => e.id).join(",");
|
|
1178
|
+
useEffect(() => {
|
|
1179
|
+
if (!isDev()) return;
|
|
1180
|
+
if (lastConflictSigRef.current === entrySig) return;
|
|
1181
|
+
lastConflictSigRef.current = entrySig;
|
|
1182
|
+
const registry = [];
|
|
1183
|
+
const ambient = [];
|
|
1184
|
+
for (const entry of entriesRef.current) {
|
|
1185
|
+
if (entry.eligibility?.focus) registry.push(entry);
|
|
1186
|
+
if (entry.eligibility?.always || entry.eligibility?.claimed) ambient.push(entry);
|
|
1187
|
+
}
|
|
1188
|
+
reportRouteConflicts({
|
|
1189
|
+
registry,
|
|
1190
|
+
ambient,
|
|
1191
|
+
actions: actionsRegistry?.list() ?? []
|
|
1192
|
+
});
|
|
1193
|
+
}, [entrySig, actionsRegistry]);
|
|
1194
|
+
return useMemo(
|
|
1195
|
+
() => ({
|
|
1196
|
+
get entries() {
|
|
1197
|
+
return entriesRef.current;
|
|
1198
|
+
},
|
|
1199
|
+
focused,
|
|
1200
|
+
setFocused,
|
|
1201
|
+
scopedBindings,
|
|
1202
|
+
overlays
|
|
1203
|
+
}),
|
|
1204
|
+
[focused, setFocused, scopedBindings, overlays]
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/tools/useTools.ts
|
|
1209
|
+
function declareSlot(tool, slot) {
|
|
1210
|
+
const hotkey = tool.def?.hotkey;
|
|
1211
|
+
const eligibility = {
|
|
1212
|
+
...tool.eligibility,
|
|
1213
|
+
[slot]: true,
|
|
1214
|
+
...hotkey ? { offhand: hotkey } : {}
|
|
1215
|
+
};
|
|
1216
|
+
return sameEligibility(tool.eligibility, eligibility) ? tool : { ...tool, eligibility };
|
|
1217
|
+
}
|
|
1218
|
+
function sameEligibility(a, b) {
|
|
1219
|
+
if (!a) return false;
|
|
1220
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
1221
|
+
for (const key of keys) if (a[key] !== b[key]) return false;
|
|
1222
|
+
return true;
|
|
1223
|
+
}
|
|
1224
|
+
function useTools(opts) {
|
|
1225
|
+
if (!(opts.active in opts.registry)) {
|
|
1226
|
+
throw new Error(`useTools: active "${opts.active}" not in registry`);
|
|
1227
|
+
}
|
|
1228
|
+
const ctx = useActiveToolContext();
|
|
1229
|
+
const slotted = useMemo(() => {
|
|
1230
|
+
const registry = {};
|
|
1231
|
+
for (const [id, tool] of Object.entries(opts.registry)) {
|
|
1232
|
+
registry[id] = declareSlot(tool, "focus");
|
|
1233
|
+
}
|
|
1234
|
+
const ambient = (opts.ambient ?? []).map((t) => declareSlot(t, "always"));
|
|
1235
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1236
|
+
for (const tool of [...Object.values(registry), ...ambient]) {
|
|
1237
|
+
const prior = byId.get(tool.id);
|
|
1238
|
+
if (prior) byId.set(tool.id, { ...tool, eligibility: { ...prior.eligibility, ...tool.eligibility } });
|
|
1239
|
+
else byId.set(tool.id, tool);
|
|
1240
|
+
}
|
|
1241
|
+
return { registry, ambient, entries: [...byId.values()] };
|
|
1242
|
+
}, [opts.registry, opts.ambient]);
|
|
1243
|
+
const contributions = useContributions({ entries: slotted.entries, focused: opts.active });
|
|
1244
|
+
const hotkeyEngaged = ctx.hotkeyStack.at(-1) ?? null;
|
|
1245
|
+
const slottedRef = useRef(slotted);
|
|
1246
|
+
slottedRef.current = slotted;
|
|
1247
|
+
const activeRef = useRef(contributions.focused);
|
|
1248
|
+
activeRef.current = contributions.focused;
|
|
1249
|
+
const hotkeyRef = useRef(hotkeyEngaged);
|
|
1250
|
+
hotkeyRef.current = hotkeyEngaged;
|
|
1251
|
+
const setFocused = contributions.setFocused;
|
|
1252
|
+
const setActive = useCallback(
|
|
1253
|
+
(id) => {
|
|
1254
|
+
if (!(id in slottedRef.current.registry)) {
|
|
1255
|
+
throw new Error(`setActive: "${id}" not in registry`);
|
|
1256
|
+
}
|
|
1257
|
+
dlog("tools", "active:", activeRef.current, "\u2192", id);
|
|
1258
|
+
setFocused(id);
|
|
1259
|
+
},
|
|
1260
|
+
[setFocused]
|
|
1261
|
+
);
|
|
1262
|
+
const engageHotkey = useCallback(
|
|
1263
|
+
(id) => {
|
|
1264
|
+
if (!(id in slottedRef.current.registry)) {
|
|
1265
|
+
throw new Error(`engageHotkey: "${id}" not in registry`);
|
|
1266
|
+
}
|
|
1267
|
+
dlog("tools", "hotkey engaged:", id);
|
|
1268
|
+
ctx.pushHotkey(id);
|
|
1269
|
+
},
|
|
1270
|
+
[ctx]
|
|
1271
|
+
);
|
|
1272
|
+
const disengageHotkey = useCallback(() => {
|
|
1273
|
+
if (hotkeyRef.current) dlog("tools", "hotkey disengaged:", hotkeyRef.current);
|
|
1274
|
+
ctx.popHotkey();
|
|
1275
|
+
}, [ctx]);
|
|
1276
|
+
const has = useCallback(
|
|
1277
|
+
(id) => id in slottedRef.current.registry || slottedRef.current.ambient.some((t) => t.id === id),
|
|
1278
|
+
[]
|
|
1279
|
+
);
|
|
1280
|
+
const active = contributions.focused;
|
|
1281
|
+
const getActiveOverlays = contributions.overlays;
|
|
1282
|
+
const registryKey = Object.keys(slotted.registry).sort().join("|") + "#" + slotted.ambient.map((t) => t.id).join("|");
|
|
1283
|
+
return useMemo(
|
|
1284
|
+
() => ({
|
|
1285
|
+
active,
|
|
1286
|
+
setActive,
|
|
1287
|
+
hotkeyEngaged,
|
|
1288
|
+
engageHotkey,
|
|
1289
|
+
disengageHotkey,
|
|
1290
|
+
ambient: slottedRef.current.ambient,
|
|
1291
|
+
registry: slottedRef.current.registry,
|
|
1292
|
+
has,
|
|
1293
|
+
getActiveOverlays
|
|
1294
|
+
}),
|
|
1295
|
+
// `registryKey` isn't read in the body on purpose: the body reads
|
|
1296
|
+
// `slottedRef.current`, and the key is what tells us that ref's contents moved.
|
|
1297
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1298
|
+
[active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays, registryKey]
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
export { ActionsProvider, ActionsScope, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, useAction, useActionsRegistry, useActiveToolContext, useContributions, useDepRegistry, useDepSource, useGestureDispatcher, useOptionalActiveToolContext, useOptionalDepRegistry, useTools };
|
|
1303
|
+
//# sourceMappingURL=react.js.map
|
|
1304
|
+
//# sourceMappingURL=react.js.map
|