@weasel-js/routing 1.6.1 → 1.7.1
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/README.md +3 -1
- package/dist/{chunk-VEC3ACV5.js → chunk-ZGKJWZ4S.js} +774 -380
- package/dist/chunk-ZGKJWZ4S.js.map +1 -0
- package/dist/{index-CKmbNpwl.d.ts → index-BV53gfyB.d.ts} +649 -257
- package/dist/index.d.ts +2 -2
- package/dist/index.js +84 -8
- package/dist/index.js.map +1 -1
- package/dist/internal--mYnGecm.d.ts +275 -0
- package/dist/internal.d.ts +7 -0
- package/dist/internal.js +3 -0
- package/dist/internal.js.map +1 -0
- package/dist/react.d.ts +213 -344
- package/dist/react.js +456 -406
- package/dist/react.js.map +1 -1
- package/package.json +10 -5
- package/dist/chunk-VEC3ACV5.js.map +0 -1
package/dist/react.js
CHANGED
|
@@ -1,37 +1,81 @@
|
|
|
1
|
-
import { createDispatcher,
|
|
2
|
-
import { createContext, useState,
|
|
1
|
+
import { createDepRegistry, createDispatcher, isTool, scopeBindings, liveScope, isDev, reportRouteConflicts, offhandKeyFor, makeToolOffhandAction, buildToolOffhandBindings, dlog, isEditableTarget, clientToCanvasRect, openPointerSession, LONG_PRESS_MS, publishLiveDispatch, pointerGestureId, itemsFromDataTransfer, itemsFromClipboardData, ScopeNode, pushOwner, validateActionId, validateActionDefaultBinding, depNodeOf, DepNode, buildDepsFromRequires } from './chunk-ZGKJWZ4S.js';
|
|
2
|
+
import { createContext, useState, useContext, useMemo, useEffect, useRef, useCallback, useSyncExternalStore } from 'react';
|
|
3
|
+
import { useLatest, useStableByContent, sameList } from '@weasel-js/react';
|
|
3
4
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
import { pastDragThreshold } from '@weasel-js/gestures';
|
|
4
6
|
import { resolveCursorTier } from '@weasel-js/cursor';
|
|
7
|
+
import { activeModeOf } from '@weasel-js/modes';
|
|
5
8
|
|
|
9
|
+
var ToolNode = class extends ScopeNode {
|
|
10
|
+
state;
|
|
11
|
+
value = null;
|
|
12
|
+
constructor(parent, descends, owns, initialActive) {
|
|
13
|
+
super(parent, descends);
|
|
14
|
+
this.state = owns ? { active: initialActive ?? parent?.owner().state?.active ?? null, hotkeyStack: [] } : null;
|
|
15
|
+
}
|
|
16
|
+
/** The node whose tool a read here resolves to. */
|
|
17
|
+
owner() {
|
|
18
|
+
for (const n of this.start().chain()) if (n.state) return n;
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
update(next) {
|
|
22
|
+
const owner = this.owner();
|
|
23
|
+
if (!owner.state) return;
|
|
24
|
+
const updated = next(owner.state);
|
|
25
|
+
if (updated === owner.state) return;
|
|
26
|
+
owner.state = updated;
|
|
27
|
+
owner.changed();
|
|
28
|
+
}
|
|
29
|
+
/** A stable value per owner and state, for `useSyncExternalStore`. */
|
|
30
|
+
snapshot() {
|
|
31
|
+
const from = this.owner();
|
|
32
|
+
const state = from.state ?? { active: null, hotkeyStack: [] };
|
|
33
|
+
const cached = this.value;
|
|
34
|
+
if (cached && cached.from === from && cached.state === state) return cached.out;
|
|
35
|
+
const out = {
|
|
36
|
+
active: state.active,
|
|
37
|
+
hotkeyStack: state.hotkeyStack,
|
|
38
|
+
setActive: (id) => this.update((s) => s.active === id ? s : { ...s, active: id }),
|
|
39
|
+
pushHotkey: (id) => this.update((s) => ({ ...s, hotkeyStack: [...s.hotkeyStack, id] })),
|
|
40
|
+
popHotkey: (id) => this.update((s) => {
|
|
41
|
+
const stack = s.hotkeyStack;
|
|
42
|
+
if (stack.length === 0) return s;
|
|
43
|
+
if (id === void 0) return { ...s, hotkeyStack: stack.slice(0, -1) };
|
|
44
|
+
const i = stack.lastIndexOf(id);
|
|
45
|
+
return i === -1 ? s : { ...s, hotkeyStack: [...stack.slice(0, i), ...stack.slice(i + 1)] };
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
this.value = { from, state, out };
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
subscribe = (listener) => {
|
|
52
|
+
this.tree.listeners.add(listener);
|
|
53
|
+
return () => {
|
|
54
|
+
this.tree.listeners.delete(listener);
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
6
58
|
var ActiveToolContext = createContext(null);
|
|
59
|
+
var ActiveToolNodeContext = ActiveToolContext;
|
|
7
60
|
function ActiveToolContextProvider({
|
|
8
61
|
children,
|
|
9
|
-
initialActive =
|
|
62
|
+
initialActive = null
|
|
10
63
|
}) {
|
|
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]
|
|
64
|
+
const [node] = useState(() => new ToolNode(null, true, true, initialActive));
|
|
65
|
+
return /* @__PURE__ */ jsx(ActiveToolContext.Provider, { value: node, children });
|
|
66
|
+
}
|
|
67
|
+
function useNodeValue(node) {
|
|
68
|
+
const subscribe = useMemo(() => node?.subscribe ?? NO_SUBSCRIBE, [node]);
|
|
69
|
+
return useSyncExternalStore(
|
|
70
|
+
subscribe,
|
|
71
|
+
() => node?.snapshot() ?? null,
|
|
72
|
+
() => node?.snapshot() ?? null
|
|
30
73
|
);
|
|
31
|
-
return /* @__PURE__ */ jsx(ActiveToolContext.Provider, { value, children });
|
|
32
74
|
}
|
|
75
|
+
var NO_SUBSCRIBE = () => () => {
|
|
76
|
+
};
|
|
33
77
|
function useActiveToolContext() {
|
|
34
|
-
const value = useContext(ActiveToolContext);
|
|
78
|
+
const value = useNodeValue(useContext(ActiveToolContext));
|
|
35
79
|
if (value === null) {
|
|
36
80
|
throw new Error(
|
|
37
81
|
"useActiveToolContext: no ActiveToolContextProvider in scope. Wrap your tree with <ActiveToolContextProvider> (typically inside <SceneCanvas>)."
|
|
@@ -40,7 +84,7 @@ function useActiveToolContext() {
|
|
|
40
84
|
return value;
|
|
41
85
|
}
|
|
42
86
|
function useOptionalActiveToolContext() {
|
|
43
|
-
return useContext(ActiveToolContext);
|
|
87
|
+
return useNodeValue(useContext(ActiveToolContext));
|
|
44
88
|
}
|
|
45
89
|
function ActiveToolContextProviderIfRoot({
|
|
46
90
|
children
|
|
@@ -51,28 +95,7 @@ function ActiveToolContextProviderIfRoot({
|
|
|
51
95
|
}
|
|
52
96
|
var DepRegistryContext = createContext(null);
|
|
53
97
|
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
|
-
}), []);
|
|
98
|
+
const registry = useMemo(createDepRegistry, []);
|
|
76
99
|
return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry, children });
|
|
77
100
|
}
|
|
78
101
|
function useDepRegistry() {
|
|
@@ -87,11 +110,10 @@ function useOptionalDepRegistry() {
|
|
|
87
110
|
}
|
|
88
111
|
function useDepSource(name, source) {
|
|
89
112
|
const registry = useDepRegistry();
|
|
90
|
-
const sourceRef =
|
|
91
|
-
sourceRef.current = source;
|
|
113
|
+
const sourceRef = useLatest(source);
|
|
92
114
|
useEffect(() => {
|
|
93
115
|
return registry.register(name, () => sourceRef.current());
|
|
94
|
-
}, [name, registry]);
|
|
116
|
+
}, [name, registry, sourceRef]);
|
|
95
117
|
}
|
|
96
118
|
var DROPOVER_CLASS = "weasel-dropover";
|
|
97
119
|
function stylusOf(e) {
|
|
@@ -134,37 +156,29 @@ 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, longPress } = opts;
|
|
138
160
|
const pointerChannel = channels?.pointer !== false;
|
|
139
161
|
const wheelChannel = channels?.wheel !== false;
|
|
140
162
|
const pinchChannel = channels?.pinch !== false;
|
|
141
163
|
const contextMenuChannel = channels?.contextMenu !== false;
|
|
142
164
|
const ingestChannel = channels?.ingest !== false;
|
|
143
|
-
const onDoubleClickRef =
|
|
144
|
-
onDoubleClickRef.current = onDoubleClick;
|
|
165
|
+
const onDoubleClickRef = useLatest(onDoubleClick);
|
|
145
166
|
const activeTool = useActiveToolContext();
|
|
146
167
|
const depRegistry = useDepRegistry();
|
|
147
168
|
const dispatcherRef = useRef(null);
|
|
148
169
|
if (!dispatcherRef.current) {
|
|
149
170
|
dispatcherRef.current = dispatcherOpt ?? createDispatcher();
|
|
150
171
|
}
|
|
151
|
-
const
|
|
172
|
+
const registryRef = useLatest(actions);
|
|
173
|
+
const ctxRef = useLatest({
|
|
152
174
|
actions,
|
|
153
175
|
depRegistry,
|
|
154
176
|
activeToolId: activeTool.active,
|
|
155
177
|
hotkeyStack: activeTool.hotkeyStack,
|
|
156
|
-
|
|
178
|
+
entriesById,
|
|
157
179
|
isMac: IS_MAC
|
|
158
180
|
});
|
|
159
|
-
|
|
160
|
-
actions,
|
|
161
|
-
depRegistry,
|
|
162
|
-
activeToolId: activeTool.active,
|
|
163
|
-
hotkeyStack: activeTool.hotkeyStack,
|
|
164
|
-
toolsById,
|
|
165
|
-
isMac: IS_MAC
|
|
166
|
-
};
|
|
167
|
-
const rootTargetRef = useRef({
|
|
181
|
+
const rootTargetRef = useLatest({
|
|
168
182
|
id: null,
|
|
169
183
|
dispatcher: dispatcherRef.current,
|
|
170
184
|
affordanceAt,
|
|
@@ -172,44 +186,47 @@ function useGestureDispatcher(opts) {
|
|
|
172
186
|
clientToWorld,
|
|
173
187
|
getRuleCtx
|
|
174
188
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
dispatcher: dispatcherRef.current,
|
|
178
|
-
affordanceAt,
|
|
179
|
-
classifyTarget,
|
|
180
|
-
clientToWorld,
|
|
181
|
-
getRuleCtx
|
|
182
|
-
};
|
|
183
|
-
const viewsRef = useRef(views);
|
|
184
|
-
viewsRef.current = views;
|
|
185
|
-
const allDispatchers = () => [
|
|
189
|
+
const viewsRef = useLatest(views);
|
|
190
|
+
const allDispatchers = useCallback(() => [
|
|
186
191
|
rootTargetRef.current.dispatcher,
|
|
187
192
|
...(viewsRef.current?.targets() ?? []).map((t) => t.dispatcher)
|
|
188
|
-
];
|
|
189
|
-
const requestRedrawRef =
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
paintedCursorRef.current = paintedCursor;
|
|
193
|
+
], [rootTargetRef, viewsRef]);
|
|
194
|
+
const requestRedrawRef = useLatest(requestRedraw);
|
|
195
|
+
const paintedCursorRef = useLatest(paintedCursor);
|
|
196
|
+
const longPressRef = useLatest(longPress);
|
|
193
197
|
const lastClickRef = useRef(null);
|
|
198
|
+
const liveToolsRef = useRef(/* @__PURE__ */ new Set());
|
|
194
199
|
const prevActiveRef = useRef(activeTool.active);
|
|
200
|
+
const entriesByIdRef = useLatest(entriesById);
|
|
201
|
+
const fireLifecycle = useCallback((id, hook) => {
|
|
202
|
+
const tool = entriesByIdRef.current.get(id);
|
|
203
|
+
if (!tool || !isTool(tool)) return;
|
|
204
|
+
const fn = tool[hook];
|
|
205
|
+
if (!fn) return;
|
|
206
|
+
try {
|
|
207
|
+
fn({ scratch: tool.initScratch?.() });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.error(`weasel: tool "${id}".${hook} threw`, err);
|
|
210
|
+
}
|
|
211
|
+
}, [entriesByIdRef]);
|
|
195
212
|
useEffect(() => {
|
|
213
|
+
const prev = liveToolsRef.current;
|
|
214
|
+
const next = new Set(
|
|
215
|
+
activeTool.active === null ? activeTool.hotkeyStack : [activeTool.active, ...activeTool.hotkeyStack]
|
|
216
|
+
);
|
|
217
|
+
liveToolsRef.current = next;
|
|
218
|
+
for (const id of prev) if (!next.has(id)) fireLifecycle(id, "onDeactivate");
|
|
196
219
|
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
220
|
for (const d of allDispatchers()) d.cancelAll("cancel");
|
|
210
221
|
}
|
|
211
222
|
prevActiveRef.current = activeTool.active;
|
|
223
|
+
for (const id of next) if (!prev.has(id)) fireLifecycle(id, "onActivate");
|
|
212
224
|
});
|
|
225
|
+
useEffect(() => () => {
|
|
226
|
+
const live = liveToolsRef.current;
|
|
227
|
+
liveToolsRef.current = /* @__PURE__ */ new Set();
|
|
228
|
+
for (const id of live) fireLifecycle(id, "onDeactivate");
|
|
229
|
+
}, [fireLifecycle]);
|
|
213
230
|
useEffect(() => {
|
|
214
231
|
if (!enabled) return;
|
|
215
232
|
let disposed = false;
|
|
@@ -249,7 +266,8 @@ function useGestureDispatcher(opts) {
|
|
|
249
266
|
if (before > 0 || after > 0) requestRedrawRef.current?.();
|
|
250
267
|
return result;
|
|
251
268
|
};
|
|
252
|
-
const heldKeys = /* @__PURE__ */ new
|
|
269
|
+
const heldKeys = /* @__PURE__ */ new Map();
|
|
270
|
+
const physicalKey = (e) => e.code || e.key.toLowerCase();
|
|
253
271
|
const held = /* @__PURE__ */ new Map();
|
|
254
272
|
const toCanvasLocal = (p) => {
|
|
255
273
|
const rect = canvas?.getBoundingClientRect();
|
|
@@ -273,23 +291,27 @@ function useGestureDispatcher(opts) {
|
|
|
273
291
|
};
|
|
274
292
|
let multiTouchStart = null;
|
|
275
293
|
const TAP_THRESHOLD_PX = 8;
|
|
276
|
-
const DRAG_THRESHOLD_PX = 4;
|
|
277
|
-
const LONG_PRESS_MS = 500;
|
|
278
294
|
const longPressTimers = /* @__PURE__ */ new Map();
|
|
295
|
+
const clearPending = (pointerId) => {
|
|
296
|
+
const store = longPressRef.current?.state;
|
|
297
|
+
const p = store?.get();
|
|
298
|
+
if (p && (pointerId === void 0 || p.pointerId === pointerId)) store.set(null);
|
|
299
|
+
};
|
|
279
300
|
const cancelLongPress = (pointerId) => {
|
|
280
301
|
const t = longPressTimers.get(pointerId);
|
|
281
302
|
if (t !== void 0) {
|
|
282
303
|
clearTimeout(t);
|
|
283
304
|
longPressTimers.delete(pointerId);
|
|
284
305
|
}
|
|
306
|
+
clearPending(pointerId);
|
|
285
307
|
};
|
|
286
308
|
const cancelAllLongPress = () => {
|
|
287
309
|
for (const t of longPressTimers.values()) clearTimeout(t);
|
|
288
310
|
longPressTimers.clear();
|
|
311
|
+
clearPending();
|
|
289
312
|
};
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
if (!down) return;
|
|
313
|
+
const HAPTIC_MS = 15;
|
|
314
|
+
const longPressEvents = (down) => {
|
|
293
315
|
const shared = {
|
|
294
316
|
target: down.target,
|
|
295
317
|
altKey: down.altKey,
|
|
@@ -300,30 +322,34 @@ function useGestureDispatcher(opts) {
|
|
|
300
322
|
...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
|
|
301
323
|
...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
|
|
302
324
|
};
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
325
|
+
const at = { x: down.worldX, y: down.worldY, clientX: down.clientX, clientY: down.clientY };
|
|
326
|
+
return {
|
|
327
|
+
longpress: { kind: "longpress", ...at, ...shared },
|
|
328
|
+
contextmenu: { kind: "contextmenu", ...at, ...shared }
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
const longPressWouldFire = (down) => {
|
|
332
|
+
const evs = longPressEvents(down);
|
|
333
|
+
const d = dispatcherNow();
|
|
334
|
+
return d.resolveOnly(evs.longpress, ctxNow()) !== null || d.resolveOnly(evs.contextmenu, ctxNow()) !== null;
|
|
335
|
+
};
|
|
336
|
+
const fireLongPress = (pointerId, pointerType) => {
|
|
337
|
+
const down = held.get(pointerId)?.down;
|
|
338
|
+
clearPending(pointerId);
|
|
339
|
+
if (!down) return;
|
|
340
|
+
const evs = longPressEvents(down);
|
|
341
|
+
let result = dispatch(evs.longpress);
|
|
342
|
+
if (result === "unhandled") result = dispatch(evs.contextmenu);
|
|
343
|
+
if (result === "handled" && longPressRef.current?.haptics !== false && (pointerType === "touch" || pointerType === "pen") && typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
|
|
344
|
+
navigator.vibrate(HAPTIC_MS);
|
|
320
345
|
}
|
|
321
346
|
};
|
|
322
347
|
const DOUBLE_CLICK_MAX_MS = 600;
|
|
323
348
|
const DOUBLE_CLICK_MAX_PX = 8;
|
|
324
349
|
const onKeyDown = (e) => {
|
|
325
|
-
if (isEditableTarget(e.target)) return;
|
|
326
|
-
if (
|
|
350
|
+
if (e.defaultPrevented || isEditableTarget(e.target)) return;
|
|
351
|
+
if (!registryRef.current.isActive()) return;
|
|
352
|
+
if (e.repeat && heldKeys.has(physicalKey(e))) {
|
|
327
353
|
e.preventDefault();
|
|
328
354
|
return;
|
|
329
355
|
}
|
|
@@ -348,17 +374,19 @@ function useGestureDispatcher(opts) {
|
|
|
348
374
|
};
|
|
349
375
|
const heldResult = dispatch(heldEv);
|
|
350
376
|
if (heldResult === "handled") {
|
|
351
|
-
heldKeys.
|
|
377
|
+
heldKeys.set(physicalKey(e), e.key);
|
|
352
378
|
}
|
|
353
379
|
if (keyResult === "handled" || heldResult === "handled") {
|
|
354
380
|
e.preventDefault();
|
|
355
381
|
}
|
|
356
382
|
};
|
|
357
383
|
const onKeyUp = (e) => {
|
|
358
|
-
|
|
384
|
+
const physical = physicalKey(e);
|
|
385
|
+
const downKey = heldKeys.get(physical);
|
|
386
|
+
if (downKey === void 0) return;
|
|
359
387
|
const ev = {
|
|
360
388
|
kind: "key-held",
|
|
361
|
-
key:
|
|
389
|
+
key: downKey,
|
|
362
390
|
phase: "up",
|
|
363
391
|
altKey: e.altKey,
|
|
364
392
|
ctrlKey: e.ctrlKey,
|
|
@@ -366,12 +394,12 @@ function useGestureDispatcher(opts) {
|
|
|
366
394
|
shiftKey: e.shiftKey
|
|
367
395
|
};
|
|
368
396
|
dispatch(ev);
|
|
369
|
-
heldKeys.delete(
|
|
397
|
+
heldKeys.delete(physical);
|
|
370
398
|
};
|
|
371
399
|
const onWindowBlur = () => {
|
|
372
400
|
endPinch();
|
|
373
401
|
for (const p of [...held.values()]) p.session.cancel();
|
|
374
|
-
for (const key of heldKeys) {
|
|
402
|
+
for (const key of heldKeys.values()) {
|
|
375
403
|
const ev = {
|
|
376
404
|
kind: "key-held",
|
|
377
405
|
key,
|
|
@@ -386,6 +414,7 @@ function useGestureDispatcher(opts) {
|
|
|
386
414
|
heldKeys.clear();
|
|
387
415
|
};
|
|
388
416
|
const onWheel = (e) => {
|
|
417
|
+
registryRef.current.activate();
|
|
389
418
|
if (pinchLive && e.ctrlKey) {
|
|
390
419
|
e.preventDefault();
|
|
391
420
|
e.stopPropagation();
|
|
@@ -469,6 +498,7 @@ function useGestureDispatcher(opts) {
|
|
|
469
498
|
endPinch();
|
|
470
499
|
};
|
|
471
500
|
const onPointerDown = (e) => {
|
|
501
|
+
registryRef.current.activate();
|
|
472
502
|
if ((e.button ?? 0) !== 0) return;
|
|
473
503
|
if (!canvas) return;
|
|
474
504
|
routeDown(e.pointerId, e.clientX, e.clientY);
|
|
@@ -524,14 +554,30 @@ function useGestureDispatcher(opts) {
|
|
|
524
554
|
if (held.size < 2) dispatch({ ...ev, stage: "press" });
|
|
525
555
|
if ((e.pointerType === "touch" || e.pointerType === "pen") && held.size === 1) {
|
|
526
556
|
cancelLongPress(e.pointerId);
|
|
557
|
+
const duration = longPressRef.current?.duration ?? LONG_PRESS_MS;
|
|
527
558
|
longPressTimers.set(
|
|
528
559
|
e.pointerId,
|
|
529
560
|
setTimeout(() => {
|
|
530
561
|
longPressTimers.delete(e.pointerId);
|
|
531
562
|
if (disposed) return;
|
|
532
|
-
fireLongPress(e.pointerId);
|
|
533
|
-
},
|
|
563
|
+
fireLongPress(e.pointerId, e.pointerType);
|
|
564
|
+
}, duration)
|
|
534
565
|
);
|
|
566
|
+
const store = longPressRef.current?.state;
|
|
567
|
+
const rec = held.get(e.pointerId);
|
|
568
|
+
if (store && rec) {
|
|
569
|
+
store.set({
|
|
570
|
+
pointerId: e.pointerId,
|
|
571
|
+
pointerType: e.pointerType,
|
|
572
|
+
client: { x: e.clientX, y: e.clientY },
|
|
573
|
+
local: toCanvasLocal({ x: e.clientX, y: e.clientY }),
|
|
574
|
+
world: { x: w.x, y: w.y },
|
|
575
|
+
viewId: target().id,
|
|
576
|
+
startedAt: performance.now(),
|
|
577
|
+
duration,
|
|
578
|
+
armed: longPressWouldFire(rec.down)
|
|
579
|
+
});
|
|
580
|
+
}
|
|
535
581
|
}
|
|
536
582
|
if (held.size >= 2) {
|
|
537
583
|
cancelAllLongPress();
|
|
@@ -561,9 +607,7 @@ function useGestureDispatcher(opts) {
|
|
|
561
607
|
if (rec) rec.pos = { x: e.clientX, y: e.clientY };
|
|
562
608
|
const buffered = rec?.buffered;
|
|
563
609
|
if (rec && buffered) {
|
|
564
|
-
|
|
565
|
-
const dy = e.clientY - buffered.clientY;
|
|
566
|
-
if (Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
|
|
610
|
+
if (pastDragThreshold(buffered, e)) {
|
|
567
611
|
rec.buffered = null;
|
|
568
612
|
cancelLongPress(e.pointerId);
|
|
569
613
|
dispatch(buffered.ev);
|
|
@@ -671,10 +715,17 @@ function useGestureDispatcher(opts) {
|
|
|
671
715
|
...bodyKind !== void 0 ? { bodyKind } : {}
|
|
672
716
|
};
|
|
673
717
|
const ctx = ctxNow();
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
718
|
+
const press = {
|
|
719
|
+
kind: "pointerdown",
|
|
720
|
+
x: w.x,
|
|
721
|
+
y: w.y,
|
|
722
|
+
clientX: h.clientX,
|
|
723
|
+
clientY: h.clientY,
|
|
724
|
+
...where
|
|
725
|
+
};
|
|
726
|
+
const d = dispatcherNow();
|
|
727
|
+
publishLiveDispatch(() => d.explain(press, ctx));
|
|
728
|
+
const dragCursor = d.resolveOnly(press, ctx)?.action.cursor;
|
|
678
729
|
const cursor = dragCursor ?? dispatcherNow().resolveOnly(
|
|
679
730
|
{ kind: "click", x: w.x, y: w.y, pressX: w.x, pressY: w.y, ...where },
|
|
680
731
|
ctx
|
|
@@ -700,6 +751,7 @@ function useGestureDispatcher(opts) {
|
|
|
700
751
|
};
|
|
701
752
|
const onHoverPointerLeave = () => {
|
|
702
753
|
lastHover = null;
|
|
754
|
+
publishLiveDispatch(null);
|
|
703
755
|
paintedCursorRef.current?.()?.clearPointer();
|
|
704
756
|
clearHoverCursor();
|
|
705
757
|
};
|
|
@@ -912,9 +964,10 @@ function useGestureDispatcher(opts) {
|
|
|
912
964
|
});
|
|
913
965
|
if (result === "handled") e.preventDefault();
|
|
914
966
|
};
|
|
967
|
+
const keyCapture = keyboard === "first";
|
|
915
968
|
if (keyboard) {
|
|
916
|
-
window.addEventListener("keydown", onKeyDown);
|
|
917
|
-
window.addEventListener("keyup", onKeyUp);
|
|
969
|
+
window.addEventListener("keydown", onKeyDown, keyCapture);
|
|
970
|
+
window.addEventListener("keyup", onKeyUp, keyCapture);
|
|
918
971
|
}
|
|
919
972
|
window.addEventListener("blur", onWindowBlur);
|
|
920
973
|
window.addEventListener("keydown", scheduleHoverCursorRefresh);
|
|
@@ -940,8 +993,8 @@ function useGestureDispatcher(opts) {
|
|
|
940
993
|
}
|
|
941
994
|
return () => {
|
|
942
995
|
if (keyboard) {
|
|
943
|
-
window.removeEventListener("keydown", onKeyDown);
|
|
944
|
-
window.removeEventListener("keyup", onKeyUp);
|
|
996
|
+
window.removeEventListener("keydown", onKeyDown, keyCapture);
|
|
997
|
+
window.removeEventListener("keyup", onKeyUp, keyCapture);
|
|
945
998
|
}
|
|
946
999
|
window.removeEventListener("blur", onWindowBlur);
|
|
947
1000
|
window.removeEventListener("keydown", scheduleHoverCursorRefresh);
|
|
@@ -967,206 +1020,172 @@ function useGestureDispatcher(opts) {
|
|
|
967
1020
|
held.clear();
|
|
968
1021
|
for (const d of allDispatchers()) d.cancelAll("cancel");
|
|
969
1022
|
};
|
|
970
|
-
}, [
|
|
1023
|
+
}, [
|
|
1024
|
+
enabled,
|
|
1025
|
+
keyboard,
|
|
1026
|
+
canvasRef,
|
|
1027
|
+
pointerChannel,
|
|
1028
|
+
wheelChannel,
|
|
1029
|
+
pinchChannel,
|
|
1030
|
+
contextMenuChannel,
|
|
1031
|
+
ingestChannel,
|
|
1032
|
+
allDispatchers,
|
|
1033
|
+
ctxRef,
|
|
1034
|
+
longPressRef,
|
|
1035
|
+
onDoubleClickRef,
|
|
1036
|
+
paintedCursorRef,
|
|
1037
|
+
registryRef,
|
|
1038
|
+
requestRedrawRef,
|
|
1039
|
+
rootTargetRef,
|
|
1040
|
+
viewsRef
|
|
1041
|
+
]);
|
|
971
1042
|
}
|
|
972
1043
|
var ActionsContext = createContext(null);
|
|
973
|
-
function
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1044
|
+
function depsFor(a, r) {
|
|
1045
|
+
if (!r) return {};
|
|
1046
|
+
if (a.requires) return buildDepsFromRequires(a, r);
|
|
1047
|
+
return {
|
|
1048
|
+
selection: r.get("selection"),
|
|
1049
|
+
scene: r.get("scene"),
|
|
1050
|
+
history: r.get("history"),
|
|
1051
|
+
view: r.get("view"),
|
|
1052
|
+
activeTool: r.get("activeTool"),
|
|
1053
|
+
booleansAdapter: r.get("booleansAdapter")
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
var ActionsNode = class extends ScopeNode {
|
|
1057
|
+
constructor(parent, descends, contextDeps) {
|
|
1058
|
+
super(parent, descends);
|
|
1059
|
+
this.contextDeps = contextDeps;
|
|
1060
|
+
NODES.set(this.registry, this);
|
|
1061
|
+
}
|
|
1062
|
+
contextDeps;
|
|
1063
|
+
actions = /* @__PURE__ */ new Map();
|
|
1064
|
+
muted = /* @__PURE__ */ new Map();
|
|
1065
|
+
dispatchers = [];
|
|
1066
|
+
cache = null;
|
|
1067
|
+
/** Called on `activate`, so a scope's dep and tool nodes move with it. */
|
|
1068
|
+
onActivate = [];
|
|
1069
|
+
resolve(id) {
|
|
1070
|
+
for (const n of this.chain()) {
|
|
1071
|
+
if (n.muted.has(id)) return void 0;
|
|
1072
|
+
const a = n.actions.get(id)?.at(-1);
|
|
1073
|
+
if (a) return a;
|
|
1074
|
+
}
|
|
1075
|
+
return void 0;
|
|
1076
|
+
}
|
|
1077
|
+
snapshot() {
|
|
1078
|
+
const leaf = this.start();
|
|
1079
|
+
const cached = this.cache;
|
|
1080
|
+
if (cached && cached.version === this.tree.version && cached.leaf === leaf) return cached.list;
|
|
1081
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1082
|
+
for (const n of leaf.chain()) for (const id of n.actions.keys()) ids.add(id);
|
|
1083
|
+
const out = [];
|
|
1084
|
+
for (const id of ids) {
|
|
1085
|
+
const a = leaf.resolve(id);
|
|
1086
|
+
if (a) out.push(a);
|
|
1087
|
+
}
|
|
1088
|
+
const list = Object.freeze(out);
|
|
1089
|
+
this.cache = { version: this.tree.version, leaf, list };
|
|
1090
|
+
return list;
|
|
1091
|
+
}
|
|
1092
|
+
deps() {
|
|
1093
|
+
for (const n of this.chain()) {
|
|
1094
|
+
const r = n.contextDeps();
|
|
1095
|
+
if (r) return r;
|
|
1096
|
+
}
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
dispatcher() {
|
|
1100
|
+
for (const n of this.chain()) {
|
|
1101
|
+
const d = n.dispatchers.at(-1);
|
|
1102
|
+
if (d) return d;
|
|
1103
|
+
}
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
activate() {
|
|
1107
|
+
super.activate();
|
|
1108
|
+
for (const f of this.onActivate) f();
|
|
1109
|
+
}
|
|
1110
|
+
registry = {
|
|
1111
|
+
register: (action) => {
|
|
1112
|
+
validateActionId(action.id);
|
|
1113
|
+
validateActionDefaultBinding(action);
|
|
1114
|
+
const stack = this.actions.get(action.id);
|
|
1115
|
+
if (stack) stack.push(action);
|
|
1116
|
+
else this.actions.set(action.id, [action]);
|
|
1117
|
+
this.changed();
|
|
1118
|
+
let released = false;
|
|
1119
|
+
return () => {
|
|
1120
|
+
if (released) return;
|
|
1121
|
+
released = true;
|
|
1122
|
+
const cur = this.actions.get(action.id);
|
|
1123
|
+
if (!cur) return;
|
|
1124
|
+
const i = cur.lastIndexOf(action);
|
|
1125
|
+
if (i === -1) return;
|
|
1126
|
+
cur.splice(i, 1);
|
|
1127
|
+
if (cur.length === 0) this.actions.delete(action.id);
|
|
1128
|
+
this.changed();
|
|
1129
|
+
};
|
|
1130
|
+
},
|
|
1131
|
+
unregister: (id) => {
|
|
1132
|
+
if (this.actions.delete(id)) this.changed();
|
|
1133
|
+
},
|
|
1134
|
+
mute: (id) => {
|
|
1135
|
+
this.muted.set(id, (this.muted.get(id) ?? 0) + 1);
|
|
1136
|
+
this.changed();
|
|
1137
|
+
let released = false;
|
|
1138
|
+
return () => {
|
|
1139
|
+
if (released) return;
|
|
1140
|
+
released = true;
|
|
1141
|
+
const held = this.muted.get(id);
|
|
1142
|
+
if (held === void 0) return;
|
|
1143
|
+
if (held > 1) this.muted.set(id, held - 1);
|
|
1144
|
+
else this.muted.delete(id);
|
|
1145
|
+
this.changed();
|
|
1146
|
+
};
|
|
1147
|
+
},
|
|
1148
|
+
list: () => this.snapshot(),
|
|
1149
|
+
trigger: (id, params) => {
|
|
1150
|
+
const leaf = this.start();
|
|
1151
|
+
const a = leaf.resolve(id);
|
|
1152
|
+
if (!a) return false;
|
|
1153
|
+
try {
|
|
1154
|
+
if (a.invoker && a.invoker.timing === "immediate") {
|
|
1155
|
+
a.invoker.run(depsFor(a, leaf.deps()), params);
|
|
986
1156
|
}
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
|
|
987
1159
|
}
|
|
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);
|
|
1160
|
+
return true;
|
|
1161
|
+
},
|
|
1162
|
+
subscribe: (listener) => {
|
|
1163
|
+
this.tree.listeners.add(listener);
|
|
1164
|
+
return () => {
|
|
1165
|
+
this.tree.listeners.delete(listener);
|
|
1166
|
+
};
|
|
1167
|
+
},
|
|
1168
|
+
setDispatcher: (d) => pushOwner(this.dispatchers, d),
|
|
1169
|
+
begin: (id, params) => {
|
|
1170
|
+
const leaf = this.start();
|
|
1171
|
+
const a = leaf.resolve(id);
|
|
1172
|
+
const disp = leaf.dispatcher();
|
|
1173
|
+
if (!a || !disp) return null;
|
|
1174
|
+
return disp.beginUiOngoing(id, depsFor(a, leaf.deps()), params);
|
|
1175
|
+
},
|
|
1176
|
+
activate: () => this.activate(),
|
|
1177
|
+
isActive: () => this.isActive()
|
|
1045
1178
|
};
|
|
1179
|
+
};
|
|
1180
|
+
var NODES = /* @__PURE__ */ new WeakMap();
|
|
1181
|
+
function actionsNodeOf(registry) {
|
|
1182
|
+
return registry ? NODES.get(registry) ?? null : null;
|
|
1046
1183
|
}
|
|
1047
1184
|
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
1185
|
const depReg = useOptionalDepRegistry();
|
|
1055
|
-
const depRegRef =
|
|
1056
|
-
depRegRef.current
|
|
1057
|
-
|
|
1058
|
-
const wiredDepRegRef = useRef([]);
|
|
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
|
-
);
|
|
1186
|
+
const depRegRef = useLatest(depReg);
|
|
1187
|
+
const node = useMemo(() => new ActionsNode(null, true, () => depRegRef.current), [depRegRef]);
|
|
1188
|
+
return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: node.registry, children });
|
|
1170
1189
|
}
|
|
1171
1190
|
var IS_DEV = (() => {
|
|
1172
1191
|
try {
|
|
@@ -1192,12 +1211,45 @@ function useAction(action) {
|
|
|
1192
1211
|
return reg.register(action);
|
|
1193
1212
|
}, [reg, action]);
|
|
1194
1213
|
}
|
|
1214
|
+
function useNodes(descends, ownsTool, parent) {
|
|
1215
|
+
const parentActions = useActionsRegistry();
|
|
1216
|
+
const parentDeps = useOptionalDepRegistry();
|
|
1217
|
+
const parentTools = useContext(ActiveToolNodeContext);
|
|
1218
|
+
const pa = parent ? parent.actions : actionsNodeOf(parentActions);
|
|
1219
|
+
const pd = parent ? parent.deps : depNodeOf(parentDeps);
|
|
1220
|
+
const pt = parent ? parent.tools : parentTools;
|
|
1221
|
+
const nodes = useMemo(() => {
|
|
1222
|
+
const deps = new DepNode(pd, descends);
|
|
1223
|
+
const tools = new ToolNode(pt, descends, ownsTool, null);
|
|
1224
|
+
const actions = new ActionsNode(pa, descends, () => deps.registry);
|
|
1225
|
+
actions.onActivate.push(() => deps.activate(), () => tools.activate());
|
|
1226
|
+
return { actions, deps, tools };
|
|
1227
|
+
}, [pa, pd, pt, descends, ownsTool]);
|
|
1228
|
+
useEffect(() => {
|
|
1229
|
+
const leave = [nodes.deps.mount(), nodes.tools.mount(), nodes.actions.mount()];
|
|
1230
|
+
return () => {
|
|
1231
|
+
for (const f of leave) f();
|
|
1232
|
+
};
|
|
1233
|
+
}, [nodes]);
|
|
1234
|
+
return nodes;
|
|
1235
|
+
}
|
|
1236
|
+
function Provide({ nodes, children }) {
|
|
1237
|
+
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 }) }) });
|
|
1238
|
+
}
|
|
1239
|
+
function InputScope({ yoke, children }) {
|
|
1240
|
+
const nodes = useNodes(false, yoke === void 0, yoke);
|
|
1241
|
+
return /* @__PURE__ */ jsx(Provide, { nodes, children });
|
|
1242
|
+
}
|
|
1243
|
+
function useYoke() {
|
|
1244
|
+
return useNodes(true, true, void 0);
|
|
1245
|
+
}
|
|
1246
|
+
function Yoke({ value, children }) {
|
|
1247
|
+
return /* @__PURE__ */ jsx(Provide, { nodes: value, children });
|
|
1248
|
+
}
|
|
1195
1249
|
function useOngoingAction(actionId) {
|
|
1196
1250
|
const reg = useActionsRegistry();
|
|
1197
|
-
const regRef =
|
|
1198
|
-
|
|
1199
|
-
const idRef = useRef(actionId);
|
|
1200
|
-
idRef.current = actionId;
|
|
1251
|
+
const regRef = useLatest(reg);
|
|
1252
|
+
const idRef = useLatest(actionId);
|
|
1201
1253
|
const ctrlRef = useRef(null);
|
|
1202
1254
|
const edit = useMemo(() => {
|
|
1203
1255
|
const end = (reason) => {
|
|
@@ -1221,51 +1273,43 @@ function useOngoingAction(actionId) {
|
|
|
1221
1273
|
end("cancel");
|
|
1222
1274
|
}
|
|
1223
1275
|
};
|
|
1224
|
-
}, []);
|
|
1276
|
+
}, [regRef, idRef]);
|
|
1225
1277
|
useEffect(() => () => edit.commit(), [edit, actionId]);
|
|
1226
1278
|
return edit;
|
|
1227
1279
|
}
|
|
1228
1280
|
function useContributions(opts) {
|
|
1229
1281
|
const ctx = useActiveToolContext();
|
|
1230
1282
|
const actionsRegistry = useActionsRegistry();
|
|
1231
|
-
const
|
|
1232
|
-
const isFirstRender =
|
|
1233
|
-
|
|
1234
|
-
if (isFirstRender) {
|
|
1235
|
-
hasInitializedRef.current = true;
|
|
1236
|
-
needsFirstMountSyncRef.current = ctx.active === "select" && opts.focused !== "select";
|
|
1237
|
-
}
|
|
1283
|
+
const firstMountRef = useRef(null);
|
|
1284
|
+
const isFirstRender = firstMountRef.current == null;
|
|
1285
|
+
if (firstMountRef.current == null) firstMountRef.current = { sync: ctx.active === null && opts.focused !== null };
|
|
1238
1286
|
useEffect(() => {
|
|
1239
|
-
|
|
1240
|
-
|
|
1287
|
+
const first = firstMountRef.current;
|
|
1288
|
+
if (!first?.sync) return;
|
|
1289
|
+
first.sync = false;
|
|
1241
1290
|
ctx.setActive(opts.focused);
|
|
1242
1291
|
}, []);
|
|
1243
|
-
const focused = isFirstRender && ctx.active ===
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1246
|
-
const focusedRef = useRef(focused);
|
|
1247
|
-
focusedRef.current = focused;
|
|
1248
|
-
const engagedRef = useRef(ctx.hotkeyStack);
|
|
1249
|
-
engagedRef.current = ctx.hotkeyStack;
|
|
1292
|
+
const focused = isFirstRender && ctx.active === null && opts.focused !== null ? opts.focused : ctx.active;
|
|
1293
|
+
const entries = useStableByContent(opts.entries, sameList);
|
|
1294
|
+
const engaged = ctx.hotkeyStack;
|
|
1250
1295
|
const setFocused = useCallback((id) => {
|
|
1251
1296
|
ctx.setActive(id);
|
|
1252
1297
|
}, [ctx]);
|
|
1253
1298
|
const snapshot = useCallback(() => {
|
|
1254
|
-
const engaged = engagedRef.current;
|
|
1255
1299
|
const engagedIds = new Set(engaged);
|
|
1256
1300
|
const ordered = [];
|
|
1257
1301
|
for (const id of engaged) {
|
|
1258
|
-
const entry =
|
|
1302
|
+
const entry = entries.find((e) => e.id === id);
|
|
1259
1303
|
if (entry && !ordered.includes(entry)) ordered.push(entry);
|
|
1260
1304
|
}
|
|
1261
|
-
for (const entry of
|
|
1305
|
+
for (const entry of entries) {
|
|
1262
1306
|
if (!ordered.includes(entry)) ordered.push(entry);
|
|
1263
1307
|
}
|
|
1264
1308
|
return {
|
|
1265
1309
|
ordered,
|
|
1266
|
-
state: { focusedId:
|
|
1310
|
+
state: { focusedId: focused, engagedIds }
|
|
1267
1311
|
};
|
|
1268
|
-
}, []);
|
|
1312
|
+
}, [entries, focused, engaged]);
|
|
1269
1313
|
const scopedBindings = useCallback(() => {
|
|
1270
1314
|
const { ordered, state } = snapshot();
|
|
1271
1315
|
return scopeBindings(ordered, state);
|
|
@@ -1286,48 +1330,43 @@ function useContributions(opts) {
|
|
|
1286
1330
|
}
|
|
1287
1331
|
return [...active, ...hotkey, ...ambient];
|
|
1288
1332
|
}, [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]);
|
|
1333
|
+
useOffhandAction(entries);
|
|
1298
1334
|
const lastConflictSigRef = useRef(null);
|
|
1299
|
-
const entrySig =
|
|
1335
|
+
const entrySig = entries.map((e) => e.id).join(",");
|
|
1336
|
+
const modesRegistry = opts.modes;
|
|
1300
1337
|
useEffect(() => {
|
|
1301
1338
|
if (!isDev()) return;
|
|
1302
|
-
|
|
1303
|
-
|
|
1339
|
+
const last = lastConflictSigRef.current;
|
|
1340
|
+
if (last?.sig === entrySig && last.modes === modesRegistry) return;
|
|
1341
|
+
lastConflictSigRef.current = { sig: entrySig, modes: modesRegistry };
|
|
1304
1342
|
const registry = [];
|
|
1305
1343
|
const ambient = [];
|
|
1306
|
-
for (const entry of
|
|
1344
|
+
for (const entry of entries) {
|
|
1307
1345
|
if (entry.eligibility?.focus) registry.push(entry);
|
|
1308
1346
|
if (entry.eligibility?.always || entry.eligibility?.claimed) ambient.push(entry);
|
|
1309
1347
|
}
|
|
1310
1348
|
reportRouteConflicts({
|
|
1311
1349
|
registry,
|
|
1312
1350
|
ambient,
|
|
1313
|
-
actions: actionsRegistry?.list() ?? []
|
|
1351
|
+
actions: actionsRegistry?.list() ?? [],
|
|
1352
|
+
...modesRegistry ? { modes: modesRegistry.list().map(activeModeOf) } : {}
|
|
1314
1353
|
});
|
|
1315
|
-
}, [entrySig, actionsRegistry]);
|
|
1354
|
+
}, [entrySig, entries, actionsRegistry, modesRegistry]);
|
|
1316
1355
|
return useMemo(
|
|
1317
|
-
() => ({
|
|
1318
|
-
|
|
1319
|
-
return entriesRef.current;
|
|
1320
|
-
},
|
|
1321
|
-
focused,
|
|
1322
|
-
setFocused,
|
|
1323
|
-
scopedBindings,
|
|
1324
|
-
overlays
|
|
1325
|
-
}),
|
|
1326
|
-
[focused, setFocused, scopedBindings, overlays]
|
|
1356
|
+
() => ({ entries, focused, setFocused, scopedBindings, overlays }),
|
|
1357
|
+
[entries, focused, setFocused, scopedBindings, overlays]
|
|
1327
1358
|
);
|
|
1328
1359
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1360
|
+
function useOffhandAction(entries) {
|
|
1361
|
+
const actionsRegistry = useActionsRegistry();
|
|
1362
|
+
const offhandSig = JSON.stringify(entries.filter((e) => e.eligibility?.offhand).map((e) => [e.id, e.eligibility.offhand]));
|
|
1363
|
+
useEffect(() => {
|
|
1364
|
+
const pairs = JSON.parse(offhandSig);
|
|
1365
|
+
if (!actionsRegistry || pairs.length === 0) return;
|
|
1366
|
+
const specs = pairs.map(([toolId, trigger]) => ({ toolId, key: offhandKeyFor(trigger) }));
|
|
1367
|
+
return actionsRegistry.register(makeToolOffhandAction(buildToolOffhandBindings(specs)));
|
|
1368
|
+
}, [actionsRegistry, offhandSig]);
|
|
1369
|
+
}
|
|
1331
1370
|
function declareSlot(tool, slot) {
|
|
1332
1371
|
const hotkey = tool.def?.hotkey;
|
|
1333
1372
|
const eligibility = {
|
|
@@ -1337,6 +1376,18 @@ 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
|
+
return useStableByContent(
|
|
1382
|
+
{ registry, ambient: ambient ?? NO_AMBIENT },
|
|
1383
|
+
(a, b) => sameRecord(a.registry, b.registry) && sameList(a.ambient, b.ambient)
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
function sameRecord(a, b) {
|
|
1387
|
+
const ak = Object.keys(a);
|
|
1388
|
+
if (ak.length !== Object.keys(b).length) return false;
|
|
1389
|
+
return ak.every((k) => k in b && a[k] === b[k]);
|
|
1390
|
+
}
|
|
1340
1391
|
function sameEligibility(a, b) {
|
|
1341
1392
|
if (!a) return false;
|
|
1342
1393
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
@@ -1344,16 +1395,18 @@ function sameEligibility(a, b) {
|
|
|
1344
1395
|
return true;
|
|
1345
1396
|
}
|
|
1346
1397
|
function useTools(opts) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1398
|
+
const initialActive = opts.active ?? null;
|
|
1399
|
+
if (initialActive !== null && !(initialActive in opts.registry)) {
|
|
1400
|
+
throw new Error(`useTools: active "${initialActive}" not in registry`);
|
|
1349
1401
|
}
|
|
1350
1402
|
const ctx = useActiveToolContext();
|
|
1403
|
+
const sources = useStableSources(opts.registry, opts.ambient);
|
|
1351
1404
|
const slotted = useMemo(() => {
|
|
1352
1405
|
const registry = {};
|
|
1353
|
-
for (const [id, tool] of Object.entries(
|
|
1406
|
+
for (const [id, tool] of Object.entries(sources.registry)) {
|
|
1354
1407
|
registry[id] = declareSlot(tool, "focus");
|
|
1355
1408
|
}
|
|
1356
|
-
const ambient =
|
|
1409
|
+
const ambient = sources.ambient.map((t) => declareSlot(t, "always"));
|
|
1357
1410
|
const byId = /* @__PURE__ */ new Map();
|
|
1358
1411
|
for (const tool of [...Object.values(registry), ...ambient]) {
|
|
1359
1412
|
const prior = byId.get(tool.id);
|
|
@@ -1361,25 +1414,26 @@ function useTools(opts) {
|
|
|
1361
1414
|
else byId.set(tool.id, tool);
|
|
1362
1415
|
}
|
|
1363
1416
|
return { registry, ambient, entries: [...byId.values()] };
|
|
1364
|
-
}, [
|
|
1365
|
-
const contributions = useContributions({
|
|
1417
|
+
}, [sources]);
|
|
1418
|
+
const contributions = useContributions({
|
|
1419
|
+
entries: slotted.entries,
|
|
1420
|
+
focused: initialActive,
|
|
1421
|
+
...opts.modes ? { modes: opts.modes } : {}
|
|
1422
|
+
});
|
|
1366
1423
|
const hotkeyEngaged = ctx.hotkeyStack.at(-1) ?? null;
|
|
1367
|
-
const slottedRef =
|
|
1368
|
-
|
|
1369
|
-
const
|
|
1370
|
-
activeRef.current = contributions.focused;
|
|
1371
|
-
const hotkeyRef = useRef(hotkeyEngaged);
|
|
1372
|
-
hotkeyRef.current = hotkeyEngaged;
|
|
1424
|
+
const slottedRef = useLatest(slotted);
|
|
1425
|
+
const activeRef = useLatest(contributions.focused);
|
|
1426
|
+
const hotkeyRef = useLatest(hotkeyEngaged);
|
|
1373
1427
|
const setFocused = contributions.setFocused;
|
|
1374
1428
|
const setActive = useCallback(
|
|
1375
1429
|
(id) => {
|
|
1376
|
-
if (!(id in slottedRef.current.registry)) {
|
|
1430
|
+
if (id !== null && !(id in slottedRef.current.registry)) {
|
|
1377
1431
|
throw new Error(`setActive: "${id}" not in registry`);
|
|
1378
1432
|
}
|
|
1379
1433
|
dlog("tools", "active:", activeRef.current, "\u2192", id);
|
|
1380
1434
|
setFocused(id);
|
|
1381
1435
|
},
|
|
1382
|
-
[setFocused]
|
|
1436
|
+
[setFocused, slottedRef, activeRef]
|
|
1383
1437
|
);
|
|
1384
1438
|
const engageHotkey = useCallback(
|
|
1385
1439
|
(id) => {
|
|
@@ -1389,19 +1443,18 @@ function useTools(opts) {
|
|
|
1389
1443
|
dlog("tools", "hotkey engaged:", id);
|
|
1390
1444
|
ctx.pushHotkey(id);
|
|
1391
1445
|
},
|
|
1392
|
-
[ctx]
|
|
1446
|
+
[ctx, slottedRef]
|
|
1393
1447
|
);
|
|
1394
1448
|
const disengageHotkey = useCallback(() => {
|
|
1395
1449
|
if (hotkeyRef.current) dlog("tools", "hotkey disengaged:", hotkeyRef.current);
|
|
1396
1450
|
ctx.popHotkey();
|
|
1397
|
-
}, [ctx]);
|
|
1451
|
+
}, [ctx, hotkeyRef]);
|
|
1398
1452
|
const has = useCallback(
|
|
1399
|
-
(id) => id in
|
|
1400
|
-
[]
|
|
1453
|
+
(id) => id in slotted.registry || slotted.ambient.some((t) => t.id === id),
|
|
1454
|
+
[slotted]
|
|
1401
1455
|
);
|
|
1402
1456
|
const active = contributions.focused;
|
|
1403
1457
|
const getActiveOverlays = contributions.overlays;
|
|
1404
|
-
const registryKey = Object.keys(slotted.registry).sort().join("|") + "#" + slotted.ambient.map((t) => t.id).join("|");
|
|
1405
1458
|
return useMemo(
|
|
1406
1459
|
() => ({
|
|
1407
1460
|
active,
|
|
@@ -1409,18 +1462,15 @@ function useTools(opts) {
|
|
|
1409
1462
|
hotkeyEngaged,
|
|
1410
1463
|
engageHotkey,
|
|
1411
1464
|
disengageHotkey,
|
|
1412
|
-
ambient:
|
|
1413
|
-
registry:
|
|
1465
|
+
ambient: slotted.ambient,
|
|
1466
|
+
registry: slotted.registry,
|
|
1414
1467
|
has,
|
|
1415
1468
|
getActiveOverlays
|
|
1416
1469
|
}),
|
|
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]
|
|
1470
|
+
[active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays, slotted]
|
|
1421
1471
|
);
|
|
1422
1472
|
}
|
|
1423
1473
|
|
|
1424
|
-
export { ActionsProvider,
|
|
1474
|
+
export { ActionsProvider, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, InputScope, Yoke, useAction, useActionsRegistry, useActiveToolContext, useContributions, useDepRegistry, useDepSource, useGestureDispatcher, useOffhandAction, useOngoingAction, useOptionalActiveToolContext, useOptionalDepRegistry, useTools, useYoke };
|
|
1425
1475
|
//# sourceMappingURL=react.js.map
|
|
1426
1476
|
//# sourceMappingURL=react.js.map
|