@weasel-js/routing 1.6.0 → 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-D2TZLDP5.js → chunk-Q3Y3DPKH.js} +770 -370
- package/dist/chunk-Q3Y3DPKH.js.map +1 -0
- package/dist/{index-B_KvoXww.d.ts → index-Bmq4lSHY.d.ts} +601 -247
- package/dist/index.d.ts +2 -3
- package/dist/index.js +94 -8
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +220 -87
- package/dist/react.js +362 -302
- package/dist/react.js.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-D2TZLDP5.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;
|
|
@@ -233,7 +271,8 @@ function useGestureDispatcher(opts) {
|
|
|
233
271
|
const ctxNow = () => ({
|
|
234
272
|
...ctxRef.current,
|
|
235
273
|
depRegistry: routedDeps,
|
|
236
|
-
getRuleCtx: routedRuleCtx
|
|
274
|
+
getRuleCtx: routedRuleCtx,
|
|
275
|
+
viewId: target().id
|
|
237
276
|
});
|
|
238
277
|
const canvas = canvasRef.current;
|
|
239
278
|
const toWorld = (cx, cy) => {
|
|
@@ -248,7 +287,8 @@ function useGestureDispatcher(opts) {
|
|
|
248
287
|
if (before > 0 || after > 0) requestRedrawRef.current?.();
|
|
249
288
|
return result;
|
|
250
289
|
};
|
|
251
|
-
const heldKeys = /* @__PURE__ */ new
|
|
290
|
+
const heldKeys = /* @__PURE__ */ new Map();
|
|
291
|
+
const physicalKey = (e) => e.code || e.key.toLowerCase();
|
|
252
292
|
const held = /* @__PURE__ */ new Map();
|
|
253
293
|
const toCanvasLocal = (p) => {
|
|
254
294
|
const rect = canvas?.getBoundingClientRect();
|
|
@@ -272,7 +312,6 @@ function useGestureDispatcher(opts) {
|
|
|
272
312
|
};
|
|
273
313
|
let multiTouchStart = null;
|
|
274
314
|
const TAP_THRESHOLD_PX = 8;
|
|
275
|
-
const DRAG_THRESHOLD_PX = 4;
|
|
276
315
|
const LONG_PRESS_MS = 500;
|
|
277
316
|
const longPressTimers = /* @__PURE__ */ new Map();
|
|
278
317
|
const cancelLongPress = (pointerId) => {
|
|
@@ -321,8 +360,9 @@ function useGestureDispatcher(opts) {
|
|
|
321
360
|
const DOUBLE_CLICK_MAX_MS = 600;
|
|
322
361
|
const DOUBLE_CLICK_MAX_PX = 8;
|
|
323
362
|
const onKeyDown = (e) => {
|
|
324
|
-
if (isEditableTarget(e.target)) return;
|
|
325
|
-
if (
|
|
363
|
+
if (e.defaultPrevented || isEditableTarget(e.target)) return;
|
|
364
|
+
if (!registryRef.current.isActive()) return;
|
|
365
|
+
if (e.repeat && heldKeys.has(physicalKey(e))) {
|
|
326
366
|
e.preventDefault();
|
|
327
367
|
return;
|
|
328
368
|
}
|
|
@@ -347,17 +387,19 @@ function useGestureDispatcher(opts) {
|
|
|
347
387
|
};
|
|
348
388
|
const heldResult = dispatch(heldEv);
|
|
349
389
|
if (heldResult === "handled") {
|
|
350
|
-
heldKeys.
|
|
390
|
+
heldKeys.set(physicalKey(e), e.key);
|
|
351
391
|
}
|
|
352
392
|
if (keyResult === "handled" || heldResult === "handled") {
|
|
353
393
|
e.preventDefault();
|
|
354
394
|
}
|
|
355
395
|
};
|
|
356
396
|
const onKeyUp = (e) => {
|
|
357
|
-
|
|
397
|
+
const physical = physicalKey(e);
|
|
398
|
+
const downKey = heldKeys.get(physical);
|
|
399
|
+
if (downKey === void 0) return;
|
|
358
400
|
const ev = {
|
|
359
401
|
kind: "key-held",
|
|
360
|
-
key:
|
|
402
|
+
key: downKey,
|
|
361
403
|
phase: "up",
|
|
362
404
|
altKey: e.altKey,
|
|
363
405
|
ctrlKey: e.ctrlKey,
|
|
@@ -365,12 +407,12 @@ function useGestureDispatcher(opts) {
|
|
|
365
407
|
shiftKey: e.shiftKey
|
|
366
408
|
};
|
|
367
409
|
dispatch(ev);
|
|
368
|
-
heldKeys.delete(
|
|
410
|
+
heldKeys.delete(physical);
|
|
369
411
|
};
|
|
370
412
|
const onWindowBlur = () => {
|
|
371
413
|
endPinch();
|
|
372
414
|
for (const p of [...held.values()]) p.session.cancel();
|
|
373
|
-
for (const key of heldKeys) {
|
|
415
|
+
for (const key of heldKeys.values()) {
|
|
374
416
|
const ev = {
|
|
375
417
|
kind: "key-held",
|
|
376
418
|
key,
|
|
@@ -385,6 +427,7 @@ function useGestureDispatcher(opts) {
|
|
|
385
427
|
heldKeys.clear();
|
|
386
428
|
};
|
|
387
429
|
const onWheel = (e) => {
|
|
430
|
+
registryRef.current.activate();
|
|
388
431
|
if (pinchLive && e.ctrlKey) {
|
|
389
432
|
e.preventDefault();
|
|
390
433
|
e.stopPropagation();
|
|
@@ -468,6 +511,7 @@ function useGestureDispatcher(opts) {
|
|
|
468
511
|
endPinch();
|
|
469
512
|
};
|
|
470
513
|
const onPointerDown = (e) => {
|
|
514
|
+
registryRef.current.activate();
|
|
471
515
|
if ((e.button ?? 0) !== 0) return;
|
|
472
516
|
if (!canvas) return;
|
|
473
517
|
routeDown(e.pointerId, e.clientX, e.clientY);
|
|
@@ -560,9 +604,7 @@ function useGestureDispatcher(opts) {
|
|
|
560
604
|
if (rec) rec.pos = { x: e.clientX, y: e.clientY };
|
|
561
605
|
const buffered = rec?.buffered;
|
|
562
606
|
if (rec && buffered) {
|
|
563
|
-
|
|
564
|
-
const dy = e.clientY - buffered.clientY;
|
|
565
|
-
if (Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
|
|
607
|
+
if (pastDragThreshold(buffered, e)) {
|
|
566
608
|
rec.buffered = null;
|
|
567
609
|
cancelLongPress(e.pointerId);
|
|
568
610
|
dispatch(buffered.ev);
|
|
@@ -670,10 +712,17 @@ function useGestureDispatcher(opts) {
|
|
|
670
712
|
...bodyKind !== void 0 ? { bodyKind } : {}
|
|
671
713
|
};
|
|
672
714
|
const ctx = ctxNow();
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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;
|
|
677
726
|
const cursor = dragCursor ?? dispatcherNow().resolveOnly(
|
|
678
727
|
{ kind: "click", x: w.x, y: w.y, pressX: w.x, pressY: w.y, ...where },
|
|
679
728
|
ctx
|
|
@@ -699,6 +748,7 @@ function useGestureDispatcher(opts) {
|
|
|
699
748
|
};
|
|
700
749
|
const onHoverPointerLeave = () => {
|
|
701
750
|
lastHover = null;
|
|
751
|
+
publishLiveDispatch(null);
|
|
702
752
|
paintedCursorRef.current?.()?.clearPointer();
|
|
703
753
|
clearHoverCursor();
|
|
704
754
|
};
|
|
@@ -911,9 +961,10 @@ function useGestureDispatcher(opts) {
|
|
|
911
961
|
});
|
|
912
962
|
if (result === "handled") e.preventDefault();
|
|
913
963
|
};
|
|
964
|
+
const keyCapture = keyboard === "first";
|
|
914
965
|
if (keyboard) {
|
|
915
|
-
window.addEventListener("keydown", onKeyDown);
|
|
916
|
-
window.addEventListener("keyup", onKeyUp);
|
|
966
|
+
window.addEventListener("keydown", onKeyDown, keyCapture);
|
|
967
|
+
window.addEventListener("keyup", onKeyUp, keyCapture);
|
|
917
968
|
}
|
|
918
969
|
window.addEventListener("blur", onWindowBlur);
|
|
919
970
|
window.addEventListener("keydown", scheduleHoverCursorRefresh);
|
|
@@ -939,8 +990,8 @@ function useGestureDispatcher(opts) {
|
|
|
939
990
|
}
|
|
940
991
|
return () => {
|
|
941
992
|
if (keyboard) {
|
|
942
|
-
window.removeEventListener("keydown", onKeyDown);
|
|
943
|
-
window.removeEventListener("keyup", onKeyUp);
|
|
993
|
+
window.removeEventListener("keydown", onKeyDown, keyCapture);
|
|
994
|
+
window.removeEventListener("keyup", onKeyUp, keyCapture);
|
|
944
995
|
}
|
|
945
996
|
window.removeEventListener("blur", onWindowBlur);
|
|
946
997
|
window.removeEventListener("keydown", scheduleHoverCursorRefresh);
|
|
@@ -969,203 +1020,152 @@ function useGestureDispatcher(opts) {
|
|
|
969
1020
|
}, [enabled, keyboard, canvasRef, pointerChannel, wheelChannel, pinchChannel, contextMenuChannel, ingestChannel]);
|
|
970
1021
|
}
|
|
971
1022
|
var ActionsContext = createContext(null);
|
|
972
|
-
function
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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);
|
|
985
1135
|
}
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
|
|
986
1138
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
offBase();
|
|
1006
|
-
listenersRef.current.delete(listener);
|
|
1007
|
-
};
|
|
1008
|
-
},
|
|
1009
|
-
mute: (id) => {
|
|
1010
|
-
mutedRef.current.set(id, (mutedRef.current.get(id) ?? 0) + 1);
|
|
1011
|
-
cacheRef.current = null;
|
|
1012
|
-
notify();
|
|
1013
|
-
let released = false;
|
|
1014
|
-
return () => {
|
|
1015
|
-
if (released) return;
|
|
1016
|
-
released = true;
|
|
1017
|
-
const held = mutedRef.current.get(id);
|
|
1018
|
-
if (held === void 0) return;
|
|
1019
|
-
if (held > 1) mutedRef.current.set(id, held - 1);
|
|
1020
|
-
else mutedRef.current.delete(id);
|
|
1021
|
-
cacheRef.current = null;
|
|
1022
|
-
notify();
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
};
|
|
1026
|
-
}, [base]);
|
|
1027
|
-
}
|
|
1028
|
-
function ActionsScope({ children }) {
|
|
1029
|
-
const parent = useActionsRegistry();
|
|
1030
|
-
const scoped = useMuted(parent);
|
|
1031
|
-
if (!scoped) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
1032
|
-
return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: scoped, children });
|
|
1033
|
-
}
|
|
1034
|
-
function pushOwner(ref, value) {
|
|
1035
|
-
if (value === null) return () => {
|
|
1036
|
-
};
|
|
1037
|
-
ref.current.push(value);
|
|
1038
|
-
let released = false;
|
|
1039
|
-
return () => {
|
|
1040
|
-
if (released) return;
|
|
1041
|
-
released = true;
|
|
1042
|
-
const i = ref.current.lastIndexOf(value);
|
|
1043
|
-
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()
|
|
1044
1157
|
};
|
|
1158
|
+
};
|
|
1159
|
+
var NODES = /* @__PURE__ */ new WeakMap();
|
|
1160
|
+
function actionsNodeOf(registry) {
|
|
1161
|
+
return registry ? NODES.get(registry) ?? null : null;
|
|
1045
1162
|
}
|
|
1046
1163
|
function ActionsProvider({ children }) {
|
|
1047
|
-
const actionsRef = useRef(/* @__PURE__ */ new Map());
|
|
1048
|
-
const liveAction = (id) => actionsRef.current.get(id)?.at(-1);
|
|
1049
|
-
const versionRef = useRef(0);
|
|
1050
|
-
const cachedRef = useRef([]);
|
|
1051
|
-
const cachedVerRef = useRef(-1);
|
|
1052
|
-
const listenersRef = useRef(/* @__PURE__ */ new Set());
|
|
1053
1164
|
const depReg = useOptionalDepRegistry();
|
|
1054
1165
|
const depRegRef = useRef(depReg);
|
|
1055
1166
|
depRegRef.current = depReg;
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
const dispatcherRef = useRef([]);
|
|
1059
|
-
const store = useMemo(() => {
|
|
1060
|
-
const snapshot = () => {
|
|
1061
|
-
const v = versionRef.current;
|
|
1062
|
-
if (cachedVerRef.current === v) return cachedRef.current;
|
|
1063
|
-
const out = Object.freeze(
|
|
1064
|
-
Array.from(actionsRef.current.values(), (stack) => stack[stack.length - 1])
|
|
1065
|
-
);
|
|
1066
|
-
cachedRef.current = out;
|
|
1067
|
-
cachedVerRef.current = v;
|
|
1068
|
-
return out;
|
|
1069
|
-
};
|
|
1070
|
-
const notify = () => {
|
|
1071
|
-
for (const l of listenersRef.current) {
|
|
1072
|
-
try {
|
|
1073
|
-
l();
|
|
1074
|
-
} catch (err) {
|
|
1075
|
-
console.error("weasel ActionsRegistry: subscriber threw", err);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
};
|
|
1079
|
-
return {
|
|
1080
|
-
register: (action) => {
|
|
1081
|
-
validateActionId(action.id);
|
|
1082
|
-
validateActionDefaultBinding(action);
|
|
1083
|
-
const stack = actionsRef.current.get(action.id);
|
|
1084
|
-
if (stack) stack.push(action);
|
|
1085
|
-
else actionsRef.current.set(action.id, [action]);
|
|
1086
|
-
versionRef.current++;
|
|
1087
|
-
notify();
|
|
1088
|
-
let released = false;
|
|
1089
|
-
return () => {
|
|
1090
|
-
if (released) return;
|
|
1091
|
-
released = true;
|
|
1092
|
-
const cur = actionsRef.current.get(action.id);
|
|
1093
|
-
if (!cur) return;
|
|
1094
|
-
const i = cur.lastIndexOf(action);
|
|
1095
|
-
if (i === -1) return;
|
|
1096
|
-
cur.splice(i, 1);
|
|
1097
|
-
if (cur.length === 0) actionsRef.current.delete(action.id);
|
|
1098
|
-
versionRef.current++;
|
|
1099
|
-
notify();
|
|
1100
|
-
};
|
|
1101
|
-
},
|
|
1102
|
-
unregister: (id) => {
|
|
1103
|
-
if (actionsRef.current.delete(id)) {
|
|
1104
|
-
versionRef.current++;
|
|
1105
|
-
notify();
|
|
1106
|
-
}
|
|
1107
|
-
},
|
|
1108
|
-
list: () => snapshot(),
|
|
1109
|
-
trigger: (id, params) => {
|
|
1110
|
-
const a = liveAction(id);
|
|
1111
|
-
if (!a) return false;
|
|
1112
|
-
try {
|
|
1113
|
-
if (a.invoker && a.invoker.timing === "immediate") {
|
|
1114
|
-
const r = wiredDepRegRef.current.at(-1) ?? depRegRef.current;
|
|
1115
|
-
const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
|
|
1116
|
-
selection: r.get("selection"),
|
|
1117
|
-
scene: r.get("scene"),
|
|
1118
|
-
history: r.get("history"),
|
|
1119
|
-
view: r.get("view"),
|
|
1120
|
-
activeTool: r.get("activeTool"),
|
|
1121
|
-
booleansAdapter: r.get("booleansAdapter")
|
|
1122
|
-
};
|
|
1123
|
-
a.invoker.run(deps, params);
|
|
1124
|
-
}
|
|
1125
|
-
} catch (err) {
|
|
1126
|
-
console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
|
|
1127
|
-
}
|
|
1128
|
-
return true;
|
|
1129
|
-
},
|
|
1130
|
-
subscribe: (listener) => {
|
|
1131
|
-
listenersRef.current.add(listener);
|
|
1132
|
-
return () => {
|
|
1133
|
-
listenersRef.current.delete(listener);
|
|
1134
|
-
};
|
|
1135
|
-
},
|
|
1136
|
-
setDispatcher: (d) => {
|
|
1137
|
-
if (d && dispatcherRef.current.length > 0 && dispatcherRef.current.at(-1) !== d && !warnedRef.current) {
|
|
1138
|
-
warnedRef.current = true;
|
|
1139
|
-
warnSharedScope();
|
|
1140
|
-
}
|
|
1141
|
-
return pushOwner(dispatcherRef, d);
|
|
1142
|
-
},
|
|
1143
|
-
setDepRegistry: (r) => pushOwner(wiredDepRegRef, r),
|
|
1144
|
-
begin: (id, params) => {
|
|
1145
|
-
const disp = dispatcherRef.current.at(-1) ?? null;
|
|
1146
|
-
if (!disp) return null;
|
|
1147
|
-
const r = wiredDepRegRef.current.at(-1) ?? depRegRef.current;
|
|
1148
|
-
const a = liveAction(id);
|
|
1149
|
-
const deps = !r ? {} : a?.requires ? buildDepsFromRequires(a, r) : {
|
|
1150
|
-
selection: r.get("selection"),
|
|
1151
|
-
scene: r.get("scene"),
|
|
1152
|
-
history: r.get("history"),
|
|
1153
|
-
view: r.get("view"),
|
|
1154
|
-
activeTool: r.get("activeTool"),
|
|
1155
|
-
booleansAdapter: r.get("booleansAdapter")
|
|
1156
|
-
};
|
|
1157
|
-
return disp.beginUiOngoing(id, deps, params);
|
|
1158
|
-
}
|
|
1159
|
-
};
|
|
1160
|
-
}, []);
|
|
1161
|
-
const registry = useMuted(store);
|
|
1162
|
-
return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry, children });
|
|
1163
|
-
}
|
|
1164
|
-
function warnSharedScope() {
|
|
1165
|
-
if (!IS_DEV) return;
|
|
1166
|
-
console.warn(
|
|
1167
|
-
"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>."
|
|
1168
|
-
);
|
|
1167
|
+
const node = useMemo(() => new ActionsNode(null, true, () => depRegRef.current), []);
|
|
1168
|
+
return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: node.registry, children });
|
|
1169
1169
|
}
|
|
1170
1170
|
var IS_DEV = (() => {
|
|
1171
1171
|
try {
|
|
@@ -1191,6 +1191,41 @@ function useAction(action) {
|
|
|
1191
1191
|
return reg.register(action);
|
|
1192
1192
|
}, [reg, action]);
|
|
1193
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
|
+
}
|
|
1194
1229
|
function useOngoingAction(actionId) {
|
|
1195
1230
|
const reg = useActionsRegistry();
|
|
1196
1231
|
const regRef = useRef(reg);
|
|
@@ -1232,14 +1267,14 @@ function useContributions(opts) {
|
|
|
1232
1267
|
const needsFirstMountSyncRef = useRef(false);
|
|
1233
1268
|
if (isFirstRender) {
|
|
1234
1269
|
hasInitializedRef.current = true;
|
|
1235
|
-
needsFirstMountSyncRef.current = ctx.active ===
|
|
1270
|
+
needsFirstMountSyncRef.current = ctx.active === null && opts.focused !== null;
|
|
1236
1271
|
}
|
|
1237
1272
|
useEffect(() => {
|
|
1238
1273
|
if (!needsFirstMountSyncRef.current) return;
|
|
1239
1274
|
needsFirstMountSyncRef.current = false;
|
|
1240
1275
|
ctx.setActive(opts.focused);
|
|
1241
1276
|
}, []);
|
|
1242
|
-
const focused = isFirstRender && ctx.active ===
|
|
1277
|
+
const focused = isFirstRender && ctx.active === null && opts.focused !== null ? opts.focused : ctx.active;
|
|
1243
1278
|
const entriesRef = useRef(opts.entries);
|
|
1244
1279
|
entriesRef.current = opts.entries;
|
|
1245
1280
|
const focusedRef = useRef(focused);
|
|
@@ -1285,21 +1320,15 @@ function useContributions(opts) {
|
|
|
1285
1320
|
}
|
|
1286
1321
|
return [...active, ...hotkey, ...ambient];
|
|
1287
1322
|
}, [snapshot]);
|
|
1288
|
-
|
|
1289
|
-
useEffect(() => {
|
|
1290
|
-
if (!actionsRegistry || offhandSig === "") return;
|
|
1291
|
-
const specs = offhandSig.split(",").map((entry) => {
|
|
1292
|
-
const [toolId, trigger] = entry.split(":");
|
|
1293
|
-
return { toolId, key: offhandKeyFor(trigger) };
|
|
1294
|
-
});
|
|
1295
|
-
return actionsRegistry.register(makeToolOffhandAction(buildToolOffhandBindings(specs)));
|
|
1296
|
-
}, [actionsRegistry, offhandSig]);
|
|
1323
|
+
useOffhandAction(opts.entries);
|
|
1297
1324
|
const lastConflictSigRef = useRef(null);
|
|
1298
1325
|
const entrySig = opts.entries.map((e) => e.id).join(",");
|
|
1326
|
+
const modesRegistry = opts.modes;
|
|
1299
1327
|
useEffect(() => {
|
|
1300
1328
|
if (!isDev()) return;
|
|
1301
|
-
|
|
1302
|
-
|
|
1329
|
+
const last = lastConflictSigRef.current;
|
|
1330
|
+
if (last?.sig === entrySig && last.modes === modesRegistry) return;
|
|
1331
|
+
lastConflictSigRef.current = { sig: entrySig, modes: modesRegistry };
|
|
1303
1332
|
const registry = [];
|
|
1304
1333
|
const ambient = [];
|
|
1305
1334
|
for (const entry of entriesRef.current) {
|
|
@@ -1309,9 +1338,10 @@ function useContributions(opts) {
|
|
|
1309
1338
|
reportRouteConflicts({
|
|
1310
1339
|
registry,
|
|
1311
1340
|
ambient,
|
|
1312
|
-
actions: actionsRegistry?.list() ?? []
|
|
1341
|
+
actions: actionsRegistry?.list() ?? [],
|
|
1342
|
+
...modesRegistry ? { modes: modesRegistry.list().map(activeModeOf) } : {}
|
|
1313
1343
|
});
|
|
1314
|
-
}, [entrySig, actionsRegistry]);
|
|
1344
|
+
}, [entrySig, actionsRegistry, modesRegistry]);
|
|
1315
1345
|
return useMemo(
|
|
1316
1346
|
() => ({
|
|
1317
1347
|
get entries() {
|
|
@@ -1325,6 +1355,16 @@ function useContributions(opts) {
|
|
|
1325
1355
|
[focused, setFocused, scopedBindings, overlays]
|
|
1326
1356
|
);
|
|
1327
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
|
+
}
|
|
1328
1368
|
|
|
1329
1369
|
// src/tools/useTools.ts
|
|
1330
1370
|
function declareSlot(tool, slot) {
|
|
@@ -1336,6 +1376,24 @@ function declareSlot(tool, slot) {
|
|
|
1336
1376
|
};
|
|
1337
1377
|
return sameEligibility(tool.eligibility, eligibility) ? tool : { ...tool, eligibility };
|
|
1338
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
|
+
}
|
|
1339
1397
|
function sameEligibility(a, b) {
|
|
1340
1398
|
if (!a) return false;
|
|
1341
1399
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
@@ -1343,16 +1401,18 @@ function sameEligibility(a, b) {
|
|
|
1343
1401
|
return true;
|
|
1344
1402
|
}
|
|
1345
1403
|
function useTools(opts) {
|
|
1346
|
-
|
|
1347
|
-
|
|
1404
|
+
const initialActive = opts.active ?? null;
|
|
1405
|
+
if (initialActive !== null && !(initialActive in opts.registry)) {
|
|
1406
|
+
throw new Error(`useTools: active "${initialActive}" not in registry`);
|
|
1348
1407
|
}
|
|
1349
1408
|
const ctx = useActiveToolContext();
|
|
1409
|
+
const sources = useStableSources(opts.registry, opts.ambient);
|
|
1350
1410
|
const slotted = useMemo(() => {
|
|
1351
1411
|
const registry = {};
|
|
1352
|
-
for (const [id, tool] of Object.entries(
|
|
1412
|
+
for (const [id, tool] of Object.entries(sources.registry)) {
|
|
1353
1413
|
registry[id] = declareSlot(tool, "focus");
|
|
1354
1414
|
}
|
|
1355
|
-
const ambient =
|
|
1415
|
+
const ambient = sources.ambient.map((t) => declareSlot(t, "always"));
|
|
1356
1416
|
const byId = /* @__PURE__ */ new Map();
|
|
1357
1417
|
for (const tool of [...Object.values(registry), ...ambient]) {
|
|
1358
1418
|
const prior = byId.get(tool.id);
|
|
@@ -1360,8 +1420,12 @@ function useTools(opts) {
|
|
|
1360
1420
|
else byId.set(tool.id, tool);
|
|
1361
1421
|
}
|
|
1362
1422
|
return { registry, ambient, entries: [...byId.values()] };
|
|
1363
|
-
}, [
|
|
1364
|
-
const contributions = useContributions({
|
|
1423
|
+
}, [sources]);
|
|
1424
|
+
const contributions = useContributions({
|
|
1425
|
+
entries: slotted.entries,
|
|
1426
|
+
focused: initialActive,
|
|
1427
|
+
...opts.modes ? { modes: opts.modes } : {}
|
|
1428
|
+
});
|
|
1365
1429
|
const hotkeyEngaged = ctx.hotkeyStack.at(-1) ?? null;
|
|
1366
1430
|
const slottedRef = useRef(slotted);
|
|
1367
1431
|
slottedRef.current = slotted;
|
|
@@ -1372,7 +1436,7 @@ function useTools(opts) {
|
|
|
1372
1436
|
const setFocused = contributions.setFocused;
|
|
1373
1437
|
const setActive = useCallback(
|
|
1374
1438
|
(id) => {
|
|
1375
|
-
if (!(id in slottedRef.current.registry)) {
|
|
1439
|
+
if (id !== null && !(id in slottedRef.current.registry)) {
|
|
1376
1440
|
throw new Error(`setActive: "${id}" not in registry`);
|
|
1377
1441
|
}
|
|
1378
1442
|
dlog("tools", "active:", activeRef.current, "\u2192", id);
|
|
@@ -1400,7 +1464,6 @@ function useTools(opts) {
|
|
|
1400
1464
|
);
|
|
1401
1465
|
const active = contributions.focused;
|
|
1402
1466
|
const getActiveOverlays = contributions.overlays;
|
|
1403
|
-
const registryKey = Object.keys(slotted.registry).sort().join("|") + "#" + slotted.ambient.map((t) => t.id).join("|");
|
|
1404
1467
|
return useMemo(
|
|
1405
1468
|
() => ({
|
|
1406
1469
|
active,
|
|
@@ -1408,18 +1471,15 @@ function useTools(opts) {
|
|
|
1408
1471
|
hotkeyEngaged,
|
|
1409
1472
|
engageHotkey,
|
|
1410
1473
|
disengageHotkey,
|
|
1411
|
-
ambient:
|
|
1412
|
-
registry:
|
|
1474
|
+
ambient: slotted.ambient,
|
|
1475
|
+
registry: slotted.registry,
|
|
1413
1476
|
has,
|
|
1414
1477
|
getActiveOverlays
|
|
1415
1478
|
}),
|
|
1416
|
-
|
|
1417
|
-
// `slottedRef.current`, and the key is what tells us that ref's contents moved.
|
|
1418
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1419
|
-
[active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays, registryKey]
|
|
1479
|
+
[active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays, slotted]
|
|
1420
1480
|
);
|
|
1421
1481
|
}
|
|
1422
1482
|
|
|
1423
|
-
export { ActionsProvider,
|
|
1483
|
+
export { ActionsProvider, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, InputScope, Yoke, useAction, useActionsRegistry, useActiveToolContext, useContributions, useDepRegistry, useDepSource, useGestureDispatcher, useOffhandAction, useOngoingAction, useOptionalActiveToolContext, useOptionalDepRegistry, useTools, useYoke };
|
|
1424
1484
|
//# sourceMappingURL=react.js.map
|
|
1425
1485
|
//# sourceMappingURL=react.js.map
|