@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/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