@weasel-js/core 0.6.0 → 0.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/CHANGELOG.md +233 -0
- package/dist/{DrawCommand-Dl0bXNfS.d.ts → DrawCommand-CJtqqt8H.d.ts} +26 -2
- package/dist/{chunk-7V6JEOXE.js → chunk-2J6V527H.js} +10033 -10912
- package/dist/chunk-2J6V527H.js.map +1 -0
- package/dist/chunk-SYM6RAM4.js +232 -0
- package/dist/chunk-SYM6RAM4.js.map +1 -0
- package/dist/clone.d.ts +1 -1
- package/dist/geometry-D9BDMiQi.d.ts +114 -0
- package/dist/{grid-Cf87knjU.d.ts → grid-CaSK9bHV.d.ts} +1 -1
- package/dist/index-DOYRTfP0.d.ts +2986 -0
- package/dist/index.css +0 -35
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1174 -2481
- package/dist/index.js +2 -2
- package/dist/insert.d.ts +2 -2
- package/dist/move.d.ts +3 -3
- package/dist/{options-BPPBWMa7.d.ts → options-DMWeTELe.d.ts} +1 -1
- package/dist/{pointSnapToGrid-D7s7QmOF.d.ts → pointSnapToGrid-C3EruUwt.d.ts} +3 -54
- package/dist/renderer.css +0 -35
- package/dist/renderer.css.map +1 -1
- package/dist/renderer.d.ts +30 -5
- package/dist/renderer.js +2 -2
- package/dist/resize.d.ts +3 -3
- package/dist/routing.d.ts +10 -8
- package/dist/routing.js +1 -1
- package/dist/{types-BjUi2vA-.d.ts → types-Dcaa0tPq.d.ts} +1 -25
- package/dist/{registerFont-CP-wCsrz.d.ts → viewToMat3-D4lrBigW.d.ts} +2 -44
- package/package.json +6 -5
- package/dist/chunk-7V6JEOXE.js.map +0 -1
- package/dist/chunk-AM6ARSPN.js +0 -517
- package/dist/chunk-AM6ARSPN.js.map +0 -1
- package/dist/fitViewToBounds-evGsnR8Q.d.ts +0 -62
- package/dist/index-DZBYMsHI.d.ts +0 -1555
- package/dist/routing.css +0 -35
- package/dist/routing.css.map +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { __export } from './chunk-PZ5AY32C.js';
|
|
2
|
+
import { parseRoute, isKnownGestureName, getGestureDescriptor, formatRoute, formatPhaseAtom, describeRouteParts, describeRoute, collapseShiftPairs, canonicalModifiers, ROUTE_TERMS, ROUTE_FIELD_DEFINITIONS, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, GESTURE_DESCRIPTORS } from '@weasel-js/gestures';
|
|
3
|
+
export { GESTURE_DESCRIPTORS, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, parseRoute } from '@weasel-js/gestures';
|
|
4
|
+
|
|
5
|
+
// src/tools/routing/index.ts
|
|
6
|
+
var routing_exports = {};
|
|
7
|
+
__export(routing_exports, {
|
|
8
|
+
GESTURE_DESCRIPTORS: () => GESTURE_DESCRIPTORS,
|
|
9
|
+
PREDICATE_TARGET: () => PREDICATE_TARGET,
|
|
10
|
+
RESERVED_ID_NAMES: () => RESERVED_ID_NAMES,
|
|
11
|
+
RESERVED_ID_PREFIXES: () => RESERVED_ID_PREFIXES,
|
|
12
|
+
ROUTE_FIELD_DEFINITIONS: () => ROUTE_FIELD_DEFINITIONS,
|
|
13
|
+
ROUTE_TERMS: () => ROUTE_TERMS,
|
|
14
|
+
buildRouteRegistry: () => buildRouteRegistry,
|
|
15
|
+
canonicalModifiers: () => canonicalModifiers,
|
|
16
|
+
collapseShiftPairs: () => collapseShiftPairs,
|
|
17
|
+
defineTool: () => defineTool,
|
|
18
|
+
defineViewportTool: () => defineViewportTool,
|
|
19
|
+
describeRoute: () => describeRoute,
|
|
20
|
+
describeRouteParts: () => describeRouteParts,
|
|
21
|
+
findConflicts: () => findConflicts,
|
|
22
|
+
findScopedConflicts: () => findScopedConflicts,
|
|
23
|
+
formatConflict: () => formatConflict,
|
|
24
|
+
formatPhaseAtom: () => formatPhaseAtom,
|
|
25
|
+
formatRoute: () => formatRoute,
|
|
26
|
+
getGestureDescriptor: () => getGestureDescriptor,
|
|
27
|
+
isKnownGestureName: () => isKnownGestureName,
|
|
28
|
+
parseRoute: () => parseRoute,
|
|
29
|
+
reportRouteConflicts: () => reportRouteConflicts
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// src/tools/routing/defineTool.ts
|
|
33
|
+
function validateId(id, kind) {
|
|
34
|
+
if (id.length === 0) {
|
|
35
|
+
throw new Error(`weasel: ${kind} id may not be empty`);
|
|
36
|
+
}
|
|
37
|
+
if (RESERVED_ID_PREFIXES.has(id[0])) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`weasel: ${kind} id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (RESERVED_ID_NAMES.has(id)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`weasel: ${kind} id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function defineTool(def) {
|
|
49
|
+
validateId(def.id, "tool");
|
|
50
|
+
const resolveCursor = (ctx) => {
|
|
51
|
+
if (def.cursor == null) return "";
|
|
52
|
+
return typeof def.cursor === "function" ? def.cursor(ctx) : def.cursor;
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
id: def.id,
|
|
56
|
+
capabilities: def.capabilities,
|
|
57
|
+
actions: def.actions,
|
|
58
|
+
def,
|
|
59
|
+
presentation: def.presentation,
|
|
60
|
+
keybinding: def.keybinding,
|
|
61
|
+
onActivate: def.onActivate,
|
|
62
|
+
onDeactivate: def.onDeactivate,
|
|
63
|
+
initScratch: def.initScratch ?? (() => null),
|
|
64
|
+
cursor: resolveCursor,
|
|
65
|
+
bindings: def.bindings,
|
|
66
|
+
overlay: def.overlay
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/tools/routing/defineViewportTool.ts
|
|
71
|
+
function defineViewportTool(def) {
|
|
72
|
+
return defineTool(def);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/tools/routing/reflection/registry.ts
|
|
76
|
+
var PREDICATE_TARGET = "predicate";
|
|
77
|
+
var SPEC_KIND_TO_GESTURE = {
|
|
78
|
+
key: "keyDown",
|
|
79
|
+
"key-held": "keyHeld",
|
|
80
|
+
wheel: "wheel",
|
|
81
|
+
click: "click",
|
|
82
|
+
doubleClick: "dblTap",
|
|
83
|
+
contextMenu: "contextMenu",
|
|
84
|
+
drag: "drag",
|
|
85
|
+
pointerDown: "pointerDown",
|
|
86
|
+
multiTouch: void 0,
|
|
87
|
+
multiTouchTap: "multiTouchTap",
|
|
88
|
+
drop: void 0,
|
|
89
|
+
paste: void 0
|
|
90
|
+
};
|
|
91
|
+
function buildRouteRegistry(tools) {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const tool of tools) {
|
|
94
|
+
for (const binding of tool.bindings ?? []) {
|
|
95
|
+
const entry = entryFor(tool.id, binding.spec, binding.actionId);
|
|
96
|
+
if (entry) out.push(entry);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
function entryFor(toolId, spec, actionId) {
|
|
102
|
+
const gesture = SPEC_KIND_TO_GESTURE[spec.kind];
|
|
103
|
+
if (!gesture || !isKnownGestureName(gesture)) return null;
|
|
104
|
+
const descriptor = getGestureDescriptor(gesture);
|
|
105
|
+
return {
|
|
106
|
+
toolId,
|
|
107
|
+
actionId,
|
|
108
|
+
gesture,
|
|
109
|
+
phase: phaseOf("phase" in spec ? spec.phase : void 0),
|
|
110
|
+
modifiers: parseModSpec("mods" in spec ? spec.mods : void 0),
|
|
111
|
+
arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : void 0,
|
|
112
|
+
target: descriptor.hasTarget ? targetOf("target" in spec ? spec.target : void 0) : void 0,
|
|
113
|
+
spec
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function phaseOf(phase) {
|
|
117
|
+
if (phase === void 0 || phase === "*") return "any";
|
|
118
|
+
if (phase === "initial" || phase === "engaged") return phase;
|
|
119
|
+
const distinct = new Set(phase.map((atom) => atom.phase));
|
|
120
|
+
if (distinct.size !== 1) return "any";
|
|
121
|
+
const only = [...distinct][0];
|
|
122
|
+
return only === "initial" || only === "engaged" ? only : "any";
|
|
123
|
+
}
|
|
124
|
+
function parseModSpec(mods) {
|
|
125
|
+
if (!mods) return {};
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const [name, req] of Object.entries(mods)) {
|
|
128
|
+
if (req === true) out[name] = "required";
|
|
129
|
+
else if (req === "optional") out[name] = "optional";
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function targetOf(target) {
|
|
134
|
+
if (target === void 0) return void 0;
|
|
135
|
+
return typeof target === "string" ? target : PREDICATE_TARGET;
|
|
136
|
+
}
|
|
137
|
+
function argOf(spec, fallback) {
|
|
138
|
+
if ("key" in spec) {
|
|
139
|
+
return Array.isArray(spec.key) ? spec.key.join("|") : spec.key;
|
|
140
|
+
}
|
|
141
|
+
if ("fingers" in spec) return String(spec.fingers);
|
|
142
|
+
if ("direction" in spec) return spec.direction ?? fallback;
|
|
143
|
+
return fallback;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/tools/routing/reflection/conflicts.ts
|
|
147
|
+
function findConflicts(tools) {
|
|
148
|
+
const entries = buildRouteRegistry(tools);
|
|
149
|
+
const groups = /* @__PURE__ */ new Map();
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
const key = `${entry.phase}|${entry.gesture}|${entry.arg ?? ""}|${targetKey(entry)}|${canonicalModifiers(entry.modifiers)}`;
|
|
152
|
+
const bucket = groups.get(key);
|
|
153
|
+
if (bucket) bucket.push(entry);
|
|
154
|
+
else groups.set(key, [entry]);
|
|
155
|
+
}
|
|
156
|
+
const conflicts = [];
|
|
157
|
+
for (const bucket of groups.values()) {
|
|
158
|
+
if (bucket.length < 2) continue;
|
|
159
|
+
const first = bucket[0];
|
|
160
|
+
conflicts.push({
|
|
161
|
+
phase: first.phase,
|
|
162
|
+
gesture: first.gesture,
|
|
163
|
+
arg: first.arg,
|
|
164
|
+
target: first.target,
|
|
165
|
+
modifiers: first.modifiers,
|
|
166
|
+
toolIds: bucket.map((e) => e.toolId)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return conflicts;
|
|
170
|
+
}
|
|
171
|
+
function targetKey(entry) {
|
|
172
|
+
if (entry.target !== PREDICATE_TARGET) return entry.target ?? "";
|
|
173
|
+
const spec = entry.spec;
|
|
174
|
+
const pred = spec.target;
|
|
175
|
+
if (typeof pred !== "object" || pred === null) return PREDICATE_TARGET;
|
|
176
|
+
return `${PREDICATE_TARGET}#${predicateId(pred)}`;
|
|
177
|
+
}
|
|
178
|
+
var PREDICATE_IDS = /* @__PURE__ */ new WeakMap();
|
|
179
|
+
var nextPredicateId = 0;
|
|
180
|
+
function predicateId(pred) {
|
|
181
|
+
let id = PREDICATE_IDS.get(pred);
|
|
182
|
+
if (id === void 0) {
|
|
183
|
+
id = nextPredicateId++;
|
|
184
|
+
PREDICATE_IDS.set(pred, id);
|
|
185
|
+
}
|
|
186
|
+
return id;
|
|
187
|
+
}
|
|
188
|
+
function findScopedConflicts(scopes) {
|
|
189
|
+
const registry = Array.isArray(scopes.registry) ? scopes.registry : Object.values(scopes.registry);
|
|
190
|
+
const ambient = scopes.ambient ?? [];
|
|
191
|
+
const out = [];
|
|
192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
193
|
+
const add = (conflicts) => {
|
|
194
|
+
for (const c of conflicts) {
|
|
195
|
+
const key = `${c.phase}|${c.gesture}|${c.arg ?? ""}|${c.target ?? ""}|${canonicalModifiers(c.modifiers)}|${c.toolIds.join(",")}`;
|
|
196
|
+
if (seen.has(key)) continue;
|
|
197
|
+
seen.add(key);
|
|
198
|
+
out.push(c);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
for (const tool of [...registry, ...ambient]) add(findConflicts([tool]));
|
|
202
|
+
if (ambient.length > 1) add(findConflicts(ambient));
|
|
203
|
+
const hotkeyTools = registry.filter(
|
|
204
|
+
(t) => t.def?.hotkey !== void 0
|
|
205
|
+
);
|
|
206
|
+
if (hotkeyTools.length > 1) add(findConflicts(hotkeyTools));
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
function formatConflict(conflict) {
|
|
210
|
+
const route = formatRoute({
|
|
211
|
+
phases: [{ channel: "&", phase: conflict.phase === "any" ? "*" : conflict.phase }],
|
|
212
|
+
gesture: conflict.gesture,
|
|
213
|
+
arg: conflict.arg,
|
|
214
|
+
target: conflict.target,
|
|
215
|
+
modifiers: conflict.modifiers
|
|
216
|
+
});
|
|
217
|
+
return `${route} \u2014 declared by ${conflict.toolIds.join(", ")}`;
|
|
218
|
+
}
|
|
219
|
+
function reportRouteConflicts(scopes, warn = (m) => console.warn(m)) {
|
|
220
|
+
const conflicts = findScopedConflicts(scopes);
|
|
221
|
+
for (const c of conflicts) {
|
|
222
|
+
warn(
|
|
223
|
+
`[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.
|
|
224
|
+
${formatConflict(c)}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return conflicts;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export { PREDICATE_TARGET, buildRouteRegistry, defineTool, defineViewportTool, findConflicts, findScopedConflicts, formatConflict, reportRouteConflicts, routing_exports };
|
|
231
|
+
//# sourceMappingURL=chunk-SYM6RAM4.js.map
|
|
232
|
+
//# sourceMappingURL=chunk-SYM6RAM4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/routing/index.ts","../src/tools/routing/defineTool.ts","../src/tools/routing/defineViewportTool.ts","../src/tools/routing/reflection/registry.ts","../src/tools/routing/reflection/conflicts.ts"],"names":[],"mappings":"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","file":"chunk-SYM6RAM4.js","sourcesContent":["export type { ToolDef, ViewportToolDef, ToolKeybinding } from './types';\nexport { parseRoute, formatRoute, formatPhaseAtom, collapseShiftPairs, describeRoute, describeRouteParts, canonicalModifiers, ROUTE_TERMS, ROUTE_FIELD_DEFINITIONS, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES } from './routeGrammar';\nexport type { ParsedRoute, ParsedModifiers, ModifierKey, ModRequirement, PhaseAtom, ChannelRef, DescribeRouteOptions, RouteDescriptionPart, RouteTermLabel, RouteFieldName } from './routeGrammar';\nexport { getGestureDescriptor, isKnownGestureName, GESTURE_DESCRIPTORS } from './gestures';\nexport type { GestureName, GestureDescriptor, GestureArgSpec } from './gestures';\nexport { defineTool } from './defineTool';\nexport { defineViewportTool } from './defineViewportTool';\n\n// Reflection consumers — registry / conflict checker / debug overlay.\nexport * from './reflection';\n","// src/tools/routing/defineTool.ts\nimport type { Tool, ToolCtx } from '../types';\nimport type { ToolDef } from './types';\nimport { RESERVED_ID_NAMES, RESERVED_ID_PREFIXES } from './routeGrammar';\n\n/** Validate that `id` is usable as a tool / channel id in the route\n * grammar. Rejects ids that start with a reserved sigil (would shadow\n * future grammar extensions) and ids that collide with phase keywords\n * (would parse as the bare-phase shorthand). */\nfunction validateId(id: string, kind: 'tool' | 'action'): void {\n if (id.length === 0) {\n throw new Error(`weasel: ${kind} id may not be empty`);\n }\n if (RESERVED_ID_PREFIXES.has(id[0]!)) {\n throw new Error(\n `weasel: ${kind} id \"${id}\" starts with reserved sigil \"${id[0]}\" ` +\n `(reserved set: ${[...RESERVED_ID_PREFIXES].join(' ')})`,\n );\n }\n if (RESERVED_ID_NAMES.has(id)) {\n throw new Error(\n `weasel: ${kind} id \"${id}\" collides with a reserved phase keyword ` +\n `(reserved: ${[...RESERVED_ID_NAMES].join(', ')})`,\n );\n }\n}\n\n/**\n * Build a `Tool<TScratch>` from a declarative `ToolDef<TScratch>`.\n *\n * This used to be a translator: `ToolDef` carried `initial` / `engaged` phase\n * tables of hit-keyed route handlers, and `defineTool` compiled them into the\n * imperative `pointer` / `drag` / `keyboard` / `wheel` handlers the\n * tool-routing dispatcher called. Both ends of that are gone — tools declare\n * `bindings` and the gesture dispatcher routes them — so what remains is\n * identity plumbing plus the id check and the cursor resolver.\n *\n * It stays a function rather than becoming a spread because the id validation\n * and the `initScratch` / `cursor` defaults are worth applying uniformly, and\n * because `Tool.def` gives reflection a handle on the authored form.\n */\nexport function defineTool<TScratch = void>(\n def: ToolDef<TScratch>,\n): Tool<TScratch> {\n validateId(def.id, 'tool');\n\n // Normalize `string | ((ctx) => string)` to the function form so callers\n // have one shape to deal with. Returns '' (not undefined) so the signature\n // satisfies `Tool.cursor: (ctx) => string`.\n const resolveCursor = (ctx: ToolCtx<TScratch>): string => {\n if (def.cursor == null) return '';\n return typeof def.cursor === 'function' ? def.cursor(ctx) : def.cursor;\n };\n\n return {\n id: def.id,\n capabilities: def.capabilities,\n actions: def.actions,\n def,\n presentation: def.presentation,\n keybinding: def.keybinding,\n onActivate: def.onActivate,\n onDeactivate: def.onDeactivate,\n initScratch: def.initScratch ?? (() => null as unknown as TScratch),\n cursor: resolveCursor,\n bindings: def.bindings,\n overlay: def.overlay,\n };\n}\n","import type { Tool } from '../types';\nimport type { ViewportToolDef } from './types';\nimport { defineTool } from './defineTool';\n\n/**\n * Define a tool that acts on the viewport rather than the scene.\n *\n * This used to do real work: `ViewportPhaseDef` was a narrowed `PhaseDef`\n * (no click routes, drag restricted to the function form), and the factory\n * lifted it back to the permissive shape before handing it to `defineTool`.\n * With phase tables gone there is no shape left to narrow — a viewport tool\n * declares `bindings` like any other, pointing at `viewport.*` actions.\n *\n * It survives as an authoring signal. `defineViewportTool` at the top of a\n * hook says \"this tool moves the camera, not the drawing\", which is worth\n * more than the type gymnastics it replaced.\n */\nexport function defineViewportTool<TScratch = void>(\n def: ViewportToolDef<TScratch>,\n): Tool<TScratch> {\n return defineTool<TScratch>(def);\n}\n","import type { Tool } from '../../types';\nimport type { GestureSpec, ModSpec, TargetSpec, PhaseSpec } from '@weasel-js/gestures';\nimport type { ParsedModifiers, ModifierKey } from '../routeGrammar';\nimport { getGestureDescriptor, isKnownGestureName, type GestureName } from '../gestures';\n\n/**\n * One row in the route registry — a single `GestureBinding` on one tool,\n * flattened into the route grammar's vocabulary so the inspector, the\n * conflict checker, and `describeRoute` can all read the same shape.\n *\n * Multiple rows can share (gesture, arg, target, modifiers) if different tools\n * declare them; consumers walk the list and group client-side.\n */\nexport interface RegistryEntry {\n toolId: string;\n /** Which phase the binding's `phase` spec restricts it to. `'any'` when it\n * declares none, declares `'*'`, or declares an atom list whose atoms\n * don't agree on one phase — such a binding fires in either phase, and\n * reporting it as `'initial'` made it collide with genuinely-initial\n * bindings in `findConflicts`' bucket key. */\n phase: 'initial' | 'engaged' | 'any';\n /** Structured v3 modifier requirements. Empty object = \"no modifiers\n * held\" (the strict default). */\n modifiers: ParsedModifiers;\n gesture: GestureName;\n /** Resolved arg value for arg-bearing gestures (wheel direction,\n * key name, multiTouchTap fingers). Undefined for gestures whose\n * descriptor has no `arg`. */\n arg: string | undefined;\n /** Target class for hit-testing gestures. `undefined` when the descriptor\n * has `hasTarget: false` or the spec declares no target;\n * {@link PREDICATE_TARGET} when the spec uses a `kindOf` predicate, which\n * the route grammar has no notation for. */\n target: string | undefined;\n /** The action this binding fires. */\n actionId: string;\n /** The `GestureSpec` this row was flattened from, by reference. Reflection\n * consumers that need something the grammar doesn't capture — the\n * specificity tuple, a `kindOf` predicate identity — read it here rather\n * than re-walking `Tool.bindings`. */\n spec: GestureSpec;\n}\n\n/**\n * Stand-in target for a `{ kindOf }` predicate spec.\n *\n * The route grammar can name target *classes* (`empty`, `selected-body`,\n * `kind:rect`) but not arbitrary predicates, so three distinct select-tool\n * bindings all render as this one token. Narrowing it would mean giving the\n * grammar a way to describe a function, which is a real design question and\n * not one this reflection layer should answer by inventing syntax.\n */\nexport const PREDICATE_TARGET = 'predicate';\n\n/** `GestureSpec.kind` → route-grammar gesture name. `multiTouch` has no\n * route-grammar gesture (only its tap synthesis does), and `drop` / `paste`\n * shipped without grammar names, so specs of those kinds are skipped. */\nconst SPEC_KIND_TO_GESTURE: Record<GestureSpec['kind'], GestureName | undefined> = {\n key: 'keyDown',\n 'key-held': 'keyHeld',\n wheel: 'wheel',\n click: 'click',\n doubleClick: 'dblTap',\n contextMenu: 'contextMenu',\n drag: 'drag',\n pointerDown: 'pointerDown',\n multiTouch: undefined,\n multiTouchTap: 'multiTouchTap',\n drop: undefined,\n paste: undefined,\n};\n\n/**\n * Flatten every tool's `bindings` into route-registry rows.\n *\n * This was `buildActionRegistry`, and it walked `ToolDef.initial` /\n * `.engaged` phase tables — the grammar that no longer exists. It was also\n * blind to `Tool.bindings` the entire time the two lived side by side, which\n * is why the inspector under-reported select by more than half. The rename\n * fixes a second thing: it never had anything to do with the Actions\n * Registry, and reading `buildActionRegistry` next to `ActionsRegistry`\n * suggested otherwise.\n */\nexport function buildRouteRegistry(\n tools: readonly Tool<unknown>[],\n): RegistryEntry[] {\n const out: RegistryEntry[] = [];\n for (const tool of tools) {\n for (const binding of tool.bindings ?? []) {\n const entry = entryFor(tool.id, binding.spec, binding.actionId);\n if (entry) out.push(entry);\n }\n }\n return out;\n}\n\nfunction entryFor(\n toolId: string,\n spec: GestureSpec,\n actionId: string,\n): RegistryEntry | null {\n const gesture = SPEC_KIND_TO_GESTURE[spec.kind];\n if (!gesture || !isKnownGestureName(gesture)) return null;\n const descriptor = getGestureDescriptor(gesture);\n return {\n toolId,\n actionId,\n gesture,\n phase: phaseOf('phase' in spec ? spec.phase : undefined),\n modifiers: parseModSpec('mods' in spec ? spec.mods : undefined),\n arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : undefined,\n target: descriptor.hasTarget\n ? targetOf('target' in spec ? spec.target : undefined)\n : undefined,\n spec,\n };\n}\n\n/** Collapse a `PhaseSpec` to the single phase a binding is restricted to, or\n * `'any'` when it isn't restricted to one.\n *\n * `PhaseAtom.channel` says which channel's phase state the binding reads,\n * not which phase it fires in, so it plays no part in this collapse — only\n * the set of distinct `atom.phase` values matters. */\nfunction phaseOf(phase: PhaseSpec | undefined): 'initial' | 'engaged' | 'any' {\n if (phase === undefined || phase === '*') return 'any';\n if (phase === 'initial' || phase === 'engaged') return phase;\n const distinct = new Set(phase.map((atom) => atom.phase));\n if (distinct.size !== 1) return 'any';\n const only = [...distinct][0];\n return only === 'initial' || only === 'engaged' ? only : 'any';\n}\n\n/** `ModSpec` (per-key tri-state) → `ParsedModifiers`. `false` and absent both\n * mean \"must not be held\", which the parsed form spells as an absent key. */\nfunction parseModSpec(mods: ModSpec | undefined): ParsedModifiers {\n if (!mods) return {};\n const out: ParsedModifiers = {};\n for (const [name, req] of Object.entries(mods)) {\n if (req === true) out[name as ModifierKey] = 'required';\n else if (req === 'optional') out[name as ModifierKey] = 'optional';\n }\n return out;\n}\n\nfunction targetOf(target: TargetSpec | undefined): string | undefined {\n if (target === undefined) return undefined;\n return typeof target === 'string' ? target : PREDICATE_TARGET;\n}\n\nfunction argOf(spec: GestureSpec, fallback: string | undefined): string | undefined {\n if ('key' in spec) {\n return Array.isArray(spec.key) ? spec.key.join('|') : spec.key;\n }\n if ('fingers' in spec) return String(spec.fingers);\n if ('direction' in spec) return spec.direction ?? fallback;\n return fallback;\n}\n\n// Re-export for downstream consumers.\nexport { getGestureDescriptor };\nexport type { GestureName };\n","import type { Tool } from '../../types';\nimport type { ParsedModifiers } from '../routeGrammar';\nimport { canonicalModifiers, formatRoute } from '../routeGrammar';\nimport { buildRouteRegistry, PREDICATE_TARGET, type RegistryEntry, type GestureName } from './registry';\n\n/** Two or more tools declare the same exact (phase, gesture, arg, target,\n * modifiers) tuple — the dispatcher's slot precedence picks one\n * arbitrarily (well, deterministically by slot order, but the author\n * probably didn't intend the duplication). */\nexport interface Conflict {\n phase: 'initial' | 'engaged' | 'any';\n gesture: GestureName;\n arg: string | undefined;\n target: string | undefined;\n modifiers: ParsedModifiers;\n /** All tool ids that registered the same tuple. At least 2 by\n * construction. Order matches the input tools[] order. */\n toolIds: string[];\n}\n\n/** Detect exact-tuple overlaps across a tool registration set.\n *\n * Intentionally NOT flagged:\n * - Broad vs. narrow targets (e.g. an untargeted `click` alongside\n * `click` on `empty`) — the dispatcher's specificity ordering resolves\n * those cleanly, and the broad one is usually the intended fallback.\n * - Different modifier requirements on the same target — they fire on\n * different inputs.\n * - A binding whose action declines via `enabled()` so a lower-priority\n * one can take the gesture. Detecting that intent would mean evaluating\n * the action; consumers can suppress known-intentional compositions in\n * their UI layer.\n *\n * - Two `{ kindOf }` predicate targets. The route grammar renders every\n * predicate as the single token {@link PREDICATE_TARGET}, so bucketing on\n * the rendered target alone reported select's `resize` / `rotate` / `move`\n * drags — three genuinely different predicates — as a three-way conflict.\n * Predicate entries bucket by function identity instead, which means two\n * *separately written but equivalent* predicates go unflagged. That's the\n * right way to be wrong here: this check has to be silent when nothing is\n * wrong or nobody will keep it on.\n *\n * Note that two bindings sharing a tuple on the SAME tool are now possible\n * (bindings are an array, where phase tables were objects with unique\n * keys) — so a conflict may name one tool twice.\n *\n * This is the raw same-tuple detector. Feeding it a whole tool *registry*\n * over-reports, because registry tools take turns in the active slot and\n * can't collide with each other — see {@link findScopedConflicts}.\n */\nexport function findConflicts(\n tools: readonly Tool<unknown>[],\n): Conflict[] {\n const entries = buildRouteRegistry(tools);\n const groups = new Map<string, RegistryEntry[]>();\n for (const entry of entries) {\n const key = `${entry.phase}|${entry.gesture}|${entry.arg ?? ''}|${targetKey(entry)}|${canonicalModifiers(entry.modifiers)}`;\n const bucket = groups.get(key);\n if (bucket) bucket.push(entry);\n else groups.set(key, [entry]);\n }\n const conflicts: Conflict[] = [];\n for (const bucket of groups.values()) {\n if (bucket.length < 2) continue;\n const first = bucket[0];\n conflicts.push({\n phase: first.phase,\n gesture: first.gesture,\n arg: first.arg,\n target: first.target,\n modifiers: first.modifiers,\n toolIds: bucket.map((e) => e.toolId),\n });\n }\n return conflicts;\n}\n\n/** Bucket key for an entry's target slot. Predicate targets all render as\n * the same grammar token, so they're keyed by the predicate's identity —\n * two different functions are two different targets. */\nfunction targetKey(entry: RegistryEntry): string {\n if (entry.target !== PREDICATE_TARGET) return entry.target ?? '';\n const spec = entry.spec as { target?: unknown };\n const pred = spec.target;\n if (typeof pred !== 'object' || pred === null) return PREDICATE_TARGET;\n return `${PREDICATE_TARGET}#${predicateId(pred)}`;\n}\n\nconst PREDICATE_IDS = new WeakMap<object, number>();\nlet nextPredicateId = 0;\n\nfunction predicateId(pred: object): number {\n let id = PREDICATE_IDS.get(pred);\n if (id === undefined) {\n id = nextPredicateId++;\n PREDICATE_IDS.set(pred, id);\n }\n return id;\n}\n\n/**\n * The tool scopes as the dispatcher sees them — which is what decides whether\n * two same-tuple bindings can actually collide.\n */\nexport interface ToolScopes {\n /** Tools eligible for the active slot, keyed by id or as a flat list. */\n registry: readonly Tool<unknown>[] | Readonly<Record<string, Tool<unknown>>>;\n /** Always-on tools. Every one of these is live at once. */\n ambient?: readonly Tool<unknown>[];\n}\n\n/**\n * Detect the same-tuple overlaps that are *reachable* — the ones where the\n * dispatcher really does fall back on declaration order.\n *\n * `matchSorted` walks scopes in strict priority (hotkey > active > ambient)\n * and only sorts by specificity *within* a scope. So a cross-scope tie isn't\n * a tie at all: an ambient tool losing a tuple to the active tool is the\n * documented design, not an accident. Likewise two registry tools sharing a\n * tuple — `rect` and `ellipse` both binding a bare `drag` — can never both be\n * in the active slot, so they never compete.\n *\n * What's left, and what this reports:\n * - a single tool colliding with **itself** (possible since bindings became\n * an array), which is ambiguous in whichever slot it occupies;\n * - two **ambient** tools, all of which are live simultaneously and are\n * ordered only by registration;\n * - two **hotkey-capable** tools, which can stack.\n *\n * Not covered: the actions registry's `defaultBinding`s, which the dispatcher\n * also folds into ambient/hotkey scope. They're assembled somewhere else\n * entirely (`ActionsRegistry`, not `useTools`), so catching tool-vs-action\n * collisions means giving this function a second input it doesn't have yet.\n */\nexport function findScopedConflicts(scopes: ToolScopes): Conflict[] {\n const registry = Array.isArray(scopes.registry)\n ? (scopes.registry as readonly Tool<unknown>[])\n : Object.values(scopes.registry as Readonly<Record<string, Tool<unknown>>>);\n const ambient = scopes.ambient ?? [];\n\n const out: Conflict[] = [];\n const seen = new Set<string>();\n const add = (conflicts: readonly Conflict[]): void => {\n for (const c of conflicts) {\n const key = `${c.phase}|${c.gesture}|${c.arg ?? ''}|${c.target ?? ''}`\n + `|${canonicalModifiers(c.modifiers)}|${c.toolIds.join(',')}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(c);\n }\n };\n\n // Self-collisions: every tool, whichever slot it lands in.\n for (const tool of [...registry, ...ambient]) add(findConflicts([tool]));\n // Ambient tools are all live together.\n if (ambient.length > 1) add(findConflicts(ambient));\n // Hotkey-capable tools can stack on each other. `hotkey` is declared on the\n // authored `ToolDef`, not carried onto the runtime `Tool` — `Tool.def` is\n // the reflection handle for exactly this kind of read, and it's typed\n // `unknown` so consumers cast at the use site.\n const hotkeyTools = registry.filter(\n (t) => (t.def as { hotkey?: unknown } | undefined)?.hotkey !== undefined,\n );\n if (hotkeyTools.length > 1) add(findConflicts(hotkeyTools));\n\n return out;\n}\n\n/**\n * Render a conflict as one line of human-readable text.\n *\n * The tuple is printed through {@link formatRoute}, so the message names the\n * collision in the same grammar the author wrote the binding in — modulo the\n * phase slot, which prints as a bare `'&'`-channel atom because the bucket key\n * collapses channel-bearing phase specs (`sel:engaged` and `&:engaged` collide\n * with each other, and the collapse is what made them collide).\n */\nexport function formatConflict(conflict: Conflict): string {\n const route = formatRoute({\n phases: [{ channel: '&', phase: conflict.phase === 'any' ? '*' : conflict.phase }],\n gesture: conflict.gesture,\n arg: conflict.arg,\n target: conflict.target,\n modifiers: conflict.modifiers,\n });\n return `${route} — declared by ${conflict.toolIds.join(', ')}`;\n}\n\n/**\n * Detect reachable route conflicts in an assembled tool set and report each one.\n *\n * This is the wiring `findConflicts` spent its first life without: the kit\n * could detect the one class of genuine routing ambiguity it has and never\n * looked. Call it once wherever a tool set is assembled (`useTools` does),\n * behind a `process.env.NODE_ENV !== 'production'` guard.\n *\n * **Warn, never throw.** A conflict between a consumer's tool and a kit tool\n * is a design question — sometimes deliberate, since the loser can still take\n * the gesture by declining through `enabled()` — and exploding in a running\n * app is the wrong way to raise it. A conflict between two *kit* tools is\n * always a bug, but the place to fail on that is the kit's own test suite\n * (`canvas/SceneCanvas.routeConflicts.test.tsx`), not a consumer's console.\n *\n * @returns The conflicts found, so callers can dedupe repeat reports.\n */\nexport function reportRouteConflicts(\n scopes: ToolScopes,\n warn: (message: string) => void = (m) => console.warn(m),\n): Conflict[] {\n const conflicts = findScopedConflicts(scopes);\n for (const c of conflicts) {\n warn(\n `[weasel] route conflict: two bindings declare the same (phase, gesture, arg, target, modifiers) tuple `\n + `in the same scope, so declaration order alone decides which one fires.\\n ${formatConflict(c)}`,\n );\n }\n return conflicts;\n}\n"]}
|
package/dist/clone.d.ts
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { V as View } from './view-DSQgxBJB.js';
|
|
2
|
+
import { R as ResizePose, a as RotatedPose } from './types-Dcaa0tPq.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Axis-aligned rectangle in world space. Kit-wide `Bounds` shape used by
|
|
6
|
+
* selection, group, and viewport helpers. The optional `rotation` field
|
|
7
|
+
* (radians, around the AABB center) lets selection chrome attach a rotated
|
|
8
|
+
* orientation to an otherwise axis-aligned rect without needing a parallel
|
|
9
|
+
* type.
|
|
10
|
+
*/
|
|
11
|
+
interface Bounds {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
rotation?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Pixel dimensions of the canvas viewport. */
|
|
19
|
+
interface ViewportDims {
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
}
|
|
23
|
+
/** Options for {@link fitViewToBounds}. */
|
|
24
|
+
interface FitViewToBoundsOptions {
|
|
25
|
+
/** Inset (in CSS px) around the bounds. Default 16. */
|
|
26
|
+
padding?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Override the kit's system max scale. Defaults to the kit-wide max zoom
|
|
29
|
+
* used by `computeWheelAction` / `useZoom` (currently `10`).
|
|
30
|
+
*/
|
|
31
|
+
maxScale?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Override the kit's system min scale. Defaults to the kit-wide min zoom
|
|
34
|
+
* used by `computeWheelAction` / `useZoom` (currently `0.1`).
|
|
35
|
+
*/
|
|
36
|
+
minScale?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Fit mode:
|
|
39
|
+
* - `'contain'` (default): uniform scale = `min(availW/w, availH/h)`. Bounds fit entirely; one axis has letterboxing.
|
|
40
|
+
* - `'fill'`: uniform scale = `max(...)`. Bounds overflow viewport on one axis.
|
|
41
|
+
* - `'stretch'`: per-axis scale. Bounds match viewport exactly; scale is non-uniform.
|
|
42
|
+
*/
|
|
43
|
+
mode?: 'contain' | 'fill' | 'stretch';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Compute the target {@link View} that fits `bounds` (world space) inside
|
|
47
|
+
* `viewportDims` (CSS px). The bounds are centered in the viewport with
|
|
48
|
+
* uniform padding on all sides; the resulting scale is clamped to the
|
|
49
|
+
* configured min/max.
|
|
50
|
+
*
|
|
51
|
+
* View semantics: `view.x` / `view.y` is the world point currently drawn at
|
|
52
|
+
* the canvas top-left, and `view.scale.{x,y}` is pixels per world unit on
|
|
53
|
+
* each axis (see `view.ts`). So:
|
|
54
|
+
* screenX = (worldX - view.x) * view.scale.x
|
|
55
|
+
* screenY = (worldY - view.y) * view.scale.y
|
|
56
|
+
*
|
|
57
|
+
* Empty / zero-sized bounds or viewports cannot be fit — in that case this
|
|
58
|
+
* returns `currentView` unchanged and logs a `console.warn`. Callers that
|
|
59
|
+
* want to no-op silently can detect zero area themselves before calling.
|
|
60
|
+
*/
|
|
61
|
+
declare function fitViewToBounds(bounds: Bounds, viewportDims: ViewportDims, currentView: View, opts?: FitViewToBoundsOptions): View;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Bridges arbitrary `TPose` shapes into the resize hook's bounds-driven math.
|
|
65
|
+
* The hook reads bounds via `getBounds`, runs anchor-relative math on those
|
|
66
|
+
* bounds, then asks `remapBounds` to project the result back into TPose.
|
|
67
|
+
*
|
|
68
|
+
* `remapBounds(pose, src, dst)` is a single operation that subsumes both
|
|
69
|
+
* "set my own AABB to dst" (single-leaf resize) and "scale me as a leaf
|
|
70
|
+
* inside parent's src→dst rect" (group resize) — they're the same affine
|
|
71
|
+
* map. For rect-shaped poses the default geometry interprets the pose as
|
|
72
|
+
* its own bounds; for Path or polygon poses the consumer supplies a
|
|
73
|
+
* projection that knows how to read and rewrite the underlying geometry.
|
|
74
|
+
*/
|
|
75
|
+
interface PoseProjection<TPose> {
|
|
76
|
+
getBounds(pose: TPose): ResizePose;
|
|
77
|
+
remapBounds(pose: TPose, src: ResizePose, dst: ResizePose): TPose;
|
|
78
|
+
/** Translate the pose by (dx, dy). Optional — when omitted, callers fall
|
|
79
|
+
* back to a translation derived from `remapBounds` (origin shifted, no
|
|
80
|
+
* scale). Path-shaped poses should provide this for performance. */
|
|
81
|
+
translate?(pose: TPose, dx: number, dy: number): TPose;
|
|
82
|
+
/** True iff any portion of the pose's geometry intersects `rect`. Optional
|
|
83
|
+
* — when omitted, area-select and similar callers test against `getBounds`
|
|
84
|
+
* AABB (looser, but correct for axis-aligned rect poses). */
|
|
85
|
+
intersectsRect?(pose: TPose, rect: ResizePose): boolean;
|
|
86
|
+
/** Interpolate between two poses. Optional — animation helpers fall back to
|
|
87
|
+
* rect-shape lerp when omitted (which fails for non-rect poses). */
|
|
88
|
+
lerp?(a: TPose, b: TPose, t: number): TPose;
|
|
89
|
+
/** Read the pose's rotation in radians. Pivot is the AABB center
|
|
90
|
+
* (`getBounds(pose)` center). Default 0 when omitted — descriptor
|
|
91
|
+
* declares "this pose has no rotation." When supplied and non-zero,
|
|
92
|
+
* `useResize` projects the drag delta into the leaf's local frame,
|
|
93
|
+
* runs anchor math there, and translates the resulting pose so the
|
|
94
|
+
* diagonally opposite world-space corner is pinned. */
|
|
95
|
+
getRotation?(pose: TPose): number;
|
|
96
|
+
/** True iff this pose shape can carry a rotation. Consulted by the
|
|
97
|
+
* rotation affordance to decide whether to render the rotate cursor /
|
|
98
|
+
* drag-band over a selection. When omitted, the kit assumes `true` for
|
|
99
|
+
* back-compat — descriptors whose poses lack `x/y/width/height/rotation`
|
|
100
|
+
* fields (e.g. polygon Paths) should return `false` so the affordance
|
|
101
|
+
* hides instead of exposing a non-functional rotate cursor. */
|
|
102
|
+
supportsRotation?(pose: TPose): boolean;
|
|
103
|
+
}
|
|
104
|
+
/** Identity geometry for `TPose extends ResizePose`. Treats the pose as its
|
|
105
|
+
* own bounds and remaps via affine scale against `src`/`dst`. */
|
|
106
|
+
declare const RECT_POSE_DESCRIPTOR: PoseProjection<ResizePose>;
|
|
107
|
+
/** Identity geometry for `RotatedPose`. Inherits rect-shape projection
|
|
108
|
+
* from `RECT_POSE_DESCRIPTOR` (the `RotatedPose extends ResizePose`
|
|
109
|
+
* subtype lets the rect descriptor's methods apply directly; `remapBounds`
|
|
110
|
+
* preserves the `rotation` field via `...p` spread). Adds `getRotation` so
|
|
111
|
+
* `useResize` knows to take the rotation-aware math path. */
|
|
112
|
+
declare const ROTATED_POSE_DESCRIPTOR: PoseProjection<RotatedPose>;
|
|
113
|
+
|
|
114
|
+
export { type Bounds as B, type FitViewToBoundsOptions as F, type PoseProjection as P, RECT_POSE_DESCRIPTOR as R, type ViewportDims as V, ROTATED_POSE_DESCRIPTOR as a, fitViewToBounds as f };
|