@weasel-js/routing 1.6.1 → 1.7.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/{chunk-VEC3ACV5.js → chunk-Q3Y3DPKH.js} +747 -374
- package/dist/chunk-Q3Y3DPKH.js.map +1 -0
- package/dist/{index-CKmbNpwl.d.ts → index-Bmq4lSHY.d.ts} +561 -242
- package/dist/index.d.ts +2 -2
- package/dist/index.js +84 -8
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +217 -84
- package/dist/react.js +360 -301
- package/dist/react.js.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-VEC3ACV5.js.map +0 -1
package/dist/react.js
CHANGED
|
@@ -1,37 +1,80 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createContext, useState,
|
|
1
|
+
import { createDepRegistry, createDispatcher, scopeBindings, liveScope, isDev, reportRouteConflicts, offhandKeyFor, makeToolOffhandAction, buildToolOffhandBindings, dlog, isTool, isEditableTarget, clientToCanvasRect, openPointerSession, publishLiveDispatch, pointerGestureId, itemsFromDataTransfer, itemsFromClipboardData, ScopeNode, pushOwner, validateActionId, validateActionDefaultBinding, depNodeOf, DepNode, buildDepsFromRequires } from './chunk-Q3Y3DPKH.js';
|
|
2
|
+
import { createContext, useState, useContext, useMemo, useRef, useEffect, useCallback, useSyncExternalStore } from 'react';
|
|
3
3
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
import { pastDragThreshold } from '@weasel-js/gestures';
|
|
4
5
|
import { resolveCursorTier } from '@weasel-js/cursor';
|
|
6
|
+
import { activeModeOf } from '@weasel-js/modes';
|
|
5
7
|
|
|
8
|
+
var ToolNode = class extends ScopeNode {
|
|
9
|
+
state;
|
|
10
|
+
value = null;
|
|
11
|
+
constructor(parent, descends, owns, initialActive) {
|
|
12
|
+
super(parent, descends);
|
|
13
|
+
this.state = owns ? { active: initialActive ?? parent?.owner().state?.active ?? null, hotkeyStack: [] } : null;
|
|
14
|
+
}
|
|
15
|
+
/** The node whose tool a read here resolves to. */
|
|
16
|
+
owner() {
|
|
17
|
+
for (const n of this.start().chain()) if (n.state) return n;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
update(next) {
|
|
21
|
+
const owner = this.owner();
|
|
22
|
+
if (!owner.state) return;
|
|
23
|
+
const updated = next(owner.state);
|
|
24
|
+
if (updated === owner.state) return;
|
|
25
|
+
owner.state = updated;
|
|
26
|
+
owner.changed();
|
|
27
|
+
}
|
|
28
|
+
/** A stable value per owner and state, for `useSyncExternalStore`. */
|
|
29
|
+
snapshot() {
|
|
30
|
+
const from = this.owner();
|
|
31
|
+
const state = from.state ?? { active: null, hotkeyStack: [] };
|
|
32
|
+
const cached = this.value;
|
|
33
|
+
if (cached && cached.from === from && cached.state === state) return cached.out;
|
|
34
|
+
const out = {
|
|
35
|
+
active: state.active,
|
|
36
|
+
hotkeyStack: state.hotkeyStack,
|
|
37
|
+
setActive: (id) => this.update((s) => s.active === id ? s : { ...s, active: id }),
|
|
38
|
+
pushHotkey: (id) => this.update((s) => ({ ...s, hotkeyStack: [...s.hotkeyStack, id] })),
|
|
39
|
+
popHotkey: (id) => this.update((s) => {
|
|
40
|
+
const stack = s.hotkeyStack;
|
|
41
|
+
if (stack.length === 0) return s;
|
|
42
|
+
if (id === void 0) return { ...s, hotkeyStack: stack.slice(0, -1) };
|
|
43
|
+
const i = stack.lastIndexOf(id);
|
|
44
|
+
return i === -1 ? s : { ...s, hotkeyStack: [...stack.slice(0, i), ...stack.slice(i + 1)] };
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
this.value = { from, state, out };
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
subscribe = (listener) => {
|
|
51
|
+
this.tree.listeners.add(listener);
|
|
52
|
+
return () => {
|
|
53
|
+
this.tree.listeners.delete(listener);
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
6
57
|
var ActiveToolContext = createContext(null);
|
|
58
|
+
var ActiveToolNodeContext = ActiveToolContext;
|
|
7
59
|
function ActiveToolContextProvider({
|
|
8
60
|
children,
|
|
9
|
-
initialActive =
|
|
61
|
+
initialActive = null
|
|
10
62
|
}) {
|
|
11
|
-
const [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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]
|
|
63
|
+
const [node] = useState(() => new ToolNode(null, true, true, initialActive));
|
|
64
|
+
return /* @__PURE__ */ jsx(ActiveToolContext.Provider, { value: node, children });
|
|
65
|
+
}
|
|
66
|
+
function useNodeValue(node) {
|
|
67
|
+
const subscribe = useMemo(() => node?.subscribe ?? NO_SUBSCRIBE, [node]);
|
|
68
|
+
return useSyncExternalStore(
|
|
69
|
+
subscribe,
|
|
70
|
+
() => node?.snapshot() ?? null,
|
|
71
|
+
() => node?.snapshot() ?? null
|
|
30
72
|
);
|
|
31
|
-
return /* @__PURE__ */ jsx(ActiveToolContext.Provider, { value, children });
|
|
32
73
|
}
|
|
74
|
+
var NO_SUBSCRIBE = () => () => {
|
|
75
|
+
};
|
|
33
76
|
function useActiveToolContext() {
|
|
34
|
-
const value = useContext(ActiveToolContext);
|
|
77
|
+
const value = useNodeValue(useContext(ActiveToolContext));
|
|
35
78
|
if (value === null) {
|
|
36
79
|
throw new Error(
|
|
37
80
|
"useActiveToolContext: no ActiveToolContextProvider in scope. Wrap your tree with <ActiveToolContextProvider> (typically inside <SceneCanvas>)."
|
|
@@ -40,7 +83,7 @@ function useActiveToolContext() {
|
|
|
40
83
|
return value;
|
|
41
84
|
}
|
|
42
85
|
function useOptionalActiveToolContext() {
|
|
43
|
-
return useContext(ActiveToolContext);
|
|
86
|
+
return useNodeValue(useContext(ActiveToolContext));
|
|
44
87
|
}
|
|
45
88
|
function ActiveToolContextProviderIfRoot({
|
|
46
89
|
children
|
|
@@ -51,28 +94,7 @@ function ActiveToolContextProviderIfRoot({
|
|
|
51
94
|
}
|
|
52
95
|
var DepRegistryContext = createContext(null);
|
|
53
96
|
function DepRegistryProvider({ children }) {
|
|
54
|
-
const
|
|
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
|
-
}), []);
|
|
97
|
+
const registry = useMemo(createDepRegistry, []);
|
|
76
98
|
return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry, children });
|
|
77
99
|
}
|
|
78
100
|
function useDepRegistry() {
|
|
@@ -134,7 +156,7 @@ function computeMultiTouchGeometry(positions) {
|
|
|
134
156
|
return { centroid, spread };
|
|
135
157
|
}
|
|
136
158
|
function useGestureDispatcher(opts) {
|
|
137
|
-
const { canvasRef, actions,
|
|
159
|
+
const { canvasRef, actions, entriesById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld, requestRedraw, paintedCursor, getRuleCtx, onDoubleClick, views, channels } = opts;
|
|
138
160
|
const pointerChannel = channels?.pointer !== false;
|
|
139
161
|
const wheelChannel = channels?.wheel !== false;
|
|
140
162
|
const pinchChannel = channels?.pinch !== false;
|
|
@@ -148,12 +170,14 @@ function useGestureDispatcher(opts) {
|
|
|
148
170
|
if (!dispatcherRef.current) {
|
|
149
171
|
dispatcherRef.current = dispatcherOpt ?? createDispatcher();
|
|
150
172
|
}
|
|
173
|
+
const registryRef = useRef(actions);
|
|
174
|
+
registryRef.current = actions;
|
|
151
175
|
const ctxRef = useRef({
|
|
152
176
|
actions,
|
|
153
177
|
depRegistry,
|
|
154
178
|
activeToolId: activeTool.active,
|
|
155
179
|
hotkeyStack: activeTool.hotkeyStack,
|
|
156
|
-
|
|
180
|
+
entriesById,
|
|
157
181
|
isMac: IS_MAC
|
|
158
182
|
});
|
|
159
183
|
ctxRef.current = {
|
|
@@ -161,7 +185,7 @@ function useGestureDispatcher(opts) {
|
|
|
161
185
|
depRegistry,
|
|
162
186
|
activeToolId: activeTool.active,
|
|
163
187
|
hotkeyStack: activeTool.hotkeyStack,
|
|
164
|
-
|
|
188
|
+
entriesById,
|
|
165
189
|
isMac: IS_MAC
|
|
166
190
|
};
|
|
167
191
|
const rootTargetRef = useRef({
|
|
@@ -191,25 +215,39 @@ function useGestureDispatcher(opts) {
|
|
|
191
215
|
const paintedCursorRef = useRef(paintedCursor);
|
|
192
216
|
paintedCursorRef.current = paintedCursor;
|
|
193
217
|
const lastClickRef = useRef(null);
|
|
218
|
+
const liveToolsRef = useRef(/* @__PURE__ */ new Set());
|
|
194
219
|
const prevActiveRef = useRef(activeTool.active);
|
|
220
|
+
const entriesByIdRef = useRef(entriesById);
|
|
221
|
+
entriesByIdRef.current = entriesById;
|
|
222
|
+
const fireLifecycle = (id, hook) => {
|
|
223
|
+
const tool = entriesByIdRef.current.get(id);
|
|
224
|
+
if (!tool || !isTool(tool)) return;
|
|
225
|
+
const fn = tool[hook];
|
|
226
|
+
if (!fn) return;
|
|
227
|
+
try {
|
|
228
|
+
fn({ scratch: tool.initScratch?.() });
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.error(`weasel: tool "${id}".${hook} threw`, err);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
195
233
|
useEffect(() => {
|
|
234
|
+
const prev = liveToolsRef.current;
|
|
235
|
+
const next = new Set(
|
|
236
|
+
activeTool.active === null ? activeTool.hotkeyStack : [activeTool.active, ...activeTool.hotkeyStack]
|
|
237
|
+
);
|
|
238
|
+
liveToolsRef.current = next;
|
|
239
|
+
for (const id of prev) if (!next.has(id)) fireLifecycle(id, "onDeactivate");
|
|
196
240
|
if (prevActiveRef.current !== activeTool.active) {
|
|
197
|
-
const prevTool = toolsById.get(prevActiveRef.current);
|
|
198
|
-
if (prevTool?.onDeactivate) {
|
|
199
|
-
const scratch = prevTool.initScratch?.();
|
|
200
|
-
try {
|
|
201
|
-
prevTool.onDeactivate({ scratch });
|
|
202
|
-
} catch (err) {
|
|
203
|
-
console.error(
|
|
204
|
-
`weasel: tool "${prevActiveRef.current}".onDeactivate threw`,
|
|
205
|
-
err
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
241
|
for (const d of allDispatchers()) d.cancelAll("cancel");
|
|
210
242
|
}
|
|
211
243
|
prevActiveRef.current = activeTool.active;
|
|
244
|
+
for (const id of next) if (!prev.has(id)) fireLifecycle(id, "onActivate");
|
|
212
245
|
});
|
|
246
|
+
useEffect(() => () => {
|
|
247
|
+
const live = liveToolsRef.current;
|
|
248
|
+
liveToolsRef.current = /* @__PURE__ */ new Set();
|
|
249
|
+
for (const id of live) fireLifecycle(id, "onDeactivate");
|
|
250
|
+
}, []);
|
|
213
251
|
useEffect(() => {
|
|
214
252
|
if (!enabled) return;
|
|
215
253
|
let disposed = false;
|
|
@@ -249,7 +287,8 @@ function useGestureDispatcher(opts) {
|
|
|
249
287
|
if (before > 0 || after > 0) requestRedrawRef.current?.();
|
|
250
288
|
return result;
|
|
251
289
|
};
|
|
252
|
-
const heldKeys = /* @__PURE__ */ new
|
|
290
|
+
const heldKeys = /* @__PURE__ */ new Map();
|
|
291
|
+
const physicalKey = (e) => e.code || e.key.toLowerCase();
|
|
253
292
|
const held = /* @__PURE__ */ new Map();
|
|
254
293
|
const toCanvasLocal = (p) => {
|
|
255
294
|
const rect = canvas?.getBoundingClientRect();
|
|
@@ -273,7 +312,6 @@ function useGestureDispatcher(opts) {
|
|
|
273
312
|
};
|
|
274
313
|
let multiTouchStart = null;
|
|
275
314
|
const TAP_THRESHOLD_PX = 8;
|
|
276
|
-
const DRAG_THRESHOLD_PX = 4;
|
|
277
315
|
const LONG_PRESS_MS = 500;
|
|
278
316
|
const longPressTimers = /* @__PURE__ */ new Map();
|
|
279
317
|
const cancelLongPress = (pointerId) => {
|
|
@@ -322,8 +360,9 @@ function useGestureDispatcher(opts) {
|
|
|
322
360
|
const DOUBLE_CLICK_MAX_MS = 600;
|
|
323
361
|
const DOUBLE_CLICK_MAX_PX = 8;
|
|
324
362
|
const onKeyDown = (e) => {
|
|
325
|
-
if (isEditableTarget(e.target)) return;
|
|
326
|
-
if (
|
|
363
|
+
if (e.defaultPrevented || isEditableTarget(e.target)) return;
|
|
364
|
+
if (!registryRef.current.isActive()) return;
|
|
365
|
+
if (e.repeat && heldKeys.has(physicalKey(e))) {
|
|
327
366
|
e.preventDefault();
|
|
328
367
|
return;
|
|
329
368
|
}
|
|
@@ -348,17 +387,19 @@ function useGestureDispatcher(opts) {
|
|
|
348
387
|
};
|
|
349
388
|
const heldResult = dispatch(heldEv);
|
|
350
389
|
if (heldResult === "handled") {
|
|
351
|
-
heldKeys.
|
|
390
|
+
heldKeys.set(physicalKey(e), e.key);
|
|
352
391
|
}
|
|
353
392
|
if (keyResult === "handled" || heldResult === "handled") {
|
|
354
393
|
e.preventDefault();
|
|
355
394
|
}
|
|
356
395
|
};
|
|
357
396
|
const onKeyUp = (e) => {
|
|
358
|
-
|
|
397
|
+
const physical = physicalKey(e);
|
|
398
|
+
const downKey = heldKeys.get(physical);
|
|
399
|
+
if (downKey === void 0) return;
|
|
359
400
|
const ev = {
|
|
360
401
|
kind: "key-held",
|
|
361
|
-
key:
|
|
402
|
+
key: downKey,
|
|
362
403
|
phase: "up",
|
|
363
404
|
altKey: e.altKey,
|
|
364
405
|
ctrlKey: e.ctrlKey,
|
|
@@ -366,12 +407,12 @@ function useGestureDispatcher(opts) {
|
|
|
366
407
|
shiftKey: e.shiftKey
|
|
367
408
|
};
|
|
368
409
|
dispatch(ev);
|
|
369
|
-
heldKeys.delete(
|
|
410
|
+
heldKeys.delete(physical);
|
|
370
411
|
};
|
|
371
412
|
const onWindowBlur = () => {
|
|
372
413
|
endPinch();
|
|
373
414
|
for (const p of [...held.values()]) p.session.cancel();
|
|
374
|
-
for (const key of heldKeys) {
|
|
415
|
+
for (const key of heldKeys.values()) {
|
|
375
416
|
const ev = {
|
|
376
417
|
kind: "key-held",
|
|
377
418
|
key,
|
|
@@ -386,6 +427,7 @@ function useGestureDispatcher(opts) {
|
|
|
386
427
|
heldKeys.clear();
|
|
387
428
|
};
|
|
388
429
|
const onWheel = (e) => {
|
|
430
|
+
registryRef.current.activate();
|
|
389
431
|
if (pinchLive && e.ctrlKey) {
|
|
390
432
|
e.preventDefault();
|
|
391
433
|
e.stopPropagation();
|
|
@@ -469,6 +511,7 @@ function useGestureDispatcher(opts) {
|
|
|
469
511
|
endPinch();
|
|
470
512
|
};
|
|
471
513
|
const onPointerDown = (e) => {
|
|
514
|
+
registryRef.current.activate();
|
|
472
515
|
if ((e.button ?? 0) !== 0) return;
|
|
473
516
|
if (!canvas) return;
|
|
474
517
|
routeDown(e.pointerId, e.clientX, e.clientY);
|
|
@@ -561,9 +604,7 @@ function useGestureDispatcher(opts) {
|
|
|
561
604
|
if (rec) rec.pos = { x: e.clientX, y: e.clientY };
|
|
562
605
|
const buffered = rec?.buffered;
|
|
563
606
|
if (rec && buffered) {
|
|
564
|
-
|
|
565
|
-
const dy = e.clientY - buffered.clientY;
|
|
566
|
-
if (Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
|
|
607
|
+
if (pastDragThreshold(buffered, e)) {
|
|
567
608
|
rec.buffered = null;
|
|
568
609
|
cancelLongPress(e.pointerId);
|
|
569
610
|
dispatch(buffered.ev);
|
|
@@ -671,10 +712,17 @@ function useGestureDispatcher(opts) {
|
|
|
671
712
|
...bodyKind !== void 0 ? { bodyKind } : {}
|
|
672
713
|
};
|
|
673
714
|
const ctx = ctxNow();
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
715
|
+
const press = {
|
|
716
|
+
kind: "pointerdown",
|
|
717
|
+
x: w.x,
|
|
718
|
+
y: w.y,
|
|
719
|
+
clientX: h.clientX,
|
|
720
|
+
clientY: h.clientY,
|
|
721
|
+
...where
|
|
722
|
+
};
|
|
723
|
+
const d = dispatcherNow();
|
|
724
|
+
publishLiveDispatch(() => d.explain(press, ctx));
|
|
725
|
+
const dragCursor = d.resolveOnly(press, ctx)?.action.cursor;
|
|
678
726
|
const cursor = dragCursor ?? dispatcherNow().resolveOnly(
|
|
679
727
|
{ kind: "click", x: w.x, y: w.y, pressX: w.x, pressY: w.y, ...where },
|
|
680
728
|
ctx
|
|
@@ -700,6 +748,7 @@ function useGestureDispatcher(opts) {
|
|
|
700
748
|
};
|
|
701
749
|
const onHoverPointerLeave = () => {
|
|
702
750
|
lastHover = null;
|
|
751
|
+
publishLiveDispatch(null);
|
|
703
752
|
paintedCursorRef.current?.()?.clearPointer();
|
|
704
753
|
clearHoverCursor();
|
|
705
754
|
};
|
|
@@ -912,9 +961,10 @@ function useGestureDispatcher(opts) {
|
|
|
912
961
|
});
|
|
913
962
|
if (result === "handled") e.preventDefault();
|
|
914
963
|
};
|
|
964
|
+
const keyCapture = keyboard === "first";
|
|
915
965
|
if (keyboard) {
|
|
916
|
-
window.addEventListener("keydown", onKeyDown);
|
|
917
|
-
window.addEventListener("keyup", onKeyUp);
|
|
966
|
+
window.addEventListener("keydown", onKeyDown, keyCapture);
|
|
967
|
+
window.addEventListener("keyup", onKeyUp, keyCapture);
|
|
918
968
|
}
|
|
919
969
|
window.addEventListener("blur", onWindowBlur);
|
|
920
970
|
window.addEventListener("keydown", scheduleHoverCursorRefresh);
|
|
@@ -940,8 +990,8 @@ function useGestureDispatcher(opts) {
|
|
|
940
990
|
}
|
|
941
991
|
return () => {
|
|
942
992
|
if (keyboard) {
|
|
943
|
-
window.removeEventListener("keydown", onKeyDown);
|
|
944
|
-
window.removeEventListener("keyup", onKeyUp);
|
|
993
|
+
window.removeEventListener("keydown", onKeyDown, keyCapture);
|
|
994
|
+
window.removeEventListener("keyup", onKeyUp, keyCapture);
|
|
945
995
|
}
|
|
946
996
|
window.removeEventListener("blur", onWindowBlur);
|
|
947
997
|
window.removeEventListener("keydown", scheduleHoverCursorRefresh);
|
|
@@ -970,203 +1020,152 @@ function useGestureDispatcher(opts) {
|
|
|
970
1020
|
}, [enabled, keyboard, canvasRef, pointerChannel, wheelChannel, pinchChannel, contextMenuChannel, ingestChannel]);
|
|
971
1021
|
}
|
|
972
1022
|
var ActionsContext = createContext(null);
|
|
973
|
-
function
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1023
|
+
function depsFor(a, r) {
|
|
1024
|
+
if (!r) return {};
|
|
1025
|
+
if (a.requires) return buildDepsFromRequires(a, r);
|
|
1026
|
+
return {
|
|
1027
|
+
selection: r.get("selection"),
|
|
1028
|
+
scene: r.get("scene"),
|
|
1029
|
+
history: r.get("history"),
|
|
1030
|
+
view: r.get("view"),
|
|
1031
|
+
activeTool: r.get("activeTool"),
|
|
1032
|
+
booleansAdapter: r.get("booleansAdapter")
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
var ActionsNode = class extends ScopeNode {
|
|
1036
|
+
constructor(parent, descends, contextDeps) {
|
|
1037
|
+
super(parent, descends);
|
|
1038
|
+
this.contextDeps = contextDeps;
|
|
1039
|
+
NODES.set(this.registry, this);
|
|
1040
|
+
}
|
|
1041
|
+
contextDeps;
|
|
1042
|
+
actions = /* @__PURE__ */ new Map();
|
|
1043
|
+
muted = /* @__PURE__ */ new Map();
|
|
1044
|
+
dispatchers = [];
|
|
1045
|
+
cache = null;
|
|
1046
|
+
/** Called on `activate`, so a scope's dep and tool nodes move with it. */
|
|
1047
|
+
onActivate = [];
|
|
1048
|
+
resolve(id) {
|
|
1049
|
+
for (const n of this.chain()) {
|
|
1050
|
+
if (n.muted.has(id)) return void 0;
|
|
1051
|
+
const a = n.actions.get(id)?.at(-1);
|
|
1052
|
+
if (a) return a;
|
|
1053
|
+
}
|
|
1054
|
+
return void 0;
|
|
1055
|
+
}
|
|
1056
|
+
snapshot() {
|
|
1057
|
+
const leaf = this.start();
|
|
1058
|
+
const cached = this.cache;
|
|
1059
|
+
if (cached && cached.version === this.tree.version && cached.leaf === leaf) return cached.list;
|
|
1060
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1061
|
+
for (const n of leaf.chain()) for (const id of n.actions.keys()) ids.add(id);
|
|
1062
|
+
const out = [];
|
|
1063
|
+
for (const id of ids) {
|
|
1064
|
+
const a = leaf.resolve(id);
|
|
1065
|
+
if (a) out.push(a);
|
|
1066
|
+
}
|
|
1067
|
+
const list = Object.freeze(out);
|
|
1068
|
+
this.cache = { version: this.tree.version, leaf, list };
|
|
1069
|
+
return list;
|
|
1070
|
+
}
|
|
1071
|
+
deps() {
|
|
1072
|
+
for (const n of this.chain()) {
|
|
1073
|
+
const r = n.contextDeps();
|
|
1074
|
+
if (r) return r;
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
dispatcher() {
|
|
1079
|
+
for (const n of this.chain()) {
|
|
1080
|
+
const d = n.dispatchers.at(-1);
|
|
1081
|
+
if (d) return d;
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
activate() {
|
|
1086
|
+
super.activate();
|
|
1087
|
+
for (const f of this.onActivate) f();
|
|
1088
|
+
}
|
|
1089
|
+
registry = {
|
|
1090
|
+
register: (action) => {
|
|
1091
|
+
validateActionId(action.id);
|
|
1092
|
+
validateActionDefaultBinding(action);
|
|
1093
|
+
const stack = this.actions.get(action.id);
|
|
1094
|
+
if (stack) stack.push(action);
|
|
1095
|
+
else this.actions.set(action.id, [action]);
|
|
1096
|
+
this.changed();
|
|
1097
|
+
let released = false;
|
|
1098
|
+
return () => {
|
|
1099
|
+
if (released) return;
|
|
1100
|
+
released = true;
|
|
1101
|
+
const cur = this.actions.get(action.id);
|
|
1102
|
+
if (!cur) return;
|
|
1103
|
+
const i = cur.lastIndexOf(action);
|
|
1104
|
+
if (i === -1) return;
|
|
1105
|
+
cur.splice(i, 1);
|
|
1106
|
+
if (cur.length === 0) this.actions.delete(action.id);
|
|
1107
|
+
this.changed();
|
|
1108
|
+
};
|
|
1109
|
+
},
|
|
1110
|
+
unregister: (id) => {
|
|
1111
|
+
if (this.actions.delete(id)) this.changed();
|
|
1112
|
+
},
|
|
1113
|
+
mute: (id) => {
|
|
1114
|
+
this.muted.set(id, (this.muted.get(id) ?? 0) + 1);
|
|
1115
|
+
this.changed();
|
|
1116
|
+
let released = false;
|
|
1117
|
+
return () => {
|
|
1118
|
+
if (released) return;
|
|
1119
|
+
released = true;
|
|
1120
|
+
const held = this.muted.get(id);
|
|
1121
|
+
if (held === void 0) return;
|
|
1122
|
+
if (held > 1) this.muted.set(id, held - 1);
|
|
1123
|
+
else this.muted.delete(id);
|
|
1124
|
+
this.changed();
|
|
1125
|
+
};
|
|
1126
|
+
},
|
|
1127
|
+
list: () => this.snapshot(),
|
|
1128
|
+
trigger: (id, params) => {
|
|
1129
|
+
const leaf = this.start();
|
|
1130
|
+
const a = leaf.resolve(id);
|
|
1131
|
+
if (!a) return false;
|
|
1132
|
+
try {
|
|
1133
|
+
if (a.invoker && a.invoker.timing === "immediate") {
|
|
1134
|
+
a.invoker.run(depsFor(a, leaf.deps()), params);
|
|
986
1135
|
}
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
|
|
987
1138
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
offBase();
|
|
1007
|
-
listenersRef.current.delete(listener);
|
|
1008
|
-
};
|
|
1009
|
-
},
|
|
1010
|
-
mute: (id) => {
|
|
1011
|
-
mutedRef.current.set(id, (mutedRef.current.get(id) ?? 0) + 1);
|
|
1012
|
-
cacheRef.current = null;
|
|
1013
|
-
notify();
|
|
1014
|
-
let released = false;
|
|
1015
|
-
return () => {
|
|
1016
|
-
if (released) return;
|
|
1017
|
-
released = true;
|
|
1018
|
-
const held = mutedRef.current.get(id);
|
|
1019
|
-
if (held === void 0) return;
|
|
1020
|
-
if (held > 1) mutedRef.current.set(id, held - 1);
|
|
1021
|
-
else mutedRef.current.delete(id);
|
|
1022
|
-
cacheRef.current = null;
|
|
1023
|
-
notify();
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
};
|
|
1027
|
-
}, [base]);
|
|
1028
|
-
}
|
|
1029
|
-
function ActionsScope({ children }) {
|
|
1030
|
-
const parent = useActionsRegistry();
|
|
1031
|
-
const scoped = useMuted(parent);
|
|
1032
|
-
if (!scoped) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
1033
|
-
return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: scoped, children });
|
|
1034
|
-
}
|
|
1035
|
-
function pushOwner(ref, value) {
|
|
1036
|
-
if (value === null) return () => {
|
|
1037
|
-
};
|
|
1038
|
-
ref.current.push(value);
|
|
1039
|
-
let released = false;
|
|
1040
|
-
return () => {
|
|
1041
|
-
if (released) return;
|
|
1042
|
-
released = true;
|
|
1043
|
-
const i = ref.current.lastIndexOf(value);
|
|
1044
|
-
if (i !== -1) ref.current.splice(i, 1);
|
|
1139
|
+
return true;
|
|
1140
|
+
},
|
|
1141
|
+
subscribe: (listener) => {
|
|
1142
|
+
this.tree.listeners.add(listener);
|
|
1143
|
+
return () => {
|
|
1144
|
+
this.tree.listeners.delete(listener);
|
|
1145
|
+
};
|
|
1146
|
+
},
|
|
1147
|
+
setDispatcher: (d) => pushOwner(this.dispatchers, d),
|
|
1148
|
+
begin: (id, params) => {
|
|
1149
|
+
const leaf = this.start();
|
|
1150
|
+
const a = leaf.resolve(id);
|
|
1151
|
+
const disp = leaf.dispatcher();
|
|
1152
|
+
if (!a || !disp) return null;
|
|
1153
|
+
return disp.beginUiOngoing(id, depsFor(a, leaf.deps()), params);
|
|
1154
|
+
},
|
|
1155
|
+
activate: () => this.activate(),
|
|
1156
|
+
isActive: () => this.isActive()
|
|
1045
1157
|
};
|
|
1158
|
+
};
|
|
1159
|
+
var NODES = /* @__PURE__ */ new WeakMap();
|
|
1160
|
+
function actionsNodeOf(registry) {
|
|
1161
|
+
return registry ? NODES.get(registry) ?? null : null;
|
|
1046
1162
|
}
|
|
1047
1163
|
function ActionsProvider({ children }) {
|
|
1048
|
-
const actionsRef = useRef(/* @__PURE__ */ new Map());
|
|
1049
|
-
const liveAction = (id) => actionsRef.current.get(id)?.at(-1);
|
|
1050
|
-
const versionRef = useRef(0);
|
|
1051
|
-
const cachedRef = useRef([]);
|
|
1052
|
-
const cachedVerRef = useRef(-1);
|
|
1053
|
-
const listenersRef = useRef(/* @__PURE__ */ new Set());
|
|
1054
1164
|
const depReg = useOptionalDepRegistry();
|
|
1055
1165
|
const depRegRef = useRef(depReg);
|
|
1056
1166
|
depRegRef.current = depReg;
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
const dispatcherRef = useRef([]);
|
|
1060
|
-
const store = useMemo(() => {
|
|
1061
|
-
const snapshot = () => {
|
|
1062
|
-
const v = versionRef.current;
|
|
1063
|
-
if (cachedVerRef.current === v) return cachedRef.current;
|
|
1064
|
-
const out = Object.freeze(
|
|
1065
|
-
Array.from(actionsRef.current.values(), (stack) => stack[stack.length - 1])
|
|
1066
|
-
);
|
|
1067
|
-
cachedRef.current = out;
|
|
1068
|
-
cachedVerRef.current = v;
|
|
1069
|
-
return out;
|
|
1070
|
-
};
|
|
1071
|
-
const notify = () => {
|
|
1072
|
-
for (const l of listenersRef.current) {
|
|
1073
|
-
try {
|
|
1074
|
-
l();
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
console.error("weasel ActionsRegistry: subscriber threw", err);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
};
|
|
1080
|
-
return {
|
|
1081
|
-
register: (action) => {
|
|
1082
|
-
validateActionId(action.id);
|
|
1083
|
-
validateActionDefaultBinding(action);
|
|
1084
|
-
const stack = actionsRef.current.get(action.id);
|
|
1085
|
-
if (stack) stack.push(action);
|
|
1086
|
-
else actionsRef.current.set(action.id, [action]);
|
|
1087
|
-
versionRef.current++;
|
|
1088
|
-
notify();
|
|
1089
|
-
let released = false;
|
|
1090
|
-
return () => {
|
|
1091
|
-
if (released) return;
|
|
1092
|
-
released = true;
|
|
1093
|
-
const cur = actionsRef.current.get(action.id);
|
|
1094
|
-
if (!cur) return;
|
|
1095
|
-
const i = cur.lastIndexOf(action);
|
|
1096
|
-
if (i === -1) return;
|
|
1097
|
-
cur.splice(i, 1);
|
|
1098
|
-
if (cur.length === 0) actionsRef.current.delete(action.id);
|
|
1099
|
-
versionRef.current++;
|
|
1100
|
-
notify();
|
|
1101
|
-
};
|
|
1102
|
-
},
|
|
1103
|
-
unregister: (id) => {
|
|
1104
|
-
if (actionsRef.current.delete(id)) {
|
|
1105
|
-
versionRef.current++;
|
|
1106
|
-
notify();
|
|
1107
|
-
}
|
|
1108
|
-
},
|
|
1109
|
-
list: () => snapshot(),
|
|
1110
|
-
trigger: (id, params) => {
|
|
1111
|
-
const a = liveAction(id);
|
|
1112
|
-
if (!a) return false;
|
|
1113
|
-
try {
|
|
1114
|
-
if (a.invoker && a.invoker.timing === "immediate") {
|
|
1115
|
-
const r = wiredDepRegRef.current.at(-1) ?? depRegRef.current;
|
|
1116
|
-
const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
|
|
1117
|
-
selection: r.get("selection"),
|
|
1118
|
-
scene: r.get("scene"),
|
|
1119
|
-
history: r.get("history"),
|
|
1120
|
-
view: r.get("view"),
|
|
1121
|
-
activeTool: r.get("activeTool"),
|
|
1122
|
-
booleansAdapter: r.get("booleansAdapter")
|
|
1123
|
-
};
|
|
1124
|
-
a.invoker.run(deps, params);
|
|
1125
|
-
}
|
|
1126
|
-
} catch (err) {
|
|
1127
|
-
console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
|
|
1128
|
-
}
|
|
1129
|
-
return true;
|
|
1130
|
-
},
|
|
1131
|
-
subscribe: (listener) => {
|
|
1132
|
-
listenersRef.current.add(listener);
|
|
1133
|
-
return () => {
|
|
1134
|
-
listenersRef.current.delete(listener);
|
|
1135
|
-
};
|
|
1136
|
-
},
|
|
1137
|
-
setDispatcher: (d) => {
|
|
1138
|
-
if (d && dispatcherRef.current.length > 0 && dispatcherRef.current.at(-1) !== d && !warnedRef.current) {
|
|
1139
|
-
warnedRef.current = true;
|
|
1140
|
-
warnSharedScope();
|
|
1141
|
-
}
|
|
1142
|
-
return pushOwner(dispatcherRef, d);
|
|
1143
|
-
},
|
|
1144
|
-
setDepRegistry: (r) => pushOwner(wiredDepRegRef, r),
|
|
1145
|
-
begin: (id, params) => {
|
|
1146
|
-
const disp = dispatcherRef.current.at(-1) ?? null;
|
|
1147
|
-
if (!disp) return null;
|
|
1148
|
-
const r = wiredDepRegRef.current.at(-1) ?? depRegRef.current;
|
|
1149
|
-
const a = liveAction(id);
|
|
1150
|
-
const deps = !r ? {} : a?.requires ? buildDepsFromRequires(a, r) : {
|
|
1151
|
-
selection: r.get("selection"),
|
|
1152
|
-
scene: r.get("scene"),
|
|
1153
|
-
history: r.get("history"),
|
|
1154
|
-
view: r.get("view"),
|
|
1155
|
-
activeTool: r.get("activeTool"),
|
|
1156
|
-
booleansAdapter: r.get("booleansAdapter")
|
|
1157
|
-
};
|
|
1158
|
-
return disp.beginUiOngoing(id, deps, params);
|
|
1159
|
-
}
|
|
1160
|
-
};
|
|
1161
|
-
}, []);
|
|
1162
|
-
const registry = useMuted(store);
|
|
1163
|
-
return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry, children });
|
|
1164
|
-
}
|
|
1165
|
-
function warnSharedScope() {
|
|
1166
|
-
if (!IS_DEV) return;
|
|
1167
|
-
console.warn(
|
|
1168
|
-
"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>."
|
|
1169
|
-
);
|
|
1167
|
+
const node = useMemo(() => new ActionsNode(null, true, () => depRegRef.current), []);
|
|
1168
|
+
return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: node.registry, children });
|
|
1170
1169
|
}
|
|
1171
1170
|
var IS_DEV = (() => {
|
|
1172
1171
|
try {
|
|
@@ -1192,6 +1191,41 @@ function useAction(action) {
|
|
|
1192
1191
|
return reg.register(action);
|
|
1193
1192
|
}, [reg, action]);
|
|
1194
1193
|
}
|
|
1194
|
+
function useNodes(descends, ownsTool, parent) {
|
|
1195
|
+
const parentActions = useActionsRegistry();
|
|
1196
|
+
const parentDeps = useOptionalDepRegistry();
|
|
1197
|
+
const parentTools = useContext(ActiveToolNodeContext);
|
|
1198
|
+
const pa = parent ? parent.actions : actionsNodeOf(parentActions);
|
|
1199
|
+
const pd = parent ? parent.deps : depNodeOf(parentDeps);
|
|
1200
|
+
const pt = parent ? parent.tools : parentTools;
|
|
1201
|
+
const nodes = useMemo(() => {
|
|
1202
|
+
const deps = new DepNode(pd, descends);
|
|
1203
|
+
const tools = new ToolNode(pt, descends, ownsTool, null);
|
|
1204
|
+
const actions = new ActionsNode(pa, descends, () => deps.registry);
|
|
1205
|
+
actions.onActivate.push(() => deps.activate(), () => tools.activate());
|
|
1206
|
+
return { actions, deps, tools };
|
|
1207
|
+
}, [pa, pd, pt, descends, ownsTool]);
|
|
1208
|
+
useEffect(() => {
|
|
1209
|
+
const leave = [nodes.deps.mount(), nodes.tools.mount(), nodes.actions.mount()];
|
|
1210
|
+
return () => {
|
|
1211
|
+
for (const f of leave) f();
|
|
1212
|
+
};
|
|
1213
|
+
}, [nodes]);
|
|
1214
|
+
return nodes;
|
|
1215
|
+
}
|
|
1216
|
+
function Provide({ nodes, children }) {
|
|
1217
|
+
return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: nodes.deps.registry, children: /* @__PURE__ */ jsx(ActionsContext.Provider, { value: nodes.actions.registry, children: /* @__PURE__ */ jsx(ActiveToolNodeContext.Provider, { value: nodes.tools, children }) }) });
|
|
1218
|
+
}
|
|
1219
|
+
function InputScope({ yoke, children }) {
|
|
1220
|
+
const nodes = useNodes(false, yoke === void 0, yoke);
|
|
1221
|
+
return /* @__PURE__ */ jsx(Provide, { nodes, children });
|
|
1222
|
+
}
|
|
1223
|
+
function useYoke() {
|
|
1224
|
+
return useNodes(true, true, void 0);
|
|
1225
|
+
}
|
|
1226
|
+
function Yoke({ value, children }) {
|
|
1227
|
+
return /* @__PURE__ */ jsx(Provide, { nodes: value, children });
|
|
1228
|
+
}
|
|
1195
1229
|
function useOngoingAction(actionId) {
|
|
1196
1230
|
const reg = useActionsRegistry();
|
|
1197
1231
|
const regRef = useRef(reg);
|
|
@@ -1233,14 +1267,14 @@ function useContributions(opts) {
|
|
|
1233
1267
|
const needsFirstMountSyncRef = useRef(false);
|
|
1234
1268
|
if (isFirstRender) {
|
|
1235
1269
|
hasInitializedRef.current = true;
|
|
1236
|
-
needsFirstMountSyncRef.current = ctx.active ===
|
|
1270
|
+
needsFirstMountSyncRef.current = ctx.active === null && opts.focused !== null;
|
|
1237
1271
|
}
|
|
1238
1272
|
useEffect(() => {
|
|
1239
1273
|
if (!needsFirstMountSyncRef.current) return;
|
|
1240
1274
|
needsFirstMountSyncRef.current = false;
|
|
1241
1275
|
ctx.setActive(opts.focused);
|
|
1242
1276
|
}, []);
|
|
1243
|
-
const focused = isFirstRender && ctx.active ===
|
|
1277
|
+
const focused = isFirstRender && ctx.active === null && opts.focused !== null ? opts.focused : ctx.active;
|
|
1244
1278
|
const entriesRef = useRef(opts.entries);
|
|
1245
1279
|
entriesRef.current = opts.entries;
|
|
1246
1280
|
const focusedRef = useRef(focused);
|
|
@@ -1286,21 +1320,15 @@ function useContributions(opts) {
|
|
|
1286
1320
|
}
|
|
1287
1321
|
return [...active, ...hotkey, ...ambient];
|
|
1288
1322
|
}, [snapshot]);
|
|
1289
|
-
|
|
1290
|
-
useEffect(() => {
|
|
1291
|
-
if (!actionsRegistry || offhandSig === "") return;
|
|
1292
|
-
const specs = offhandSig.split(",").map((entry) => {
|
|
1293
|
-
const [toolId, trigger] = entry.split(":");
|
|
1294
|
-
return { toolId, key: offhandKeyFor(trigger) };
|
|
1295
|
-
});
|
|
1296
|
-
return actionsRegistry.register(makeToolOffhandAction(buildToolOffhandBindings(specs)));
|
|
1297
|
-
}, [actionsRegistry, offhandSig]);
|
|
1323
|
+
useOffhandAction(opts.entries);
|
|
1298
1324
|
const lastConflictSigRef = useRef(null);
|
|
1299
1325
|
const entrySig = opts.entries.map((e) => e.id).join(",");
|
|
1326
|
+
const modesRegistry = opts.modes;
|
|
1300
1327
|
useEffect(() => {
|
|
1301
1328
|
if (!isDev()) return;
|
|
1302
|
-
|
|
1303
|
-
|
|
1329
|
+
const last = lastConflictSigRef.current;
|
|
1330
|
+
if (last?.sig === entrySig && last.modes === modesRegistry) return;
|
|
1331
|
+
lastConflictSigRef.current = { sig: entrySig, modes: modesRegistry };
|
|
1304
1332
|
const registry = [];
|
|
1305
1333
|
const ambient = [];
|
|
1306
1334
|
for (const entry of entriesRef.current) {
|
|
@@ -1310,9 +1338,10 @@ function useContributions(opts) {
|
|
|
1310
1338
|
reportRouteConflicts({
|
|
1311
1339
|
registry,
|
|
1312
1340
|
ambient,
|
|
1313
|
-
actions: actionsRegistry?.list() ?? []
|
|
1341
|
+
actions: actionsRegistry?.list() ?? [],
|
|
1342
|
+
...modesRegistry ? { modes: modesRegistry.list().map(activeModeOf) } : {}
|
|
1314
1343
|
});
|
|
1315
|
-
}, [entrySig, actionsRegistry]);
|
|
1344
|
+
}, [entrySig, actionsRegistry, modesRegistry]);
|
|
1316
1345
|
return useMemo(
|
|
1317
1346
|
() => ({
|
|
1318
1347
|
get entries() {
|
|
@@ -1326,6 +1355,16 @@ function useContributions(opts) {
|
|
|
1326
1355
|
[focused, setFocused, scopedBindings, overlays]
|
|
1327
1356
|
);
|
|
1328
1357
|
}
|
|
1358
|
+
function useOffhandAction(entries) {
|
|
1359
|
+
const actionsRegistry = useActionsRegistry();
|
|
1360
|
+
const offhandSig = JSON.stringify(entries.filter((e) => e.eligibility?.offhand).map((e) => [e.id, e.eligibility.offhand]));
|
|
1361
|
+
useEffect(() => {
|
|
1362
|
+
const pairs = JSON.parse(offhandSig);
|
|
1363
|
+
if (!actionsRegistry || pairs.length === 0) return;
|
|
1364
|
+
const specs = pairs.map(([toolId, trigger]) => ({ toolId, key: offhandKeyFor(trigger) }));
|
|
1365
|
+
return actionsRegistry.register(makeToolOffhandAction(buildToolOffhandBindings(specs)));
|
|
1366
|
+
}, [actionsRegistry, offhandSig]);
|
|
1367
|
+
}
|
|
1329
1368
|
|
|
1330
1369
|
// src/tools/useTools.ts
|
|
1331
1370
|
function declareSlot(tool, slot) {
|
|
@@ -1337,6 +1376,24 @@ function declareSlot(tool, slot) {
|
|
|
1337
1376
|
};
|
|
1338
1377
|
return sameEligibility(tool.eligibility, eligibility) ? tool : { ...tool, eligibility };
|
|
1339
1378
|
}
|
|
1379
|
+
var NO_AMBIENT = [];
|
|
1380
|
+
function useStableSources(registry, ambient) {
|
|
1381
|
+
const next = { registry, ambient: ambient ?? NO_AMBIENT };
|
|
1382
|
+
const ref = useRef(next);
|
|
1383
|
+
const prev = ref.current;
|
|
1384
|
+
if (prev !== next && !(sameRecord(prev.registry, next.registry) && sameList(prev.ambient, next.ambient))) {
|
|
1385
|
+
ref.current = next;
|
|
1386
|
+
}
|
|
1387
|
+
return ref.current;
|
|
1388
|
+
}
|
|
1389
|
+
function sameRecord(a, b) {
|
|
1390
|
+
const ak = Object.keys(a);
|
|
1391
|
+
if (ak.length !== Object.keys(b).length) return false;
|
|
1392
|
+
return ak.every((k) => k in b && a[k] === b[k]);
|
|
1393
|
+
}
|
|
1394
|
+
function sameList(a, b) {
|
|
1395
|
+
return a.length === b.length && a.every((t, i) => t === b[i]);
|
|
1396
|
+
}
|
|
1340
1397
|
function sameEligibility(a, b) {
|
|
1341
1398
|
if (!a) return false;
|
|
1342
1399
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
@@ -1344,16 +1401,18 @@ function sameEligibility(a, b) {
|
|
|
1344
1401
|
return true;
|
|
1345
1402
|
}
|
|
1346
1403
|
function useTools(opts) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1404
|
+
const initialActive = opts.active ?? null;
|
|
1405
|
+
if (initialActive !== null && !(initialActive in opts.registry)) {
|
|
1406
|
+
throw new Error(`useTools: active "${initialActive}" not in registry`);
|
|
1349
1407
|
}
|
|
1350
1408
|
const ctx = useActiveToolContext();
|
|
1409
|
+
const sources = useStableSources(opts.registry, opts.ambient);
|
|
1351
1410
|
const slotted = useMemo(() => {
|
|
1352
1411
|
const registry = {};
|
|
1353
|
-
for (const [id, tool] of Object.entries(
|
|
1412
|
+
for (const [id, tool] of Object.entries(sources.registry)) {
|
|
1354
1413
|
registry[id] = declareSlot(tool, "focus");
|
|
1355
1414
|
}
|
|
1356
|
-
const ambient =
|
|
1415
|
+
const ambient = sources.ambient.map((t) => declareSlot(t, "always"));
|
|
1357
1416
|
const byId = /* @__PURE__ */ new Map();
|
|
1358
1417
|
for (const tool of [...Object.values(registry), ...ambient]) {
|
|
1359
1418
|
const prior = byId.get(tool.id);
|
|
@@ -1361,8 +1420,12 @@ function useTools(opts) {
|
|
|
1361
1420
|
else byId.set(tool.id, tool);
|
|
1362
1421
|
}
|
|
1363
1422
|
return { registry, ambient, entries: [...byId.values()] };
|
|
1364
|
-
}, [
|
|
1365
|
-
const contributions = useContributions({
|
|
1423
|
+
}, [sources]);
|
|
1424
|
+
const contributions = useContributions({
|
|
1425
|
+
entries: slotted.entries,
|
|
1426
|
+
focused: initialActive,
|
|
1427
|
+
...opts.modes ? { modes: opts.modes } : {}
|
|
1428
|
+
});
|
|
1366
1429
|
const hotkeyEngaged = ctx.hotkeyStack.at(-1) ?? null;
|
|
1367
1430
|
const slottedRef = useRef(slotted);
|
|
1368
1431
|
slottedRef.current = slotted;
|
|
@@ -1373,7 +1436,7 @@ function useTools(opts) {
|
|
|
1373
1436
|
const setFocused = contributions.setFocused;
|
|
1374
1437
|
const setActive = useCallback(
|
|
1375
1438
|
(id) => {
|
|
1376
|
-
if (!(id in slottedRef.current.registry)) {
|
|
1439
|
+
if (id !== null && !(id in slottedRef.current.registry)) {
|
|
1377
1440
|
throw new Error(`setActive: "${id}" not in registry`);
|
|
1378
1441
|
}
|
|
1379
1442
|
dlog("tools", "active:", activeRef.current, "\u2192", id);
|
|
@@ -1401,7 +1464,6 @@ function useTools(opts) {
|
|
|
1401
1464
|
);
|
|
1402
1465
|
const active = contributions.focused;
|
|
1403
1466
|
const getActiveOverlays = contributions.overlays;
|
|
1404
|
-
const registryKey = Object.keys(slotted.registry).sort().join("|") + "#" + slotted.ambient.map((t) => t.id).join("|");
|
|
1405
1467
|
return useMemo(
|
|
1406
1468
|
() => ({
|
|
1407
1469
|
active,
|
|
@@ -1409,18 +1471,15 @@ function useTools(opts) {
|
|
|
1409
1471
|
hotkeyEngaged,
|
|
1410
1472
|
engageHotkey,
|
|
1411
1473
|
disengageHotkey,
|
|
1412
|
-
ambient:
|
|
1413
|
-
registry:
|
|
1474
|
+
ambient: slotted.ambient,
|
|
1475
|
+
registry: slotted.registry,
|
|
1414
1476
|
has,
|
|
1415
1477
|
getActiveOverlays
|
|
1416
1478
|
}),
|
|
1417
|
-
|
|
1418
|
-
// `slottedRef.current`, and the key is what tells us that ref's contents moved.
|
|
1419
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1420
|
-
[active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays, registryKey]
|
|
1479
|
+
[active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays, slotted]
|
|
1421
1480
|
);
|
|
1422
1481
|
}
|
|
1423
1482
|
|
|
1424
|
-
export { ActionsProvider,
|
|
1483
|
+
export { ActionsProvider, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, InputScope, Yoke, useAction, useActionsRegistry, useActiveToolContext, useContributions, useDepRegistry, useDepSource, useGestureDispatcher, useOffhandAction, useOngoingAction, useOptionalActiveToolContext, useOptionalDepRegistry, useTools, useYoke };
|
|
1425
1484
|
//# sourceMappingURL=react.js.map
|
|
1426
1485
|
//# sourceMappingURL=react.js.map
|