@weasel-js/routing 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/chunk-Y5FHAA4U.js +1504 -0
- package/dist/chunk-Y5FHAA4U.js.map +1 -0
- package/dist/index-BOlkWrA-.d.ts +2446 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +405 -0
- package/dist/react.js +1304 -0
- package/dist/react.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,1504 @@
|
|
|
1
|
+
import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, parseTargetSpec, matchSpec, isKnownGestureName, getGestureDescriptor, formatRoute, canonicalModifiers } from '@weasel-js/gestures';
|
|
2
|
+
export { GESTURE_DESCRIPTORS, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, parseRoute } from '@weasel-js/gestures';
|
|
3
|
+
import { NORMAL, IMPLICIT_TAGS } from '@weasel-js/modes';
|
|
4
|
+
import { resolveCursor } from '@weasel-js/cursor';
|
|
5
|
+
|
|
6
|
+
// src/interactions/actions/binding.ts
|
|
7
|
+
function actionBindings(action) {
|
|
8
|
+
const gs = action.defaultBinding;
|
|
9
|
+
if (!gs) return [];
|
|
10
|
+
const raw = Array.isArray(gs) ? gs : [gs];
|
|
11
|
+
return raw.map((entry) => {
|
|
12
|
+
const isBoundObj = !("kind" in entry);
|
|
13
|
+
const spec = isBoundObj ? entry.spec : entry;
|
|
14
|
+
const opts = isBoundObj ? entry.opts : void 0;
|
|
15
|
+
return { spec, actionId: action.id, ...opts !== void 0 ? { opts } : {} };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/interactions/actions/buildDeps.ts
|
|
20
|
+
var DEV = (() => {
|
|
21
|
+
try {
|
|
22
|
+
return Boolean(import.meta.env?.DEV);
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
function buildDepsFromRequires(action, depRegistry) {
|
|
28
|
+
const requires = action.requires ?? [];
|
|
29
|
+
const entries = requires.map((name) => [name, depRegistry.get(name)]);
|
|
30
|
+
const deps = Object.fromEntries(entries);
|
|
31
|
+
if (DEV) {
|
|
32
|
+
const declared = new Set(requires);
|
|
33
|
+
return new Proxy(deps, {
|
|
34
|
+
get(target, prop, receiver) {
|
|
35
|
+
if (typeof prop === "string" && !declared.has(prop) && prop !== "then") {
|
|
36
|
+
const message = `[weasel:dispatcher] action "${action.id}" read deps.${prop} but did not declare it in \`requires\`. Add \`requires: [...'${prop}']\` to the descriptor or the dep will be undefined at runtime.`;
|
|
37
|
+
console.warn(message);
|
|
38
|
+
throw new Error(message);
|
|
39
|
+
}
|
|
40
|
+
return Reflect.get(target, prop, receiver);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return deps;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/interactions/actions/registry.ts
|
|
48
|
+
function validateActionId(id) {
|
|
49
|
+
if (id.length === 0) {
|
|
50
|
+
throw new Error(`weasel: action id may not be empty`);
|
|
51
|
+
}
|
|
52
|
+
if (RESERVED_ID_PREFIXES.has(id[0])) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`weasel: action id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (RESERVED_ID_NAMES.has(id)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`weasel: action id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function validateActionDefaultBinding(action) {
|
|
64
|
+
const gs = action.defaultBinding;
|
|
65
|
+
if (!gs) return;
|
|
66
|
+
const entries = Array.isArray(gs) ? gs : [gs];
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const spec = "kind" in entry ? entry : entry.spec;
|
|
69
|
+
const phase = spec.phase;
|
|
70
|
+
if (phase === void 0) continue;
|
|
71
|
+
const atoms = Array.isArray(phase) ? phase : [{ channel: "&", phase }];
|
|
72
|
+
for (const atom of atoms) {
|
|
73
|
+
if (atom.channel === "&") {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`weasel: action "${action.id}" defaultBinding uses '&' channel which has no owning tool to resolve to. Use a named tool channel (e.g. '[rect:engaged]') or '*' instead, or move this binding to the tool's own Tool.bindings.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/interactions/actions/invoker.ts
|
|
83
|
+
function resolveParams(params) {
|
|
84
|
+
if (params === void 0) return void 0;
|
|
85
|
+
return typeof params === "function" ? params() : params;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/interactions/actions/toolOffhand.ts
|
|
89
|
+
var TOOL_OFFHAND_ID = "tool.offhand";
|
|
90
|
+
function offhandKeyFor(trigger) {
|
|
91
|
+
switch (trigger) {
|
|
92
|
+
case "space":
|
|
93
|
+
return " ";
|
|
94
|
+
case "alt":
|
|
95
|
+
return "Alt";
|
|
96
|
+
case "ctrl":
|
|
97
|
+
return "Control";
|
|
98
|
+
case "meta":
|
|
99
|
+
return "Meta";
|
|
100
|
+
case "shift":
|
|
101
|
+
return "Shift";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function buildToolOffhandBindings(specs) {
|
|
105
|
+
return specs.map(({ toolId, key }) => ({
|
|
106
|
+
spec: { kind: "key-held", key },
|
|
107
|
+
opts: { params: { toolId } }
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
function makeToolOffhandAction(bindings) {
|
|
111
|
+
return {
|
|
112
|
+
id: TOOL_OFFHAND_ID,
|
|
113
|
+
label: "Engage tool (offhand)",
|
|
114
|
+
defaultBinding: bindings,
|
|
115
|
+
scope: "hotkey",
|
|
116
|
+
invoker: {
|
|
117
|
+
timing: "ongoing",
|
|
118
|
+
start: (ctx, opts) => {
|
|
119
|
+
const activeTool = ctx.deps.activeTool;
|
|
120
|
+
const toolId = resolveParams(opts?.params)?.toolId;
|
|
121
|
+
if (!activeTool || !toolId) return {};
|
|
122
|
+
activeTool.pushHotkey(toolId);
|
|
123
|
+
return {
|
|
124
|
+
onEnd: () => {
|
|
125
|
+
activeTool.popHotkey(toolId);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
requires: ["activeTool"]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/contributions/eligibility.ts
|
|
135
|
+
function liveScope(id, eligibility, state) {
|
|
136
|
+
const tags = eligibility.capabilities;
|
|
137
|
+
if (tags && tags.length > 0 && state.allows && !state.allows(tags)) return null;
|
|
138
|
+
if (state.engagedIds?.has(id)) return "hotkey";
|
|
139
|
+
if (eligibility.offhand && state.heldTriggers.has(eligibility.offhand)) return "hotkey";
|
|
140
|
+
if (eligibility.focus && state.focusedId === id) return "active";
|
|
141
|
+
if (eligibility.always || eligibility.claimed) return "ambient";
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/contributions/assemble.ts
|
|
146
|
+
function scopeBindings(entries, state) {
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
|
|
150
|
+
if (scope === null) continue;
|
|
151
|
+
for (const binding of entry.bindings ?? []) {
|
|
152
|
+
out.push({ binding, scope, ownerToolId: entry.id });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/devFlag.ts
|
|
159
|
+
function isDev() {
|
|
160
|
+
return typeof process !== "undefined" ? process.env?.NODE_ENV !== "production" : true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/interactions/dispatcher/matcher.ts
|
|
164
|
+
var SCOPE_PRIORITY = ["hotkey", "active", "ambient"];
|
|
165
|
+
function modsCount(mods) {
|
|
166
|
+
if (!mods) return 0;
|
|
167
|
+
let n = 0;
|
|
168
|
+
if (mods.alt === true) n++;
|
|
169
|
+
if (mods.ctrl === true) n++;
|
|
170
|
+
if (mods.meta === true) n++;
|
|
171
|
+
if (mods.mod === true) n++;
|
|
172
|
+
if (mods.shift === true) n++;
|
|
173
|
+
return n;
|
|
174
|
+
}
|
|
175
|
+
function targetRank(target) {
|
|
176
|
+
if (target === void 0) return 0;
|
|
177
|
+
const form = parseTargetSpec(target);
|
|
178
|
+
if (form === null) return 1;
|
|
179
|
+
switch (form.form) {
|
|
180
|
+
case "kind":
|
|
181
|
+
return form.requireSelected ? 3 : 2;
|
|
182
|
+
case "affordance":
|
|
183
|
+
return 2;
|
|
184
|
+
case "body":
|
|
185
|
+
return 1;
|
|
186
|
+
case "predicate":
|
|
187
|
+
return 1;
|
|
188
|
+
default: {
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function targetConsultsAffordance(specTarget) {
|
|
194
|
+
if (specTarget === void 0) return false;
|
|
195
|
+
const form = parseTargetSpec(specTarget);
|
|
196
|
+
if (form === null) return false;
|
|
197
|
+
switch (form.form) {
|
|
198
|
+
case "predicate":
|
|
199
|
+
return form.kindOf.readsAffordance !== false;
|
|
200
|
+
case "affordance":
|
|
201
|
+
return true;
|
|
202
|
+
case "body":
|
|
203
|
+
return false;
|
|
204
|
+
case "kind":
|
|
205
|
+
return false;
|
|
206
|
+
default: {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function specTargetOf(spec) {
|
|
212
|
+
return "target" in spec ? spec.target : void 0;
|
|
213
|
+
}
|
|
214
|
+
function claimOf(e) {
|
|
215
|
+
return "affordance" in e ? e.affordance : void 0;
|
|
216
|
+
}
|
|
217
|
+
function claimGestureOf(e) {
|
|
218
|
+
switch (e.kind) {
|
|
219
|
+
case "pointerdown":
|
|
220
|
+
case "click":
|
|
221
|
+
return "pointer";
|
|
222
|
+
case "doubleclick":
|
|
223
|
+
return "doubleClick";
|
|
224
|
+
case "contextmenu":
|
|
225
|
+
return "contextMenu";
|
|
226
|
+
case "longpress":
|
|
227
|
+
return "longPress";
|
|
228
|
+
case "wheel":
|
|
229
|
+
return "wheel";
|
|
230
|
+
default:
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function isExclusiveClaim(e) {
|
|
235
|
+
const claim = claimOf(e);
|
|
236
|
+
if (claim?.strength !== "exclusive") return false;
|
|
237
|
+
const gesture = claimGestureOf(e);
|
|
238
|
+
if (gesture === null) return false;
|
|
239
|
+
return claim.claimedKinds === void 0 || claim.claimedKinds.includes(gesture);
|
|
240
|
+
}
|
|
241
|
+
var warnedDeadClaims = /* @__PURE__ */ new Set();
|
|
242
|
+
function reportDeadClaim(owner, warn) {
|
|
243
|
+
if (!isDev()) return;
|
|
244
|
+
const key = String(owner);
|
|
245
|
+
if (warnedDeadClaims.has(key)) return;
|
|
246
|
+
warnedDeadClaims.add(key);
|
|
247
|
+
warn(
|
|
248
|
+
`[weasel] exclusive claim by "${key}" matched no binding: no \`affordance:\` or \`kindOf\` target resolved against it, so the press was dropped.`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
function phaseRank(spec) {
|
|
252
|
+
if (spec === void 0) return 0;
|
|
253
|
+
const atoms = typeof spec === "string" ? [{ channel: "&", phase: spec }] : spec;
|
|
254
|
+
if (atoms.length === 0) return 0;
|
|
255
|
+
let min = 2;
|
|
256
|
+
for (const a of atoms) {
|
|
257
|
+
const rank = (a.channel === "*" ? 0 : 1) + (a.phase === "*" ? 0 : 1);
|
|
258
|
+
if (rank < min) min = rank;
|
|
259
|
+
}
|
|
260
|
+
return min;
|
|
261
|
+
}
|
|
262
|
+
function specificity(spec) {
|
|
263
|
+
const t = targetRank(specTargetOf(spec));
|
|
264
|
+
const mods = "mods" in spec ? spec.mods : void 0;
|
|
265
|
+
const m = modsCount(mods);
|
|
266
|
+
const p = phaseRank("phase" in spec ? spec.phase : void 0);
|
|
267
|
+
const typed = (spec.kind === "drop" || spec.kind === "paste") && spec.types !== void 0 && spec.types.length > 0 ? 2 : 1;
|
|
268
|
+
return [t, m, p, typed];
|
|
269
|
+
}
|
|
270
|
+
function compareSpecificity(a, b) {
|
|
271
|
+
const sa = specificity(a);
|
|
272
|
+
const sb = specificity(b);
|
|
273
|
+
for (let i = 0; i < sa.length; i++) {
|
|
274
|
+
if (sa[i] !== sb[i]) return sb[i] - sa[i];
|
|
275
|
+
}
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
function matchBest(e, bindings, isMac, engagedChannels) {
|
|
279
|
+
const sorted = matchSorted(e, bindings, isMac, engagedChannels);
|
|
280
|
+
return sorted.length > 0 ? sorted[0] : null;
|
|
281
|
+
}
|
|
282
|
+
function matchSorted(e, bindings, isMac, engagedChannels, warn = (m) => console.warn(m)) {
|
|
283
|
+
const out = [];
|
|
284
|
+
const engaged = engagedChannels ?? EMPTY_ENGAGED;
|
|
285
|
+
const exclusive = isExclusiveClaim(e);
|
|
286
|
+
const pool = exclusive ? bindings.filter((sb) => targetConsultsAffordance(specTargetOf(sb.binding.spec))) : bindings;
|
|
287
|
+
for (const scope of SCOPE_PRIORITY) {
|
|
288
|
+
const scopeMatches = [];
|
|
289
|
+
for (const sb of pool) {
|
|
290
|
+
if (sb.scope !== scope) continue;
|
|
291
|
+
const phaseCtx = { selfChannel: sb.ownerToolId, engagedChannels: engaged };
|
|
292
|
+
if (matchSpec(e, sb.binding.spec, isMac, phaseCtx)) {
|
|
293
|
+
scopeMatches.push({ binding: sb.binding, scope, ownerToolId: sb.ownerToolId });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
|
|
297
|
+
out.push(...scopeMatches);
|
|
298
|
+
}
|
|
299
|
+
if (exclusive && out.length === 0 && bindings.length > 0) {
|
|
300
|
+
reportDeadClaim(claimOf(e)?.owner, warn);
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
|
|
305
|
+
|
|
306
|
+
// src/eligibility/deviceProfile.ts
|
|
307
|
+
var COARSE_TARGET_SCALE = 1.75;
|
|
308
|
+
function resolveDeviceProfile(detected, overrides) {
|
|
309
|
+
const coarsePointer = overrides?.coarsePointer ?? detected.coarsePointer;
|
|
310
|
+
const canHover = overrides?.canHover ?? detected.canHover;
|
|
311
|
+
const dpr = overrides?.dpr ?? detected.dpr;
|
|
312
|
+
const targetScale = overrides?.targetScale ?? (coarsePointer ? COARSE_TARGET_SCALE : 1);
|
|
313
|
+
return { coarsePointer, canHover, dpr, targetScale };
|
|
314
|
+
}
|
|
315
|
+
var DEFAULT_DEVICE_PROFILE = resolveDeviceProfile({
|
|
316
|
+
coarsePointer: false,
|
|
317
|
+
canHover: true,
|
|
318
|
+
dpr: 1
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// src/eligibility/rule.ts
|
|
322
|
+
var ALWAYS = { all: [] };
|
|
323
|
+
var NEVER = { any: [] };
|
|
324
|
+
function isAllRule(r) {
|
|
325
|
+
return typeof r === "object" && r !== null && "all" in r;
|
|
326
|
+
}
|
|
327
|
+
function isAnyRule(r) {
|
|
328
|
+
return typeof r === "object" && r !== null && "any" in r;
|
|
329
|
+
}
|
|
330
|
+
function isNotRule(r) {
|
|
331
|
+
return typeof r === "object" && r !== null && "not" in r;
|
|
332
|
+
}
|
|
333
|
+
function isWhenRule(r) {
|
|
334
|
+
return typeof r === "object" && r !== null && "when" in r;
|
|
335
|
+
}
|
|
336
|
+
function checkSelection(s, ctx) {
|
|
337
|
+
if (s.empty !== void 0 && ctx.selection.length === 0 !== s.empty) return false;
|
|
338
|
+
if (s.is !== void 0 && ctx.selection.length !== s.is) return false;
|
|
339
|
+
if (s.atLeast !== void 0 && ctx.selection.length < s.atLeast) return false;
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
function checkMode(m, ctx) {
|
|
343
|
+
if (typeof m === "string") return ctx.mode === m;
|
|
344
|
+
if ("not" in m) return ctx.mode !== m.not;
|
|
345
|
+
if ("in" in m) return m.in.includes(ctx.mode);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
function checkCapability(c, ctx) {
|
|
349
|
+
if (typeof c === "string") return ctx.allowedCapabilities.has(c);
|
|
350
|
+
if (Array.isArray(c)) return c.every((tag) => ctx.allowedCapabilities.has(tag));
|
|
351
|
+
if ("in" in c) return c.in.some((tag) => ctx.allowedCapabilities.has(tag));
|
|
352
|
+
if ("not" in c) return !ctx.allowedCapabilities.has(c.not);
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
function evaluateSelector(s, ctx) {
|
|
356
|
+
if (s.selection !== void 0 && !checkSelection(s.selection, ctx)) return false;
|
|
357
|
+
if (s.mode !== void 0 && !checkMode(s.mode, ctx)) return false;
|
|
358
|
+
if (s.capability !== void 0 && !checkCapability(s.capability, ctx)) return false;
|
|
359
|
+
if (s.gesturing !== void 0 && ctx.action.kind !== null !== s.gesturing) return false;
|
|
360
|
+
if (s.actionIs !== void 0 && ctx.action.kind !== s.actionIs) return false;
|
|
361
|
+
if (s.modifierHeld !== void 0 && !ctx.modifiers[s.modifierHeld]) return false;
|
|
362
|
+
if (s.focused !== void 0 && ctx.focused !== s.focused) return false;
|
|
363
|
+
if (s.hovering !== void 0 && ctx.hover !== null !== s.hovering) return false;
|
|
364
|
+
if (s.hoveringSelected !== void 0) {
|
|
365
|
+
const isHovSel = ctx.hover !== null && ctx.selection.includes(ctx.hover);
|
|
366
|
+
if (isHovSel !== s.hoveringSelected) return false;
|
|
367
|
+
}
|
|
368
|
+
if (s.editingAnchors !== void 0 && (ctx.editingAnchors ?? false) !== s.editingAnchors) return false;
|
|
369
|
+
if (s.zoomAtLeast !== void 0) {
|
|
370
|
+
if (ctx.zoom === void 0 || ctx.zoom < s.zoomAtLeast) return false;
|
|
371
|
+
}
|
|
372
|
+
if (s.resizable !== void 0) {
|
|
373
|
+
const resizable = ctx.selectionResizable ?? true;
|
|
374
|
+
if (resizable !== s.resizable) return false;
|
|
375
|
+
}
|
|
376
|
+
if (s.coarsePointer !== void 0) {
|
|
377
|
+
const device = ctx.device ?? DEFAULT_DEVICE_PROFILE;
|
|
378
|
+
if (device.coarsePointer !== s.coarsePointer) return false;
|
|
379
|
+
}
|
|
380
|
+
if (s.canHover !== void 0) {
|
|
381
|
+
const device = ctx.device ?? DEFAULT_DEVICE_PROFILE;
|
|
382
|
+
if (device.canHover !== s.canHover) return false;
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
var MAX_RULE_DEPTH = 10;
|
|
387
|
+
function describeValue(value, depth) {
|
|
388
|
+
if (depth > MAX_RULE_DEPTH) return "\u2026";
|
|
389
|
+
if (value === null || typeof value !== "object") return String(value);
|
|
390
|
+
if (Array.isArray(value)) return value.map((v) => describeValue(v, depth + 1)).join("+");
|
|
391
|
+
const obj = value;
|
|
392
|
+
const keys = Object.keys(obj);
|
|
393
|
+
if (keys.length === 1 && keys[0] === "not") return `not(${describeValue(obj.not, depth + 1)})`;
|
|
394
|
+
if (keys.length === 1 && keys[0] === "in") return `in(${describeValue(obj.in, depth + 1)})`;
|
|
395
|
+
return keys.map((k) => `${k}=${describeValue(obj[k], depth + 1)}`).join(",");
|
|
396
|
+
}
|
|
397
|
+
function describeSelector(s, depth) {
|
|
398
|
+
const keys = Object.keys(s);
|
|
399
|
+
if (keys.length === 0) return "*";
|
|
400
|
+
return keys.map((k) => `${k}:${describeValue(s[k], depth + 1)}`).join(" & ");
|
|
401
|
+
}
|
|
402
|
+
function describeAt(rule, depth) {
|
|
403
|
+
if (depth > MAX_RULE_DEPTH) return "\u2026";
|
|
404
|
+
if (isAllRule(rule)) return `all(${rule.all.map((r) => describeAt(r, depth + 1)).join(", ")})`;
|
|
405
|
+
if (isAnyRule(rule)) return `any(${rule.any.map((r) => describeAt(r, depth + 1)).join(", ")})`;
|
|
406
|
+
if (isNotRule(rule)) return `not(${describeAt(rule.not, depth + 1)})`;
|
|
407
|
+
if (isWhenRule(rule)) return `when(${rule.when.name || "\u0192"})`;
|
|
408
|
+
return describeSelector(rule, depth);
|
|
409
|
+
}
|
|
410
|
+
function describeRule(rule) {
|
|
411
|
+
return describeAt(rule, 0);
|
|
412
|
+
}
|
|
413
|
+
function evaluate(rule, ctx) {
|
|
414
|
+
if (isAllRule(rule)) return rule.all.every((r) => evaluate(r, ctx));
|
|
415
|
+
if (isAnyRule(rule)) return rule.any.some((r) => evaluate(r, ctx));
|
|
416
|
+
if (isNotRule(rule)) return !evaluate(rule.not, ctx);
|
|
417
|
+
if (isWhenRule(rule)) return rule.when(ctx);
|
|
418
|
+
return evaluateSelector(rule, ctx);
|
|
419
|
+
}
|
|
420
|
+
var DEFAULT_ALLOWED_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
421
|
+
...NORMAL.allows,
|
|
422
|
+
...IMPLICIT_TAGS
|
|
423
|
+
]);
|
|
424
|
+
function buildRuleCtx(args) {
|
|
425
|
+
return {
|
|
426
|
+
focused: args.focused,
|
|
427
|
+
selection: args.selection,
|
|
428
|
+
multiActive: args.multiActive,
|
|
429
|
+
modifiers: args.modifiers,
|
|
430
|
+
action: args.action,
|
|
431
|
+
hover: args.hover,
|
|
432
|
+
zoom: args.zoom,
|
|
433
|
+
mode: args.mode,
|
|
434
|
+
allowedCapabilities: args.allowedCapabilities,
|
|
435
|
+
selectionResizable: args.selectionResizable,
|
|
436
|
+
editingAnchors: args.editingAnchors,
|
|
437
|
+
device: args.device
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function pointerGestureId(pointerId) {
|
|
441
|
+
return `pointer-${pointerId ?? "mouse"}`;
|
|
442
|
+
}
|
|
443
|
+
var DEV2 = (() => {
|
|
444
|
+
try {
|
|
445
|
+
return Boolean(import.meta.env?.DEV);
|
|
446
|
+
} catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
})();
|
|
450
|
+
var TRACE_LIMIT = 200;
|
|
451
|
+
var traceLog = DEV2 && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
|
|
452
|
+
function recordTrace(entry) {
|
|
453
|
+
if (!DEV2) return;
|
|
454
|
+
traceLog.push(entry);
|
|
455
|
+
if (traceLog.length > TRACE_LIMIT) traceLog.shift();
|
|
456
|
+
}
|
|
457
|
+
function recordModeSwitch(mode, from, to, detail) {
|
|
458
|
+
recordTrace({
|
|
459
|
+
kind: "mode",
|
|
460
|
+
ts: Date.now(),
|
|
461
|
+
mode,
|
|
462
|
+
from,
|
|
463
|
+
to,
|
|
464
|
+
...detail !== void 0 ? { detail } : {}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function eligibleToRule(eligible) {
|
|
468
|
+
return typeof eligible === "function" ? eligible.rule : eligible;
|
|
469
|
+
}
|
|
470
|
+
function isEligible(action, ruleCtx) {
|
|
471
|
+
if (!action.eligible) return true;
|
|
472
|
+
return evaluate(eligibleToRule(action.eligible), ruleCtx);
|
|
473
|
+
}
|
|
474
|
+
function describeEligible(eligible) {
|
|
475
|
+
return describeRule(eligibleToRule(eligible));
|
|
476
|
+
}
|
|
477
|
+
function filterEligible(matches, actionLookup, ruleCtx) {
|
|
478
|
+
return matches.filter((m) => {
|
|
479
|
+
const action = actionLookup(m.binding.actionId);
|
|
480
|
+
if (!action) return true;
|
|
481
|
+
return isEligible(action, ruleCtx);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
var EMPTY_TRIGGERS = /* @__PURE__ */ new Set();
|
|
485
|
+
var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
|
|
486
|
+
function modifiersOf(e) {
|
|
487
|
+
return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
|
|
488
|
+
}
|
|
489
|
+
function createDispatcher(opts) {
|
|
490
|
+
const inFlightHandles = /* @__PURE__ */ new Map();
|
|
491
|
+
const inFlightOwners = /* @__PURE__ */ new Map();
|
|
492
|
+
const inFlightActions = /* @__PURE__ */ new Map();
|
|
493
|
+
const dragOrigins = /* @__PURE__ */ new Map();
|
|
494
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
495
|
+
let version = 0;
|
|
496
|
+
function notify() {
|
|
497
|
+
version++;
|
|
498
|
+
for (const fn of subscribers) fn();
|
|
499
|
+
}
|
|
500
|
+
const dragPoints = /* @__PURE__ */ new Map();
|
|
501
|
+
const pinchStartSpreads = /* @__PURE__ */ new Map();
|
|
502
|
+
let uiOngoingSeq = 0;
|
|
503
|
+
const uiOngoingByAction = /* @__PURE__ */ new Map();
|
|
504
|
+
function isEmptyOngoingHandle(h) {
|
|
505
|
+
return !h.onMove && !h.onEnd && !h.overlay && !h.previewIds && !h.previewPose;
|
|
506
|
+
}
|
|
507
|
+
function buildUiInvocationCtx(deps, params) {
|
|
508
|
+
return {
|
|
509
|
+
world: { x: 0, y: 0 },
|
|
510
|
+
modifiers: { alt: false, ctrl: false, meta: false, shift: false },
|
|
511
|
+
deps,
|
|
512
|
+
...params !== void 0 ? { params } : {}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function buildInvocationCtx(event, deps, gestureId) {
|
|
516
|
+
const modifiers = {
|
|
517
|
+
alt: event.altKey,
|
|
518
|
+
ctrl: event.ctrlKey,
|
|
519
|
+
meta: event.metaKey,
|
|
520
|
+
shift: event.shiftKey
|
|
521
|
+
};
|
|
522
|
+
const base = {
|
|
523
|
+
world: { x: 0, y: 0 },
|
|
524
|
+
modifiers,
|
|
525
|
+
deps
|
|
526
|
+
};
|
|
527
|
+
const clientPoint = (ev) => {
|
|
528
|
+
const cx = ev.clientX;
|
|
529
|
+
const cy = ev.clientY;
|
|
530
|
+
return cx !== void 0 && cy !== void 0 ? { x: cx, y: cy } : void 0;
|
|
531
|
+
};
|
|
532
|
+
const screen = clientPoint(event);
|
|
533
|
+
if (screen) base.screen = screen;
|
|
534
|
+
if (event.kind === "key") {
|
|
535
|
+
base.key = { key: event.key, repeat: event.repeat ?? false };
|
|
536
|
+
} else if (event.kind === "key-held") {
|
|
537
|
+
base.key = { key: event.key, repeat: false };
|
|
538
|
+
} else if (event.kind === "wheel") {
|
|
539
|
+
base.wheel = { deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: 0 };
|
|
540
|
+
} else if (event.kind === "pointerdown" || event.kind === "click") {
|
|
541
|
+
const sx = event.kind === "pointerdown" ? event.x ?? 0 : event.worldX ?? event.pressX ?? 0;
|
|
542
|
+
const sy = event.kind === "pointerdown" ? event.y ?? 0 : event.worldY ?? event.pressY ?? 0;
|
|
543
|
+
base.world = { x: sx, y: sy };
|
|
544
|
+
const affordance = event.kind === "pointerdown" ? event.affordance : void 0;
|
|
545
|
+
base.drag = {
|
|
546
|
+
start: { x: sx, y: sy },
|
|
547
|
+
current: { x: sx, y: sy },
|
|
548
|
+
delta: { x: 0, y: 0 },
|
|
549
|
+
...affordance !== void 0 ? { affordance } : {}
|
|
550
|
+
};
|
|
551
|
+
} else if (event.kind === "pointermove" || event.kind === "pointerup") {
|
|
552
|
+
const cx = event.x;
|
|
553
|
+
const cy = event.y;
|
|
554
|
+
base.world = { x: cx, y: cy };
|
|
555
|
+
const origin = gestureId ? dragOrigins.get(gestureId) : void 0;
|
|
556
|
+
const ox = origin?.x ?? cx;
|
|
557
|
+
const oy = origin?.y ?? cy;
|
|
558
|
+
const points = gestureId ? dragPoints.get(gestureId) : void 0;
|
|
559
|
+
const eventClientX = event.clientX;
|
|
560
|
+
const eventClientY = event.clientY;
|
|
561
|
+
const hasClient = eventClientX !== void 0 && eventClientY !== void 0 && origin?.clientX !== void 0 && origin?.clientY !== void 0;
|
|
562
|
+
base.drag = {
|
|
563
|
+
start: { x: ox, y: oy },
|
|
564
|
+
current: { x: cx, y: cy },
|
|
565
|
+
delta: { x: cx - ox, y: cy - oy },
|
|
566
|
+
...hasClient ? { screenDelta: { x: eventClientX - origin.clientX, y: eventClientY - origin.clientY } } : {},
|
|
567
|
+
...points !== void 0 ? { points } : {}
|
|
568
|
+
};
|
|
569
|
+
} else if (event.kind === "multitouch") {
|
|
570
|
+
const centroid = event.centroid ?? { x: 0, y: 0 };
|
|
571
|
+
const spread = event.spread ?? 1;
|
|
572
|
+
const startSpread = gestureId ? pinchStartSpreads.get(gestureId) : void 0;
|
|
573
|
+
base.multiTouch = {
|
|
574
|
+
centroid,
|
|
575
|
+
spread,
|
|
576
|
+
rotation: 0,
|
|
577
|
+
...startSpread !== void 0 && event.spread !== void 0 ? { pinch: { startSpread, currentSpread: spread, centroid } } : {}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return base;
|
|
581
|
+
}
|
|
582
|
+
function gestureIdFor(event) {
|
|
583
|
+
if (event.kind === "key-held") {
|
|
584
|
+
return `key-held-${event.key}`;
|
|
585
|
+
}
|
|
586
|
+
if (event.kind === "pointerdown" || event.kind === "pointermove" || event.kind === "pointerup" || event.kind === "pointercancel") {
|
|
587
|
+
return pointerGestureId(event.pointerId);
|
|
588
|
+
}
|
|
589
|
+
if (event.kind === "multitouch") {
|
|
590
|
+
return `multitouch-${event.fingers}`;
|
|
591
|
+
}
|
|
592
|
+
return `ongoing-${event.kind}`;
|
|
593
|
+
}
|
|
594
|
+
function assembleScopedBindings(ctx) {
|
|
595
|
+
const ordered = [];
|
|
596
|
+
for (let i = ctx.hotkeyStack.length - 1; i >= 0; i--) {
|
|
597
|
+
const id = ctx.hotkeyStack[i];
|
|
598
|
+
const tool = ctx.toolsById.get(id);
|
|
599
|
+
if (tool && !ordered.includes(tool)) ordered.push(tool);
|
|
600
|
+
}
|
|
601
|
+
for (const tool of ctx.toolsById.values()) {
|
|
602
|
+
if (!ordered.includes(tool)) ordered.push(tool);
|
|
603
|
+
}
|
|
604
|
+
const capsCtx = ctx.getRuleCtx?.();
|
|
605
|
+
const allows = capsCtx ? (tags) => tags.every((tag) => capsCtx.allowedCapabilities.has(tag)) : void 0;
|
|
606
|
+
const result = scopeBindings(ordered, {
|
|
607
|
+
focusedId: ctx.activeToolId,
|
|
608
|
+
heldTriggers: EMPTY_TRIGGERS,
|
|
609
|
+
engagedIds: new Set(ctx.hotkeyStack),
|
|
610
|
+
...allows ? { allows } : {}
|
|
611
|
+
});
|
|
612
|
+
for (const action of ctx.actions.list()) {
|
|
613
|
+
const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
|
|
614
|
+
for (const binding of actionBindings(action)) {
|
|
615
|
+
result.push({ binding, scope: targetScope, ownerToolId: null });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
function snapshotEngagedChannels() {
|
|
621
|
+
if (inFlightOwners.size === 0) return EMPTY_ENGAGED2;
|
|
622
|
+
const out = /* @__PURE__ */ new Set();
|
|
623
|
+
for (const owner of inFlightOwners.values()) {
|
|
624
|
+
if (owner != null) out.add(owner);
|
|
625
|
+
}
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
function buildActionMap(registry) {
|
|
629
|
+
const map = /* @__PURE__ */ new Map();
|
|
630
|
+
for (const action of registry.list()) {
|
|
631
|
+
map.set(action.id, action);
|
|
632
|
+
}
|
|
633
|
+
return map;
|
|
634
|
+
}
|
|
635
|
+
function handleInput(event, ctx) {
|
|
636
|
+
if (event.kind === "key-held" && event.phase === "up") {
|
|
637
|
+
const gestureId = gestureIdFor(event);
|
|
638
|
+
const handle = inFlightHandles.get(gestureId);
|
|
639
|
+
if (handle) {
|
|
640
|
+
const stubCtx = buildInvocationCtx(event, {}, gestureId);
|
|
641
|
+
handle.onEnd?.(stubCtx, "commit");
|
|
642
|
+
inFlightHandles.delete(gestureId);
|
|
643
|
+
inFlightOwners.delete(gestureId);
|
|
644
|
+
inFlightActions.delete(gestureId);
|
|
645
|
+
dragOrigins.delete(gestureId);
|
|
646
|
+
}
|
|
647
|
+
return handle ? "handled" : "unhandled";
|
|
648
|
+
}
|
|
649
|
+
if (event.kind === "key-held" && event.phase === "down") {
|
|
650
|
+
const gestureId = gestureIdFor(event);
|
|
651
|
+
if (inFlightHandles.has(gestureId)) {
|
|
652
|
+
return "handled";
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (event.kind === "pointermove") {
|
|
656
|
+
const gestureId = gestureIdFor(event);
|
|
657
|
+
const handle = inFlightHandles.get(gestureId);
|
|
658
|
+
if (!handle) return "unhandled";
|
|
659
|
+
const pts = dragPoints.get(gestureId);
|
|
660
|
+
if (pts) {
|
|
661
|
+
pts.push({
|
|
662
|
+
x: event.x,
|
|
663
|
+
y: event.y,
|
|
664
|
+
...event.pressure !== void 0 ? { pressure: event.pressure } : {},
|
|
665
|
+
...event.tiltX !== void 0 ? { tiltX: event.tiltX } : {},
|
|
666
|
+
...event.tiltY !== void 0 ? { tiltY: event.tiltY } : {}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
if (handle.onMove) {
|
|
670
|
+
const moveCtx = buildInvocationCtx(event, {}, gestureId);
|
|
671
|
+
handle.onMove(moveCtx);
|
|
672
|
+
return "handled";
|
|
673
|
+
}
|
|
674
|
+
return "unhandled";
|
|
675
|
+
}
|
|
676
|
+
if (event.kind === "pointerup") {
|
|
677
|
+
const gestureId = gestureIdFor(event);
|
|
678
|
+
const handle = inFlightHandles.get(gestureId);
|
|
679
|
+
if (handle) {
|
|
680
|
+
const endCtx = buildInvocationCtx(event, {}, gestureId);
|
|
681
|
+
handle.onEnd?.(endCtx, "commit");
|
|
682
|
+
inFlightHandles.delete(gestureId);
|
|
683
|
+
inFlightOwners.delete(gestureId);
|
|
684
|
+
inFlightActions.delete(gestureId);
|
|
685
|
+
dragOrigins.delete(gestureId);
|
|
686
|
+
dragPoints.delete(gestureId);
|
|
687
|
+
}
|
|
688
|
+
return handle ? "handled" : "unhandled";
|
|
689
|
+
}
|
|
690
|
+
if (event.kind === "pointercancel") {
|
|
691
|
+
const gestureId = gestureIdFor(event);
|
|
692
|
+
const handle = inFlightHandles.get(gestureId);
|
|
693
|
+
if (handle) {
|
|
694
|
+
const endCtx = buildInvocationCtx(event, {}, gestureId);
|
|
695
|
+
handle.onEnd?.(endCtx, "cancel");
|
|
696
|
+
inFlightHandles.delete(gestureId);
|
|
697
|
+
inFlightOwners.delete(gestureId);
|
|
698
|
+
inFlightActions.delete(gestureId);
|
|
699
|
+
dragOrigins.delete(gestureId);
|
|
700
|
+
dragPoints.delete(gestureId);
|
|
701
|
+
}
|
|
702
|
+
return handle ? "handled" : "unhandled";
|
|
703
|
+
}
|
|
704
|
+
if (event.kind === "multitouch" && event.centroid !== void 0) {
|
|
705
|
+
const gestureId = gestureIdFor(event);
|
|
706
|
+
const handle = inFlightHandles.get(gestureId);
|
|
707
|
+
if (handle?.onMove) {
|
|
708
|
+
const moveCtx = buildInvocationCtx(event, {}, gestureId);
|
|
709
|
+
handle.onMove(moveCtx);
|
|
710
|
+
return "handled";
|
|
711
|
+
}
|
|
712
|
+
for (const [id, other] of [...inFlightHandles]) {
|
|
713
|
+
if (!id.startsWith("multitouch-") || id === gestureId) continue;
|
|
714
|
+
other.onEnd?.(buildInvocationCtx(event, {}, id), "cancel");
|
|
715
|
+
inFlightHandles.delete(id);
|
|
716
|
+
inFlightOwners.delete(id);
|
|
717
|
+
inFlightActions.delete(id);
|
|
718
|
+
pinchStartSpreads.delete(id);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const scopedBindings = assembleScopedBindings(ctx);
|
|
722
|
+
const engagedChannels = snapshotEngagedChannels();
|
|
723
|
+
const rawMatches = matchSorted(event, scopedBindings, ctx.isMac, engagedChannels);
|
|
724
|
+
const traceCandidates = [];
|
|
725
|
+
const eventKey = event.kind === "key" || event.kind === "key-held" ? event.key : void 0;
|
|
726
|
+
const finishTrace = (fired, outcome) => {
|
|
727
|
+
recordTrace({
|
|
728
|
+
kind: "dispatch",
|
|
729
|
+
ts: Date.now(),
|
|
730
|
+
eventKind: event.kind,
|
|
731
|
+
...eventKey !== void 0 ? { key: eventKey } : {},
|
|
732
|
+
candidates: traceCandidates,
|
|
733
|
+
fired,
|
|
734
|
+
outcome
|
|
735
|
+
});
|
|
736
|
+
};
|
|
737
|
+
if (rawMatches.length === 0) {
|
|
738
|
+
finishTrace(null, "unhandled");
|
|
739
|
+
return "unhandled";
|
|
740
|
+
}
|
|
741
|
+
const actionMap = buildActionMap(ctx.actions);
|
|
742
|
+
const ruleCtx = ctx.getRuleCtx?.();
|
|
743
|
+
const matches = ruleCtx ? filterEligible(rawMatches, (id) => actionMap.get(id), ruleCtx) : rawMatches;
|
|
744
|
+
if (matches.length === 0) {
|
|
745
|
+
finishTrace(null, "unhandled");
|
|
746
|
+
return "unhandled";
|
|
747
|
+
}
|
|
748
|
+
const triedActionIds = /* @__PURE__ */ new Set();
|
|
749
|
+
for (const match of matches) {
|
|
750
|
+
if (triedActionIds.has(match.binding.actionId)) continue;
|
|
751
|
+
triedActionIds.add(match.binding.actionId);
|
|
752
|
+
const action = actionMap.get(match.binding.actionId);
|
|
753
|
+
if (!action) {
|
|
754
|
+
traceCandidates.push({ actionId: match.binding.actionId, scope: match.scope, enabledResult: "no-such-action" });
|
|
755
|
+
console.warn(
|
|
756
|
+
`weasel dispatcher: binding resolved actionId "${match.binding.actionId}" which has no registered action. Skipping. (misconfiguration)`
|
|
757
|
+
);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const deps = buildDepsFromRequires(action, ctx.depRegistry);
|
|
761
|
+
if (action.enabled) {
|
|
762
|
+
const result = action.enabled(deps);
|
|
763
|
+
if (result !== true) {
|
|
764
|
+
traceCandidates.push({ actionId: action.id, scope: match.scope, enabledResult: String(result) });
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
traceCandidates.push({ actionId: action.id, scope: match.scope, enabledResult: true });
|
|
769
|
+
if (action.invoker?.timing === "immediate") {
|
|
770
|
+
try {
|
|
771
|
+
const resolved = resolveParams(match.binding.opts?.params);
|
|
772
|
+
let params;
|
|
773
|
+
if (event.kind === "wheel") {
|
|
774
|
+
params = {
|
|
775
|
+
deltaX: event.deltaX,
|
|
776
|
+
deltaY: event.deltaY,
|
|
777
|
+
clientX: event.clientX,
|
|
778
|
+
clientY: event.clientY,
|
|
779
|
+
affordance: event.affordance,
|
|
780
|
+
...resolved
|
|
781
|
+
};
|
|
782
|
+
} else if (event.kind === "click" || event.kind === "doubleclick") {
|
|
783
|
+
params = {
|
|
784
|
+
worldX: event.worldX,
|
|
785
|
+
worldY: event.worldY,
|
|
786
|
+
affordance: event.affordance,
|
|
787
|
+
// Press point as well as release point — an action that places
|
|
788
|
+
// geometry at the click wants the former. See `ClickEvent`.
|
|
789
|
+
...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
|
|
790
|
+
mods: modifiersOf(event),
|
|
791
|
+
...resolved
|
|
792
|
+
};
|
|
793
|
+
} else if (event.kind === "contextmenu" || event.kind === "longpress") {
|
|
794
|
+
params = {
|
|
795
|
+
worldX: event.kind === "contextmenu" ? event.worldX : event.x,
|
|
796
|
+
worldY: event.kind === "contextmenu" ? event.worldY : event.y,
|
|
797
|
+
affordance: event.affordance,
|
|
798
|
+
bodyTarget: event.bodyTarget,
|
|
799
|
+
mods: modifiersOf(event),
|
|
800
|
+
...resolved
|
|
801
|
+
};
|
|
802
|
+
} else if (event.kind === "pointerdown") {
|
|
803
|
+
params = {
|
|
804
|
+
worldX: event.x,
|
|
805
|
+
worldY: event.y,
|
|
806
|
+
affordance: event.affordance,
|
|
807
|
+
bodyTarget: event.bodyTarget,
|
|
808
|
+
mods: modifiersOf(event),
|
|
809
|
+
...resolved
|
|
810
|
+
};
|
|
811
|
+
} else if (event.kind === "drop" || event.kind === "paste") {
|
|
812
|
+
params = {
|
|
813
|
+
items: event.items,
|
|
814
|
+
via: event.kind,
|
|
815
|
+
...event.kind === "drop" && event.x !== void 0 ? { worldX: event.x, worldY: event.y } : {},
|
|
816
|
+
...resolved
|
|
817
|
+
};
|
|
818
|
+
} else {
|
|
819
|
+
params = resolved;
|
|
820
|
+
}
|
|
821
|
+
action.invoker.run(deps, params);
|
|
822
|
+
} catch (err) {
|
|
823
|
+
console.error(`weasel dispatcher: action "${action.id}" invoker threw`, err);
|
|
824
|
+
}
|
|
825
|
+
finishTrace(action.id, "handled");
|
|
826
|
+
return "handled";
|
|
827
|
+
}
|
|
828
|
+
if (action.invoker?.timing === "ongoing") {
|
|
829
|
+
if (event.kind === "pointerdown" && event.stage === "press") {
|
|
830
|
+
console.error(
|
|
831
|
+
`weasel dispatcher: action "${action.id}" has an ongoing invoker but is bound to a pointerDown spec, which fires at press time. Bind it to a drag spec, or give the action an immediate invoker.`
|
|
832
|
+
);
|
|
833
|
+
finishTrace(action.id, "unhandled");
|
|
834
|
+
return "unhandled";
|
|
835
|
+
}
|
|
836
|
+
const gestureId = gestureIdFor(event);
|
|
837
|
+
if (event.kind === "pointerdown") {
|
|
838
|
+
dragOrigins.set(gestureId, {
|
|
839
|
+
x: event.x ?? 0,
|
|
840
|
+
y: event.y ?? 0,
|
|
841
|
+
...event.clientX !== void 0 ? { clientX: event.clientX } : {},
|
|
842
|
+
...event.clientY !== void 0 ? { clientY: event.clientY } : {}
|
|
843
|
+
});
|
|
844
|
+
dragPoints.set(gestureId, [{
|
|
845
|
+
x: event.x ?? 0,
|
|
846
|
+
y: event.y ?? 0,
|
|
847
|
+
...event.pressure !== void 0 ? { pressure: event.pressure } : {},
|
|
848
|
+
...event.tiltX !== void 0 ? { tiltX: event.tiltX } : {},
|
|
849
|
+
...event.tiltY !== void 0 ? { tiltY: event.tiltY } : {}
|
|
850
|
+
}]);
|
|
851
|
+
}
|
|
852
|
+
if (event.kind === "multitouch" && event.spread !== void 0) {
|
|
853
|
+
pinchStartSpreads.set(gestureId, event.spread);
|
|
854
|
+
}
|
|
855
|
+
const invCtx = buildInvocationCtx(event, deps, gestureId);
|
|
856
|
+
const handle = action.invoker.start(invCtx, match.binding.opts);
|
|
857
|
+
if (isEmptyOngoingHandle(handle)) {
|
|
858
|
+
if (event.kind === "pointerdown") {
|
|
859
|
+
dragOrigins.delete(gestureId);
|
|
860
|
+
dragPoints.delete(gestureId);
|
|
861
|
+
}
|
|
862
|
+
if (event.kind === "multitouch") {
|
|
863
|
+
pinchStartSpreads.delete(gestureId);
|
|
864
|
+
}
|
|
865
|
+
traceCandidates.push({ actionId: action.id, scope: match.scope, enabledResult: "empty-handle" });
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
inFlightHandles.set(gestureId, handle);
|
|
869
|
+
inFlightOwners.set(gestureId, match.ownerToolId);
|
|
870
|
+
inFlightActions.set(gestureId, action);
|
|
871
|
+
finishTrace(action.id, "handled");
|
|
872
|
+
return "handled";
|
|
873
|
+
}
|
|
874
|
+
finishTrace(action.id, "handled");
|
|
875
|
+
return "handled";
|
|
876
|
+
}
|
|
877
|
+
finishTrace(null, "unhandled");
|
|
878
|
+
return "unhandled";
|
|
879
|
+
}
|
|
880
|
+
function resolveAll(event, ctx, resolveOpts) {
|
|
881
|
+
const scopedBindings = assembleScopedBindings(ctx);
|
|
882
|
+
const engagedChannels = snapshotEngagedChannels();
|
|
883
|
+
const matches = matchSorted(event, scopedBindings, ctx.isMac, engagedChannels);
|
|
884
|
+
if (matches.length === 0) return [];
|
|
885
|
+
const actionMap = buildActionMap(ctx.actions);
|
|
886
|
+
const ruleCtx = ctx.getRuleCtx?.();
|
|
887
|
+
const verdictByAction = /* @__PURE__ */ new Map();
|
|
888
|
+
let fired = false;
|
|
889
|
+
const out = [];
|
|
890
|
+
for (const match of matches) {
|
|
891
|
+
const actionId = match.binding.actionId;
|
|
892
|
+
const action = actionMap.get(actionId);
|
|
893
|
+
if (!action) continue;
|
|
894
|
+
let verdict = verdictByAction.get(actionId);
|
|
895
|
+
if (verdict === void 0) {
|
|
896
|
+
if (fired && !resolveOpts?.evaluateShadowed) {
|
|
897
|
+
verdict = { kind: "shadowed" };
|
|
898
|
+
} else if (ruleCtx && action.eligible && !isEligible(action, ruleCtx)) {
|
|
899
|
+
verdict = { kind: "ineligible", reason: describeEligible(action.eligible) };
|
|
900
|
+
} else {
|
|
901
|
+
const disabled = action.enabled ? action.enabled(buildDepsFromRequires(action, ctx.depRegistry)) : true;
|
|
902
|
+
if (disabled !== true) {
|
|
903
|
+
verdict = { kind: "disabled", reason: String(disabled) };
|
|
904
|
+
} else if (fired) {
|
|
905
|
+
verdict = { kind: "shadowed" };
|
|
906
|
+
} else {
|
|
907
|
+
verdict = { kind: "would-fire" };
|
|
908
|
+
fired = true;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
verdictByAction.set(actionId, verdict);
|
|
912
|
+
} else if (verdict.kind === "would-fire") {
|
|
913
|
+
verdict = { kind: "shadowed" };
|
|
914
|
+
}
|
|
915
|
+
out.push({
|
|
916
|
+
actionId,
|
|
917
|
+
action,
|
|
918
|
+
binding: match.binding,
|
|
919
|
+
scope: match.scope,
|
|
920
|
+
ownerToolId: match.ownerToolId,
|
|
921
|
+
specificity: specificity(match.binding.spec),
|
|
922
|
+
verdict
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
return out;
|
|
926
|
+
}
|
|
927
|
+
function resolveOnly(event, ctx) {
|
|
928
|
+
const winner = resolveAll(event, ctx).find((c) => c.verdict.kind === "would-fire");
|
|
929
|
+
if (!winner) return null;
|
|
930
|
+
return {
|
|
931
|
+
actionId: winner.actionId,
|
|
932
|
+
action: winner.action,
|
|
933
|
+
scope: winner.scope,
|
|
934
|
+
ownerToolId: winner.ownerToolId
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function cancelAll(reason) {
|
|
938
|
+
const stubCtx = {
|
|
939
|
+
world: { x: 0, y: 0 },
|
|
940
|
+
modifiers: { alt: false, ctrl: false, meta: false, shift: false },
|
|
941
|
+
deps: {}
|
|
942
|
+
};
|
|
943
|
+
for (const handle of inFlightHandles.values()) {
|
|
944
|
+
handle.onEnd?.(stubCtx, reason);
|
|
945
|
+
}
|
|
946
|
+
inFlightHandles.clear();
|
|
947
|
+
inFlightOwners.clear();
|
|
948
|
+
inFlightActions.clear();
|
|
949
|
+
uiOngoingByAction.clear();
|
|
950
|
+
dragOrigins.clear();
|
|
951
|
+
dragPoints.clear();
|
|
952
|
+
pinchStartSpreads.clear();
|
|
953
|
+
}
|
|
954
|
+
function inFlight() {
|
|
955
|
+
return inFlightHandles;
|
|
956
|
+
}
|
|
957
|
+
function inFlightCursor() {
|
|
958
|
+
let cursor = null;
|
|
959
|
+
for (const action of inFlightActions.values()) {
|
|
960
|
+
const c = resolveCursor(action.activeCursor ?? action.cursor);
|
|
961
|
+
if (c) cursor = c;
|
|
962
|
+
}
|
|
963
|
+
return cursor;
|
|
964
|
+
}
|
|
965
|
+
function getInFlightHandles() {
|
|
966
|
+
return inFlightHandles.values();
|
|
967
|
+
}
|
|
968
|
+
function subscribe(fn) {
|
|
969
|
+
subscribers.add(fn);
|
|
970
|
+
return () => {
|
|
971
|
+
subscribers.delete(fn);
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function getVersion() {
|
|
975
|
+
return version;
|
|
976
|
+
}
|
|
977
|
+
function getActiveAction() {
|
|
978
|
+
let lastId = null;
|
|
979
|
+
let lastHandle = null;
|
|
980
|
+
for (const [id, handle] of inFlightHandles) {
|
|
981
|
+
lastId = id;
|
|
982
|
+
lastHandle = handle;
|
|
983
|
+
}
|
|
984
|
+
if (lastHandle === null) return { kind: null, id: null };
|
|
985
|
+
return { kind: lastHandle.kind ?? null, id: lastId };
|
|
986
|
+
}
|
|
987
|
+
const handleInputWithNotify = (event, ctx) => {
|
|
988
|
+
const out = handleInput(event, ctx);
|
|
989
|
+
notify();
|
|
990
|
+
return out;
|
|
991
|
+
};
|
|
992
|
+
const cancelAllWithNotify = (reason) => {
|
|
993
|
+
cancelAll(reason);
|
|
994
|
+
notify();
|
|
995
|
+
};
|
|
996
|
+
function beginUiOngoing(actionId, deps, params) {
|
|
997
|
+
const getAction = opts?.getAction;
|
|
998
|
+
if (!getAction) {
|
|
999
|
+
console.warn("weasel dispatcher: beginUiOngoing called but no getAction was provided to createDispatcher");
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
const action = getAction(actionId);
|
|
1003
|
+
if (!action || !action.invoker || action.invoker.timing !== "ongoing") {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
const prevGestureId = uiOngoingByAction.get(actionId);
|
|
1007
|
+
if (prevGestureId !== void 0) {
|
|
1008
|
+
const prevHandle = inFlightHandles.get(prevGestureId);
|
|
1009
|
+
if (prevHandle?.onEnd) {
|
|
1010
|
+
try {
|
|
1011
|
+
prevHandle.onEnd(buildUiInvocationCtx(deps), "commit");
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
console.error(`weasel dispatcher: prior UI handle for "${actionId}" threw on auto-commit`, e);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
inFlightHandles.delete(prevGestureId);
|
|
1017
|
+
inFlightOwners.delete(prevGestureId);
|
|
1018
|
+
uiOngoingByAction.delete(actionId);
|
|
1019
|
+
}
|
|
1020
|
+
const gestureId = `ui-${actionId}-${++uiOngoingSeq}`;
|
|
1021
|
+
let handle;
|
|
1022
|
+
try {
|
|
1023
|
+
handle = action.invoker.start(buildUiInvocationCtx(deps, params), { params });
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
console.error(`weasel dispatcher: action "${actionId}" threw on start`, e);
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
if (isEmptyOngoingHandle(handle)) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
inFlightHandles.set(gestureId, handle);
|
|
1032
|
+
inFlightOwners.set(gestureId, null);
|
|
1033
|
+
uiOngoingByAction.set(actionId, gestureId);
|
|
1034
|
+
notify();
|
|
1035
|
+
let ended = false;
|
|
1036
|
+
const live = () => !ended && inFlightHandles.get(gestureId) === handle;
|
|
1037
|
+
return {
|
|
1038
|
+
gestureId,
|
|
1039
|
+
update(nextParams) {
|
|
1040
|
+
if (!live()) return;
|
|
1041
|
+
if (!handle.onMove) return;
|
|
1042
|
+
try {
|
|
1043
|
+
handle.onMove(buildUiInvocationCtx(deps, nextParams));
|
|
1044
|
+
} catch (e) {
|
|
1045
|
+
console.error(`weasel dispatcher: action "${actionId}" threw on onMove`, e);
|
|
1046
|
+
}
|
|
1047
|
+
notify();
|
|
1048
|
+
},
|
|
1049
|
+
end(reason) {
|
|
1050
|
+
if (!live()) {
|
|
1051
|
+
ended = true;
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
ended = true;
|
|
1055
|
+
if (handle.onEnd) {
|
|
1056
|
+
try {
|
|
1057
|
+
handle.onEnd(buildUiInvocationCtx(deps), reason);
|
|
1058
|
+
} catch (e) {
|
|
1059
|
+
console.error(`weasel dispatcher: action "${actionId}" threw on onEnd`, e);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
inFlightHandles.delete(gestureId);
|
|
1063
|
+
inFlightOwners.delete(gestureId);
|
|
1064
|
+
inFlightActions.delete(gestureId);
|
|
1065
|
+
if (uiOngoingByAction.get(actionId) === gestureId) {
|
|
1066
|
+
uiOngoingByAction.delete(actionId);
|
|
1067
|
+
}
|
|
1068
|
+
notify();
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
handleInput: handleInputWithNotify,
|
|
1074
|
+
resolveOnly,
|
|
1075
|
+
resolveAll,
|
|
1076
|
+
cancelAll: cancelAllWithNotify,
|
|
1077
|
+
inFlight,
|
|
1078
|
+
inFlightCursor,
|
|
1079
|
+
getInFlightHandles,
|
|
1080
|
+
subscribe,
|
|
1081
|
+
getVersion,
|
|
1082
|
+
getActiveAction,
|
|
1083
|
+
beginUiOngoing
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/interactions/pointerSession/pointerSession.ts
|
|
1088
|
+
var LOST_CAPTURE_EVENT = "lostpointercapture";
|
|
1089
|
+
function reportsButtons(down) {
|
|
1090
|
+
return !!down.buttons;
|
|
1091
|
+
}
|
|
1092
|
+
function isMissedRelease(move) {
|
|
1093
|
+
return move.buttons === 0;
|
|
1094
|
+
}
|
|
1095
|
+
function openPointerSession(origin, down, cb, opts = {}) {
|
|
1096
|
+
const pointerId = down.pointerId;
|
|
1097
|
+
const doc = origin.ownerDocument ?? document;
|
|
1098
|
+
const buttonsReported = reportsButtons(down);
|
|
1099
|
+
let active = true;
|
|
1100
|
+
const teardown = () => {
|
|
1101
|
+
active = false;
|
|
1102
|
+
doc.removeEventListener("pointermove", onMove, true);
|
|
1103
|
+
doc.removeEventListener("pointerup", onUp, true);
|
|
1104
|
+
doc.removeEventListener("pointercancel", onPointerCancel, true);
|
|
1105
|
+
doc.removeEventListener("pointerdown", onRepress, true);
|
|
1106
|
+
origin.removeEventListener(LOST_CAPTURE_EVENT, onLostCapture);
|
|
1107
|
+
if (opts.capture !== false) {
|
|
1108
|
+
try {
|
|
1109
|
+
origin.releasePointerCapture?.(pointerId);
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
const mine = (e) => e.pointerId === pointerId;
|
|
1115
|
+
const end = (e) => {
|
|
1116
|
+
if (!active) return;
|
|
1117
|
+
teardown();
|
|
1118
|
+
cb.onEnd?.(e);
|
|
1119
|
+
};
|
|
1120
|
+
const abort = (reason) => {
|
|
1121
|
+
if (!active) return;
|
|
1122
|
+
teardown();
|
|
1123
|
+
cb.onCancel?.(reason);
|
|
1124
|
+
};
|
|
1125
|
+
function onMove(e) {
|
|
1126
|
+
if (!active || !mine(e)) return;
|
|
1127
|
+
if (buttonsReported && isMissedRelease(e)) {
|
|
1128
|
+
end(e);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
cb.onMove?.(e);
|
|
1132
|
+
}
|
|
1133
|
+
function onUp(e) {
|
|
1134
|
+
if (mine(e)) end(e);
|
|
1135
|
+
}
|
|
1136
|
+
function onPointerCancel(e) {
|
|
1137
|
+
if (mine(e)) abort("pointercancel");
|
|
1138
|
+
}
|
|
1139
|
+
function onRepress(e) {
|
|
1140
|
+
if (mine(e)) abort("superseded");
|
|
1141
|
+
}
|
|
1142
|
+
function onLostCapture(e) {
|
|
1143
|
+
if (e.pointerId !== pointerId) return;
|
|
1144
|
+
if (!origin.isConnected) abort("lostcapture");
|
|
1145
|
+
}
|
|
1146
|
+
if (opts.capture !== false) {
|
|
1147
|
+
try {
|
|
1148
|
+
origin.setPointerCapture?.(pointerId);
|
|
1149
|
+
} catch {
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
doc.addEventListener("pointermove", onMove, true);
|
|
1153
|
+
doc.addEventListener("pointerup", onUp, true);
|
|
1154
|
+
doc.addEventListener("pointercancel", onPointerCancel, true);
|
|
1155
|
+
doc.addEventListener("pointerdown", onRepress, true);
|
|
1156
|
+
origin.addEventListener(LOST_CAPTURE_EVENT, onLostCapture);
|
|
1157
|
+
return {
|
|
1158
|
+
pointerId,
|
|
1159
|
+
get active() {
|
|
1160
|
+
return active;
|
|
1161
|
+
},
|
|
1162
|
+
cancel: () => abort("aborted")
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/tools/routing/reflection/registry.ts
|
|
1167
|
+
var PREDICATE_TARGET = "predicate";
|
|
1168
|
+
var SPEC_KIND_TO_GESTURE = {
|
|
1169
|
+
key: "keyDown",
|
|
1170
|
+
"key-held": "keyHeld",
|
|
1171
|
+
wheel: "wheel",
|
|
1172
|
+
click: "click",
|
|
1173
|
+
doubleClick: "dblTap",
|
|
1174
|
+
contextMenu: "contextMenu",
|
|
1175
|
+
longPress: "longPress",
|
|
1176
|
+
drag: "drag",
|
|
1177
|
+
pointerDown: "pointerDown",
|
|
1178
|
+
multiTouch: void 0,
|
|
1179
|
+
multiTouchTap: "multiTouchTap",
|
|
1180
|
+
drop: "drop",
|
|
1181
|
+
paste: "paste"
|
|
1182
|
+
};
|
|
1183
|
+
function buildRouteRegistry(tools) {
|
|
1184
|
+
const out = [];
|
|
1185
|
+
for (const tool of tools) {
|
|
1186
|
+
for (const binding of tool.bindings ?? []) {
|
|
1187
|
+
const entry = entryFor(tool.id, binding.spec, binding.actionId);
|
|
1188
|
+
if (entry) out.push(entry);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
function routeGestureForSpecKind(kind) {
|
|
1194
|
+
const gesture = SPEC_KIND_TO_GESTURE[kind];
|
|
1195
|
+
return gesture && isKnownGestureName(gesture) ? gesture : void 0;
|
|
1196
|
+
}
|
|
1197
|
+
function routesForSpec(spec) {
|
|
1198
|
+
const gesture = routeGestureForSpecKind(spec.kind);
|
|
1199
|
+
if (!gesture) return [];
|
|
1200
|
+
const descriptor = getGestureDescriptor(gesture);
|
|
1201
|
+
const modifiers = parseModSpec("mods" in spec ? spec.mods : void 0);
|
|
1202
|
+
const phases = phaseAtomsForSpec("phase" in spec ? spec.phase : void 0);
|
|
1203
|
+
const target = descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) ?? "*" : void 0;
|
|
1204
|
+
const args = descriptor.arg ? routeArgsForSpec(spec, descriptor.arg.default) : [void 0];
|
|
1205
|
+
return args.map((arg) => formatRoute({ phases, gesture, arg, target, modifiers }));
|
|
1206
|
+
}
|
|
1207
|
+
function entryFor(toolId, spec, actionId) {
|
|
1208
|
+
const gesture = SPEC_KIND_TO_GESTURE[spec.kind];
|
|
1209
|
+
if (!gesture || !isKnownGestureName(gesture)) return null;
|
|
1210
|
+
const descriptor = getGestureDescriptor(gesture);
|
|
1211
|
+
return {
|
|
1212
|
+
toolId,
|
|
1213
|
+
actionId,
|
|
1214
|
+
gesture,
|
|
1215
|
+
phase: phaseOf("phase" in spec ? spec.phase : void 0),
|
|
1216
|
+
modifiers: parseModSpec("mods" in spec ? spec.mods : void 0),
|
|
1217
|
+
arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : void 0,
|
|
1218
|
+
...descriptor.arg ? argAlternativesOf(spec, descriptor.arg.default) : {},
|
|
1219
|
+
target: descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) : void 0,
|
|
1220
|
+
spec
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
function phaseOf(phase) {
|
|
1224
|
+
if (phase === void 0 || phase === "*") return "any";
|
|
1225
|
+
if (phase === "initial" || phase === "engaged") return phase;
|
|
1226
|
+
const distinct = new Set(phase.map((atom) => atom.phase));
|
|
1227
|
+
if (distinct.size !== 1) return "any";
|
|
1228
|
+
const only = [...distinct][0];
|
|
1229
|
+
return only === "initial" || only === "engaged" ? only : "any";
|
|
1230
|
+
}
|
|
1231
|
+
function parseModSpec(mods) {
|
|
1232
|
+
if (!mods) return {};
|
|
1233
|
+
const out = {};
|
|
1234
|
+
for (const [name, req] of Object.entries(mods)) {
|
|
1235
|
+
if (req === true) out[name] = "required";
|
|
1236
|
+
else if (req === "optional") out[name] = "optional";
|
|
1237
|
+
}
|
|
1238
|
+
return out;
|
|
1239
|
+
}
|
|
1240
|
+
function phaseAtomsForSpec(phase) {
|
|
1241
|
+
if (phase === void 0) return [{ channel: "&", phase: "*" }];
|
|
1242
|
+
if (phase === "initial" || phase === "engaged" || phase === "*") {
|
|
1243
|
+
return [{ channel: "&", phase }];
|
|
1244
|
+
}
|
|
1245
|
+
return phase;
|
|
1246
|
+
}
|
|
1247
|
+
function routeTargetForSpec(target) {
|
|
1248
|
+
if (target === void 0) return void 0;
|
|
1249
|
+
return typeof target === "string" ? target : PREDICATE_TARGET;
|
|
1250
|
+
}
|
|
1251
|
+
function routeArgsForSpec(spec, fallback) {
|
|
1252
|
+
if ("key" in spec) return Array.isArray(spec.key) ? spec.key : [spec.key];
|
|
1253
|
+
if ("fingers" in spec) return [String(spec.fingers)];
|
|
1254
|
+
if ("direction" in spec) return [spec.direction ?? fallback];
|
|
1255
|
+
if ("types" in spec) return [spec.types?.length ? spec.types.join("|") : fallback];
|
|
1256
|
+
return [fallback];
|
|
1257
|
+
}
|
|
1258
|
+
function argOf(spec, fallback) {
|
|
1259
|
+
const args = routeArgsForSpec(spec, fallback);
|
|
1260
|
+
return args.length > 1 ? args.join("|") : args[0];
|
|
1261
|
+
}
|
|
1262
|
+
function argAlternativesOf(spec, fallback) {
|
|
1263
|
+
const types = "types" in spec ? spec.types : void 0;
|
|
1264
|
+
const alts = types?.length ? types : routeArgsForSpec(spec, fallback).filter((a) => a !== void 0);
|
|
1265
|
+
return alts.length > 1 ? { argAlternatives: alts } : {};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/tools/routing/reflection/conflicts.ts
|
|
1269
|
+
function findConflicts(tools) {
|
|
1270
|
+
return findConflictsKeyed(tools).map((k) => k.conflict);
|
|
1271
|
+
}
|
|
1272
|
+
function findConflictsKeyed(tools) {
|
|
1273
|
+
const entries = buildRouteRegistry(tools);
|
|
1274
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1275
|
+
for (const entry of entries) {
|
|
1276
|
+
for (const arg of argKeysOf(entry)) {
|
|
1277
|
+
const key = `${entry.phase}|${entry.gesture}|${arg}|${targetKey(entry)}|${canonicalModifiers(entry.modifiers)}`;
|
|
1278
|
+
const bucket = groups.get(key);
|
|
1279
|
+
if (bucket) {
|
|
1280
|
+
if (!bucket.includes(entry)) bucket.push(entry);
|
|
1281
|
+
} else groups.set(key, [entry]);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const conflicts = [];
|
|
1285
|
+
for (const [key, bucket] of groups) {
|
|
1286
|
+
if (bucket.length < 2) continue;
|
|
1287
|
+
const first = bucket[0];
|
|
1288
|
+
conflicts.push({
|
|
1289
|
+
key,
|
|
1290
|
+
conflict: {
|
|
1291
|
+
phase: first.phase,
|
|
1292
|
+
gesture: first.gesture,
|
|
1293
|
+
arg: first.arg,
|
|
1294
|
+
target: first.target,
|
|
1295
|
+
modifiers: first.modifiers,
|
|
1296
|
+
toolIds: bucket.map((e) => e.toolId)
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
return conflicts;
|
|
1301
|
+
}
|
|
1302
|
+
function argKeysOf(entry) {
|
|
1303
|
+
const alts = entry.argAlternatives;
|
|
1304
|
+
if (alts && alts.length > 0) return [...new Set(alts.map((a) => a.toLowerCase()))];
|
|
1305
|
+
return [(entry.arg ?? "").toLowerCase()];
|
|
1306
|
+
}
|
|
1307
|
+
function targetKey(entry) {
|
|
1308
|
+
if (entry.target !== PREDICATE_TARGET) return entry.target ?? "";
|
|
1309
|
+
const spec = entry.spec;
|
|
1310
|
+
const pred = spec.target;
|
|
1311
|
+
if (typeof pred !== "object" || pred === null) return PREDICATE_TARGET;
|
|
1312
|
+
return `${PREDICATE_TARGET}#${predicateId(pred)}`;
|
|
1313
|
+
}
|
|
1314
|
+
var PREDICATE_IDS = /* @__PURE__ */ new WeakMap();
|
|
1315
|
+
var nextPredicateId = 0;
|
|
1316
|
+
function predicateId(pred) {
|
|
1317
|
+
let id = PREDICATE_IDS.get(pred);
|
|
1318
|
+
if (id === void 0) {
|
|
1319
|
+
id = nextPredicateId++;
|
|
1320
|
+
PREDICATE_IDS.set(pred, id);
|
|
1321
|
+
}
|
|
1322
|
+
return id;
|
|
1323
|
+
}
|
|
1324
|
+
function actionAsTool(action) {
|
|
1325
|
+
return { id: action.id, eligibility: {}, bindings: actionBindings(action) };
|
|
1326
|
+
}
|
|
1327
|
+
function findScopedConflicts(scopes) {
|
|
1328
|
+
const registry = Array.isArray(scopes.registry) ? scopes.registry : Object.values(scopes.registry);
|
|
1329
|
+
const actions = scopes.actions ?? [];
|
|
1330
|
+
const ambientActions = actions.filter((a) => a.scope !== "hotkey").map(actionAsTool);
|
|
1331
|
+
const hotkeyActions = actions.filter((a) => a.scope === "hotkey").map(actionAsTool);
|
|
1332
|
+
const ambient = [...scopes.ambient ?? [], ...ambientActions];
|
|
1333
|
+
const out = [];
|
|
1334
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1335
|
+
const add = (keyed) => {
|
|
1336
|
+
for (const { key, conflict } of keyed) {
|
|
1337
|
+
const dedupeKey = `${key}|${conflict.toolIds.join(",")}`;
|
|
1338
|
+
if (seen.has(dedupeKey)) continue;
|
|
1339
|
+
seen.add(dedupeKey);
|
|
1340
|
+
out.push(conflict);
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
for (const tool of [...registry, ...ambient, ...hotkeyActions]) add(findConflictsKeyed([tool]));
|
|
1344
|
+
if (ambient.length > 1) add(findConflictsKeyed(ambient));
|
|
1345
|
+
const hotkey = [
|
|
1346
|
+
...registry.filter((t) => t.def?.hotkey !== void 0),
|
|
1347
|
+
...hotkeyActions
|
|
1348
|
+
];
|
|
1349
|
+
if (hotkey.length > 1) add(findConflictsKeyed(hotkey));
|
|
1350
|
+
return out;
|
|
1351
|
+
}
|
|
1352
|
+
function formatConflict(conflict) {
|
|
1353
|
+
const route = formatRoute({
|
|
1354
|
+
phases: [{ channel: "&", phase: conflict.phase === "any" ? "*" : conflict.phase }],
|
|
1355
|
+
gesture: conflict.gesture,
|
|
1356
|
+
arg: conflict.arg,
|
|
1357
|
+
target: conflict.target,
|
|
1358
|
+
modifiers: conflict.modifiers
|
|
1359
|
+
});
|
|
1360
|
+
return `${route} \u2014 declared by ${conflict.toolIds.join(", ")}`;
|
|
1361
|
+
}
|
|
1362
|
+
function reportRouteConflicts(scopes, warn = (m) => console.warn(m)) {
|
|
1363
|
+
const conflicts = findScopedConflicts(scopes);
|
|
1364
|
+
for (const c of conflicts) {
|
|
1365
|
+
warn(
|
|
1366
|
+
`[weasel] route conflict: two bindings declare the same (phase, gesture, arg, target, modifiers) tuple in the same scope, so declaration order alone decides which one fires.
|
|
1367
|
+
${formatConflict(c)}`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
return conflicts;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/interactions/keyHelpers.ts
|
|
1374
|
+
function isEditableTarget(target) {
|
|
1375
|
+
if (!target || !(target instanceof Element)) return false;
|
|
1376
|
+
const tag = target.tagName;
|
|
1377
|
+
if (tag === "INPUT" || tag === "TEXTAREA") return true;
|
|
1378
|
+
if (target.isContentEditable) return true;
|
|
1379
|
+
const ce = target.getAttribute("contenteditable");
|
|
1380
|
+
return ce === "true" || ce === "";
|
|
1381
|
+
}
|
|
1382
|
+
function keyMatches(eventKey, spec) {
|
|
1383
|
+
const want = typeof spec === "string" ? [spec] : spec;
|
|
1384
|
+
const ek = eventKey.toLowerCase();
|
|
1385
|
+
return want.some((k) => k.toLowerCase() === ek);
|
|
1386
|
+
}
|
|
1387
|
+
function matchesKeyBinding(e, b) {
|
|
1388
|
+
if (!keyMatches(e.key, b.key)) return false;
|
|
1389
|
+
const wantsMod = b.mod === true;
|
|
1390
|
+
const hasMod = e.metaKey || e.ctrlKey;
|
|
1391
|
+
if (wantsMod !== hasMod) return false;
|
|
1392
|
+
const wantsAlt = b.alt === true;
|
|
1393
|
+
if (wantsAlt !== e.altKey) return false;
|
|
1394
|
+
const shift = b.shift;
|
|
1395
|
+
if (shift === void 0 || shift === false) {
|
|
1396
|
+
if (e.shiftKey) return false;
|
|
1397
|
+
} else if (shift === true) {
|
|
1398
|
+
if (!e.shiftKey) return false;
|
|
1399
|
+
}
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/ingestion/ingestItems.ts
|
|
1404
|
+
var INGEST_STRING_MIMES = /* @__PURE__ */ new Set([
|
|
1405
|
+
"text/plain",
|
|
1406
|
+
"text/html",
|
|
1407
|
+
"text/uri-list"
|
|
1408
|
+
]);
|
|
1409
|
+
var FALLBACK_MIME = "application/octet-stream";
|
|
1410
|
+
function normalizeMime(raw) {
|
|
1411
|
+
const bare = raw.split(";")[0].trim().toLowerCase();
|
|
1412
|
+
return bare || FALLBACK_MIME;
|
|
1413
|
+
}
|
|
1414
|
+
function itemsFromFiles(files) {
|
|
1415
|
+
return files.map((file) => ({ kind: "file", mime: normalizeMime(file.type), file }));
|
|
1416
|
+
}
|
|
1417
|
+
function itemsFromDataTransfer(dt) {
|
|
1418
|
+
const files = [];
|
|
1419
|
+
const strings = [];
|
|
1420
|
+
const reads = [];
|
|
1421
|
+
for (const item of Array.from(dt.items ?? [])) {
|
|
1422
|
+
if (item.kind === "file") {
|
|
1423
|
+
const file = item.getAsFile();
|
|
1424
|
+
if (file) files.push({ kind: "file", mime: normalizeMime(file.type), file });
|
|
1425
|
+
} else if (item.kind === "string") {
|
|
1426
|
+
const mime = normalizeMime(item.type);
|
|
1427
|
+
if (INGEST_STRING_MIMES.has(mime)) {
|
|
1428
|
+
const slot = strings.length;
|
|
1429
|
+
strings.push(null);
|
|
1430
|
+
reads.push(
|
|
1431
|
+
new Promise((resolve) => {
|
|
1432
|
+
item.getAsString((text) => {
|
|
1433
|
+
strings[slot] = { kind: "string", mime, text };
|
|
1434
|
+
resolve();
|
|
1435
|
+
});
|
|
1436
|
+
})
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (files.length === 0 && reads.length === 0) {
|
|
1442
|
+
for (const file of Array.from(dt.files ?? [])) {
|
|
1443
|
+
files.push({ kind: "file", mime: normalizeMime(file.type), file });
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return Promise.all(reads).then(() => [...files, ...strings]);
|
|
1447
|
+
}
|
|
1448
|
+
function itemsFromClipboardData(cd) {
|
|
1449
|
+
const out = [];
|
|
1450
|
+
for (const file of Array.from(cd.files ?? [])) {
|
|
1451
|
+
out.push({ kind: "file", mime: normalizeMime(file.type), file });
|
|
1452
|
+
}
|
|
1453
|
+
for (const mime of INGEST_STRING_MIMES) {
|
|
1454
|
+
const text = cd.getData(mime);
|
|
1455
|
+
if (text) out.push({ kind: "string", mime, text });
|
|
1456
|
+
}
|
|
1457
|
+
return out;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// src/viewport/clientToCanvas.ts
|
|
1461
|
+
function clientToCanvas(canvas, clientX, clientY) {
|
|
1462
|
+
const rect = canvas.getBoundingClientRect();
|
|
1463
|
+
return [clientX - rect.left, clientY - rect.top];
|
|
1464
|
+
}
|
|
1465
|
+
function clientToCanvasRect(rect, clientX, clientY) {
|
|
1466
|
+
return [clientX - rect.left, clientY - rect.top];
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// src/dlog.ts
|
|
1470
|
+
var KEYS = ["weasel.debug", "weaseldraw.debug"];
|
|
1471
|
+
var cached = null;
|
|
1472
|
+
function read() {
|
|
1473
|
+
const empty = { all: false, set: /* @__PURE__ */ new Set() };
|
|
1474
|
+
if (typeof localStorage === "undefined") return empty;
|
|
1475
|
+
try {
|
|
1476
|
+
for (const k of KEYS) {
|
|
1477
|
+
const v = localStorage.getItem(k);
|
|
1478
|
+
if (v == null || v === "") continue;
|
|
1479
|
+
if (v === "1" || v === "true" || v === "*") return { all: true, set: /* @__PURE__ */ new Set() };
|
|
1480
|
+
return { all: false, set: new Set(v.split(/[\s,]+/).filter(Boolean)) };
|
|
1481
|
+
}
|
|
1482
|
+
return empty;
|
|
1483
|
+
} catch {
|
|
1484
|
+
return empty;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function get() {
|
|
1488
|
+
if (cached === null) cached = read();
|
|
1489
|
+
return cached;
|
|
1490
|
+
}
|
|
1491
|
+
function isDebugEnabled(namespace) {
|
|
1492
|
+
const c = get();
|
|
1493
|
+
if (c.all) return true;
|
|
1494
|
+
if (namespace === void 0) return c.set.size > 0;
|
|
1495
|
+
return c.set.has(namespace);
|
|
1496
|
+
}
|
|
1497
|
+
function dlog(namespace, ...args) {
|
|
1498
|
+
if (!isDebugEnabled(namespace)) return;
|
|
1499
|
+
console.debug(`[${namespace}]`, ...args);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
export { ALWAYS, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, INGEST_STRING_MIMES, NEVER, PREDICATE_TARGET, TOOL_OFFHAND_ID, actionBindings, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, clientToCanvas, clientToCanvasRect, createDispatcher, describeRule, dlog, evaluate, filterEligible, findConflicts, isDev, isEditableTarget, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchesKeyBinding, offhandKeyFor, openPointerSession, pointerGestureId, recordModeSwitch, reportRouteConflicts, resolveDeviceProfile, resolveParams, routeGestureForSpecKind, routesForSpec, scopeBindings, specificity, targetConsultsAffordance, validateActionDefaultBinding, validateActionId };
|
|
1503
|
+
//# sourceMappingURL=chunk-Y5FHAA4U.js.map
|
|
1504
|
+
//# sourceMappingURL=chunk-Y5FHAA4U.js.map
|