@weasel-js/core 0.7.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.
@@ -19,11 +19,14 @@ __export(routing_exports, {
19
19
  describeRoute: () => describeRoute,
20
20
  describeRouteParts: () => describeRouteParts,
21
21
  findConflicts: () => findConflicts,
22
+ findScopedConflicts: () => findScopedConflicts,
23
+ formatConflict: () => formatConflict,
22
24
  formatPhaseAtom: () => formatPhaseAtom,
23
25
  formatRoute: () => formatRoute,
24
26
  getGestureDescriptor: () => getGestureDescriptor,
25
27
  isKnownGestureName: () => isKnownGestureName,
26
- parseRoute: () => parseRoute
28
+ parseRoute: () => parseRoute,
29
+ reportRouteConflicts: () => reportRouteConflicts
27
30
  });
28
31
 
29
32
  // src/tools/routing/defineTool.ts
@@ -145,7 +148,7 @@ function findConflicts(tools) {
145
148
  const entries = buildRouteRegistry(tools);
146
149
  const groups = /* @__PURE__ */ new Map();
147
150
  for (const entry of entries) {
148
- const key = `${entry.phase}|${entry.gesture}|${entry.arg ?? ""}|${entry.target ?? ""}|${canonicalModifiers(entry.modifiers)}`;
151
+ const key = `${entry.phase}|${entry.gesture}|${entry.arg ?? ""}|${targetKey(entry)}|${canonicalModifiers(entry.modifiers)}`;
149
152
  const bucket = groups.get(key);
150
153
  if (bucket) bucket.push(entry);
151
154
  else groups.set(key, [entry]);
@@ -165,7 +168,65 @@ function findConflicts(tools) {
165
168
  }
166
169
  return conflicts;
167
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
+ }
168
229
 
169
- export { PREDICATE_TARGET, buildRouteRegistry, defineTool, defineViewportTool, findConflicts, routing_exports };
170
- //# sourceMappingURL=chunk-FSZEXVCR.js.map
171
- //# sourceMappingURL=chunk-FSZEXVCR.js.map
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"]}
@@ -122,6 +122,17 @@ interface AffordanceRegion<TScratch = unknown> {
122
122
  innerY: number;
123
123
  innerWidth: number;
124
124
  innerHeight: number;
125
+ /** Minimum band thickness outside the inner rect, in **screen**
126
+ * pixels. The framework widens `rx`/`ry` to at least
127
+ * `innerHalfExtent + minBandPx / meanScale(view.scale)` for both
128
+ * paint and hit-test.
129
+ *
130
+ * This exists because the clamp has to know the view and the
131
+ * affordance doesn't: `ChromeState` carries no scale. Expressing the
132
+ * floor in world units instead (which is what the rotate ring used
133
+ * to do) makes the band shrink on screen as you zoom in, until the
134
+ * ring around a small shape is too thin to hover. */
135
+ minBandPx?: number;
125
136
  };
126
137
  /** Optional paint. World position is derived from `shape` + target
127
138
  * transform; visual size stays in screen pixels (so handles don't
@@ -146,13 +157,16 @@ interface AffordanceRegion<TScratch = unknown> {
146
157
  kind: 'custom';
147
158
  draw: (ctx: CustomPaintContext) => DrawCommand[];
148
159
  };
149
- /** Declared CSS cursor for this region. NOTE: nothing consumes this
150
- * field yet the hover-cursor pump lives on the dispatcher pipeline
151
- * (`useGestureDispatcher`) and reads `AffordanceHit.cursor` from
152
- * `buildAffordanceAt`'s synthesized hits, which do not go through
153
- * region objects. Honoring this field is part of the pending
154
- * affordance-layer unification (see the selection-overlay note in
155
- * `docs/taxonomy.md`). */
160
+ /** Discriminator a press on this region reports as `AffordanceHit.kind`
161
+ * the string routing specs match on (`'handle:top-left'`,
162
+ * `'rotate-handle'`, `'anchor:3'`). Omit for regions that only exist
163
+ * inside a consumer-registered layer, where the layer id is the
164
+ * discriminator; `buildAffordanceAt` falls back to
165
+ * `<affordanceId>:<regionId>`. */
166
+ hitKind?: string;
167
+ /** CSS cursor to show while hovering this region. Read by the hover-cursor
168
+ * pump in `useGestureDispatcher` via `AffordanceHit.cursor`, which
169
+ * `buildAffordanceAt` fills in from the region the walk landed on. */
156
170
  cursor?: string;
157
171
  /** Drag binding produced when this region is hit. Lazily called so
158
172
  * affordances don't pay binding-construction cost on every paint frame —
@@ -196,6 +210,35 @@ interface CustomPaintContext {
196
210
  interface AffordanceBinding<TScratch = unknown> {
197
211
  initialScratch?: TScratch;
198
212
  }
213
+ /**
214
+ * The fields `buildAffordanceAt` lifts out of a region's `initialScratch`
215
+ * when it turns a region hit into an `AffordanceHit`.
216
+ *
217
+ * Scratch is otherwise opaque — whatever the affordance wants to hand the
218
+ * action that picks up the drag. These few names are the exception: they mean
219
+ * the same thing to every affordance, and the actions that consume them
220
+ * (`resizeAction`, `rotateAction`) read them off `AffordanceHit` rather than
221
+ * out of scratch. An affordance that doesn't set them simply produces a hit
222
+ * without those fields.
223
+ */
224
+ interface CommonAffordanceScratch {
225
+ /** The node (or `MULTI_RESIZE_TARGET_ID`) this chrome acts on. Becomes
226
+ * `AffordanceHit.targetIds`. */
227
+ targetId?: string;
228
+ /** For resize chrome: which corner stays pinned. Mirrors the kit's
229
+ * `ResizeAnchor`, spelled inline so `affordances/` doesn't take a type
230
+ * dependency on the gesture layer for one field. */
231
+ anchor?: {
232
+ x: 'min' | 'max' | 'free';
233
+ y: 'min' | 'max' | 'free';
234
+ };
235
+ /** World-space invariant point of the transform — the fixed corner for a
236
+ * resize, the pivot for a rotation. */
237
+ fixedPoint?: {
238
+ x: number;
239
+ y: number;
240
+ };
241
+ }
199
242
 
200
243
  /**
201
244
  * Canvas size in CSS pixels — passed to `draw` for layers that anchor to
@@ -253,10 +296,18 @@ interface RenderLayer<TData> {
253
296
  */
254
297
  space?: 'world' | 'screen';
255
298
  /**
256
- * Optional hit-test. When defined, the dispatcher consults this on
257
- * pointerdown (top-down layer order) before falling through to the
258
- * active tool's slot walk. First non-null result wins; null means
259
- * "I don't claim this hit, try the next layer."
299
+ * Optional hit-test for **consumer-attached** layers.
300
+ *
301
+ * Only layers registered through `CanvasExtensionApi.registerLayer` are
302
+ * hit-tested: `hitTestExtras` walks them last-registered-first on
303
+ * pointerdown, and `<SceneCanvas>` folds the result into its `affordanceAt`
304
+ * thunk ahead of the kit's own selection chrome. First non-null result
305
+ * wins; null means "I don't claim this hit, try the next layer."
306
+ *
307
+ * Layers that reach the draw stack some other way — a `Tool.overlay`, an
308
+ * entry in the `layers` map — are painted but never hit-tested, so defining
309
+ * `hitTest` on one has no effect. (The kit's own chrome doesn't need it: it
310
+ * goes through `buildAffordanceAt`.)
260
311
  *
261
312
  * Coordinates are world-space. The `data` arg is the layer's
262
313
  * configured data slot (same as `draw`); `view` and `dims` mirror
@@ -2086,9 +2137,11 @@ declare function specificity(spec: GestureSpec): readonly [number, number, numbe
2086
2137
  * ## gestureId scheme
2087
2138
  * - `key-held` ongoing actions: `key-held-<key>` (e.g. `key-held- ` for Space).
2088
2139
  * Chosen because key-held gestures are identified by the held key alone.
2089
- * - `pointerdown` / drag ongoing actions: `pointer-<pointerId>`, where
2090
- * `pointerId` defaults to `'mouse'` (Phase 3 has no real pointer IDs; the
2091
- * React seam in Task 4 will supply the actual DOM pointerId).
2140
+ * - `pointerdown` / drag ongoing actions: `pointer-<pointerId>`, taken from
2141
+ * the originating DOM `PointerEvent`. Each physical pointer mouse, each
2142
+ * touch, the stylus gets its own handle slot. Events with no
2143
+ * `pointerId` (synthesized probes, programmatic drags, most tests) key to
2144
+ * `pointer-mouse`, so a single synthetic pointer behaves as it always has.
2092
2145
  * - `multitouch` ongoing actions: `multitouch-<fingers>`.
2093
2146
  * - Fallback for any other kind that triggers an ongoing invoker: `ongoing-<kind>`.
2094
2147
  *
@@ -2317,7 +2370,7 @@ interface Dispatcher {
2317
2370
  * - `kind` — the `OngoingHandle.kind` reported by the in-flight
2318
2371
  * handle (e.g. `'marquee'`, `'move'`). `null` when no action is
2319
2372
  * in flight OR the handle didn't declare a kind.
2320
- * - `id` — the dispatcher's internal `gestureId` (`pointer-mouse`,
2373
+ * - `id` — the dispatcher's internal `gestureId` (`pointer-1`,
2321
2374
  * `key-held-Space`, …) — the pointer/key channel the action rode
2322
2375
  * in on. `null` when no action is in flight.
2323
2376
  *
@@ -2325,6 +2378,13 @@ interface Dispatcher {
2325
2378
  * action overlapping a pointer action), the most-recently-started
2326
2379
  * handle wins. This matches user intent: the latest interaction is
2327
2380
  * the one consumers care about.
2381
+ *
2382
+ * That rule used to be near-vacuous on the pointer side, because every
2383
+ * pointer shared one handle slot and two pointer drags could not coexist.
2384
+ * With per-pointer keying they can — but only via paths that bypass the
2385
+ * multi-pointer policy in `useGestureDispatcher` (which stops a second
2386
+ * finger from opening a drag while a pinch is live), such as a mouse and
2387
+ * a pen used together. Latest-start remains the right answer there.
2328
2388
  */
2329
2389
  getActiveAction(): {
2330
2390
  kind: string | null;
@@ -2797,11 +2857,86 @@ interface Conflict {
2797
2857
  * the action; consumers can suppress known-intentional compositions in
2798
2858
  * their UI layer.
2799
2859
  *
2860
+ * - Two `{ kindOf }` predicate targets. The route grammar renders every
2861
+ * predicate as the single token {@link PREDICATE_TARGET}, so bucketing on
2862
+ * the rendered target alone reported select's `resize` / `rotate` / `move`
2863
+ * drags — three genuinely different predicates — as a three-way conflict.
2864
+ * Predicate entries bucket by function identity instead, which means two
2865
+ * *separately written but equivalent* predicates go unflagged. That's the
2866
+ * right way to be wrong here: this check has to be silent when nothing is
2867
+ * wrong or nobody will keep it on.
2868
+ *
2800
2869
  * Note that two bindings sharing a tuple on the SAME tool are now possible
2801
2870
  * (bindings are an array, where phase tables were objects with unique
2802
2871
  * keys) — so a conflict may name one tool twice.
2872
+ *
2873
+ * This is the raw same-tuple detector. Feeding it a whole tool *registry*
2874
+ * over-reports, because registry tools take turns in the active slot and
2875
+ * can't collide with each other — see {@link findScopedConflicts}.
2803
2876
  */
2804
2877
  declare function findConflicts(tools: readonly Tool<unknown>[]): Conflict[];
2878
+ /**
2879
+ * The tool scopes as the dispatcher sees them — which is what decides whether
2880
+ * two same-tuple bindings can actually collide.
2881
+ */
2882
+ interface ToolScopes {
2883
+ /** Tools eligible for the active slot, keyed by id or as a flat list. */
2884
+ registry: readonly Tool<unknown>[] | Readonly<Record<string, Tool<unknown>>>;
2885
+ /** Always-on tools. Every one of these is live at once. */
2886
+ ambient?: readonly Tool<unknown>[];
2887
+ }
2888
+ /**
2889
+ * Detect the same-tuple overlaps that are *reachable* — the ones where the
2890
+ * dispatcher really does fall back on declaration order.
2891
+ *
2892
+ * `matchSorted` walks scopes in strict priority (hotkey > active > ambient)
2893
+ * and only sorts by specificity *within* a scope. So a cross-scope tie isn't
2894
+ * a tie at all: an ambient tool losing a tuple to the active tool is the
2895
+ * documented design, not an accident. Likewise two registry tools sharing a
2896
+ * tuple — `rect` and `ellipse` both binding a bare `drag` — can never both be
2897
+ * in the active slot, so they never compete.
2898
+ *
2899
+ * What's left, and what this reports:
2900
+ * - a single tool colliding with **itself** (possible since bindings became
2901
+ * an array), which is ambiguous in whichever slot it occupies;
2902
+ * - two **ambient** tools, all of which are live simultaneously and are
2903
+ * ordered only by registration;
2904
+ * - two **hotkey-capable** tools, which can stack.
2905
+ *
2906
+ * Not covered: the actions registry's `defaultBinding`s, which the dispatcher
2907
+ * also folds into ambient/hotkey scope. They're assembled somewhere else
2908
+ * entirely (`ActionsRegistry`, not `useTools`), so catching tool-vs-action
2909
+ * collisions means giving this function a second input it doesn't have yet.
2910
+ */
2911
+ declare function findScopedConflicts(scopes: ToolScopes): Conflict[];
2912
+ /**
2913
+ * Render a conflict as one line of human-readable text.
2914
+ *
2915
+ * The tuple is printed through {@link formatRoute}, so the message names the
2916
+ * collision in the same grammar the author wrote the binding in — modulo the
2917
+ * phase slot, which prints as a bare `'&'`-channel atom because the bucket key
2918
+ * collapses channel-bearing phase specs (`sel:engaged` and `&:engaged` collide
2919
+ * with each other, and the collapse is what made them collide).
2920
+ */
2921
+ declare function formatConflict(conflict: Conflict): string;
2922
+ /**
2923
+ * Detect reachable route conflicts in an assembled tool set and report each one.
2924
+ *
2925
+ * This is the wiring `findConflicts` spent its first life without: the kit
2926
+ * could detect the one class of genuine routing ambiguity it has and never
2927
+ * looked. Call it once wherever a tool set is assembled (`useTools` does),
2928
+ * behind a `process.env.NODE_ENV !== 'production'` guard.
2929
+ *
2930
+ * **Warn, never throw.** A conflict between a consumer's tool and a kit tool
2931
+ * is a design question — sometimes deliberate, since the loser can still take
2932
+ * the gesture by declining through `enabled()` — and exploding in a running
2933
+ * app is the wrong way to raise it. A conflict between two *kit* tools is
2934
+ * always a bug, but the place to fail on that is the kit's own test suite
2935
+ * (`canvas/SceneCanvas.routeConflicts.test.tsx`), not a consumer's console.
2936
+ *
2937
+ * @returns The conflicts found, so callers can dedupe repeat reports.
2938
+ */
2939
+ declare function reportRouteConflicts(scopes: ToolScopes, warn?: (message: string) => void): Conflict[];
2805
2940
 
2806
2941
  declare const index_ChannelRef: typeof ChannelRef;
2807
2942
  type index_Conflict = Conflict;
@@ -2826,6 +2961,7 @@ declare const index_RouteFieldName: typeof RouteFieldName;
2826
2961
  declare const index_RouteTermLabel: typeof RouteTermLabel;
2827
2962
  type index_ToolDef<TScratch = void> = ToolDef<TScratch>;
2828
2963
  type index_ToolKeybinding = ToolKeybinding;
2964
+ type index_ToolScopes = ToolScopes;
2829
2965
  type index_ViewportToolDef<TScratch = void> = ViewportToolDef<TScratch>;
2830
2966
  declare const index_buildRouteRegistry: typeof buildRouteRegistry;
2831
2967
  declare const index_canonicalModifiers: typeof canonicalModifiers;
@@ -2835,13 +2971,16 @@ declare const index_defineViewportTool: typeof defineViewportTool;
2835
2971
  declare const index_describeRoute: typeof describeRoute;
2836
2972
  declare const index_describeRouteParts: typeof describeRouteParts;
2837
2973
  declare const index_findConflicts: typeof findConflicts;
2974
+ declare const index_findScopedConflicts: typeof findScopedConflicts;
2975
+ declare const index_formatConflict: typeof formatConflict;
2838
2976
  declare const index_formatPhaseAtom: typeof formatPhaseAtom;
2839
2977
  declare const index_formatRoute: typeof formatRoute;
2840
2978
  declare const index_getGestureDescriptor: typeof getGestureDescriptor;
2841
2979
  declare const index_isKnownGestureName: typeof isKnownGestureName;
2842
2980
  declare const index_parseRoute: typeof parseRoute;
2981
+ declare const index_reportRouteConflicts: typeof reportRouteConflicts;
2843
2982
  declare namespace index {
2844
- export { index_ChannelRef as ChannelRef, type index_Conflict as Conflict, index_DescribeRouteOptions as DescribeRouteOptions, index_GESTURE_DESCRIPTORS as GESTURE_DESCRIPTORS, index_GestureArgSpec as GestureArgSpec, index_GestureDescriptor as GestureDescriptor, index_GestureName as GestureName, index_ModRequirement as ModRequirement, index_ModifierKey as ModifierKey, index_PREDICATE_TARGET as PREDICATE_TARGET, index_ParsedModifiers as ParsedModifiers, index_ParsedRoute as ParsedRoute, index_PhaseAtom as PhaseAtom, index_RESERVED_ID_NAMES as RESERVED_ID_NAMES, index_RESERVED_ID_PREFIXES as RESERVED_ID_PREFIXES, index_ROUTE_FIELD_DEFINITIONS as ROUTE_FIELD_DEFINITIONS, index_ROUTE_TERMS as ROUTE_TERMS, type index_RegistryEntry as RegistryEntry, index_RouteDescriptionPart as RouteDescriptionPart, index_RouteFieldName as RouteFieldName, index_RouteTermLabel as RouteTermLabel, type index_ToolDef as ToolDef, type index_ToolKeybinding as ToolKeybinding, type index_ViewportToolDef as ViewportToolDef, index_buildRouteRegistry as buildRouteRegistry, index_canonicalModifiers as canonicalModifiers, index_collapseShiftPairs as collapseShiftPairs, index_defineTool as defineTool, index_defineViewportTool as defineViewportTool, index_describeRoute as describeRoute, index_describeRouteParts as describeRouteParts, index_findConflicts as findConflicts, index_formatPhaseAtom as formatPhaseAtom, index_formatRoute as formatRoute, index_getGestureDescriptor as getGestureDescriptor, index_isKnownGestureName as isKnownGestureName, index_parseRoute as parseRoute };
2983
+ export { index_ChannelRef as ChannelRef, type index_Conflict as Conflict, index_DescribeRouteOptions as DescribeRouteOptions, index_GESTURE_DESCRIPTORS as GESTURE_DESCRIPTORS, index_GestureArgSpec as GestureArgSpec, index_GestureDescriptor as GestureDescriptor, index_GestureName as GestureName, index_ModRequirement as ModRequirement, index_ModifierKey as ModifierKey, index_PREDICATE_TARGET as PREDICATE_TARGET, index_ParsedModifiers as ParsedModifiers, index_ParsedRoute as ParsedRoute, index_PhaseAtom as PhaseAtom, index_RESERVED_ID_NAMES as RESERVED_ID_NAMES, index_RESERVED_ID_PREFIXES as RESERVED_ID_PREFIXES, index_ROUTE_FIELD_DEFINITIONS as ROUTE_FIELD_DEFINITIONS, index_ROUTE_TERMS as ROUTE_TERMS, type index_RegistryEntry as RegistryEntry, index_RouteDescriptionPart as RouteDescriptionPart, index_RouteFieldName as RouteFieldName, index_RouteTermLabel as RouteTermLabel, type index_ToolDef as ToolDef, type index_ToolKeybinding as ToolKeybinding, type index_ToolScopes as ToolScopes, type index_ViewportToolDef as ViewportToolDef, index_buildRouteRegistry as buildRouteRegistry, index_canonicalModifiers as canonicalModifiers, index_collapseShiftPairs as collapseShiftPairs, index_defineTool as defineTool, index_defineViewportTool as defineViewportTool, index_describeRoute as describeRoute, index_describeRouteParts as describeRouteParts, index_findConflicts as findConflicts, index_findScopedConflicts as findScopedConflicts, index_formatConflict as formatConflict, index_formatPhaseAtom as formatPhaseAtom, index_formatRoute as formatRoute, index_getGestureDescriptor as getGestureDescriptor, index_isKnownGestureName as isKnownGestureName, index_parseRoute as parseRoute, index_reportRouteConflicts as reportRouteConflicts };
2845
2984
  }
2846
2985
 
2847
- export { type DispatcherContext as $, type Action as A, type BooleansAdapter as B, type Condition as C, type Dims as D, type ActiveToolContextProviderProps as E, type ActiveToolContextValue as F, type GeometryProjection as G, type HotkeyTrigger as H, type InsertExtras as I, type AffordanceRegion as J, type AreaSelectDep as K, type BindingOpts as L, type BindingScope as M, type BooleanOp as N, type BooleanOpResult as O, type BoundGesture as P, type BuildRuleCtxArgs as Q, type RenderLayer as R, type SliceDep as S, type Tool as T, type UseSelectionOptions as U, type VisibilityRules as V, type ClipboardIngestCtx as W, type CustomPaintContext as X, type DepName as Y, type DepRegistry as Z, DepRegistryProvider as _, type Rule as a, buildRouteRegistry as a$, type DragSample as a0, type EditAnchorsDep as a1, type GestureBinding as a2, type ImmediateInvoker as a3, type IngestCtx as a4, type IngestionDep as a5, type InsertDep as a6, type InvocationCtx as a7, type Invoker as a8, type LassoSelectDep as a9, type ViewApi as aA, applyBooleanOp as aB, buildRuleCtx as aC, createDispatcher as aD, describeRule as aE, drawLayers as aF, enterTextEditAction as aG, evaluate as aH, evaluateEnabled as aI, registerContentHandler as aJ, index as aK, sliceAction as aL, specificity as aM, useAction as aN, useActionsRegistry as aO, useActiveToolContext as aP, useDepRegistry as aQ, useDepSource as aR, useOptionalActiveToolContext as aS, useOptionalDepRegistry as aT, usePointerContext as aU, useSelection as aV, type Conflict as aW, PREDICATE_TARGET as aX, type RegistryEntry as aY, type ToolDef as aZ, type ViewportToolDef as a_, type LayoutDep as aa, type MatchResult as ab, NEVER as ac, type NodeAtPointDep as ad, type OngoingHandle as ae, type OngoingInvoker as af, type OngoingOverlay as ag, type Point2 as ah, PointerContextProvider as ai, type PointerContextValue as aj, type PointerWorldPos as ak, type ResizePolicy as al, type ResolveAllOptions as am, type ResolveOnlyResult as an, type ResolvedCandidate as ao, type ScopedBinding as ap, type SelectionExtendKey as aq, type SelectionMode as ar, type Selector as as, type SnapDep as at, type SvgUnpacker as au, type TextEditDep as av, type ToolModifiers as aw, type ToolPresentation as ax, type ToolSlot as ay, type UiOngoingControl as az, type RuleCtx as b, defineTool as b0, defineViewportTool as b1, findConflicts as b2, type ChromeCtx as c, type ChromeId as d, type DepSchema as e, type ActionsRegistry as f, type AffordanceHit as g, type Dispatcher as h, type AnyTool as i, type ToolCtx as j, type ToolKeybinding as k, type AffordanceBinding as l, type ChromeState as m, type SelectionApi as n, type ContentHandlerEntry as o, type SvgIngestOptions as p, type ActionsProp as q, type Affordance as r, ALWAYS as s, type ActionDeps as t, ActionDisabledReason as u, type ActionEnabledResult as v, type ActionEntry as w, ActionsProvider as x, ActiveToolContextProvider as y, ActiveToolContextProviderIfRoot as z };
2986
+ export { DepRegistryProvider as $, type Action as A, type BooleansAdapter as B, type Condition as C, type Dims as D, ActiveToolContextProvider as E, ActiveToolContextProviderIfRoot as F, type GeometryProjection as G, type HotkeyTrigger as H, type InsertExtras as I, type ActiveToolContextProviderProps as J, type ActiveToolContextValue as K, type AreaSelectDep as L, type BindingOpts as M, type BindingScope as N, type BooleanOp as O, type BooleanOpResult as P, type BoundGesture as Q, type RenderLayer as R, type SliceDep as S, type Tool as T, type UseSelectionOptions as U, type VisibilityRules as V, type BuildRuleCtxArgs as W, type ClipboardIngestCtx as X, type CustomPaintContext as Y, type DepName as Z, type DepRegistry as _, type Rule as a, type ToolScopes as a$, type DispatcherContext as a0, type DragSample as a1, type EditAnchorsDep as a2, type GestureBinding as a3, type ImmediateInvoker as a4, type IngestCtx as a5, type IngestionDep as a6, type InsertDep as a7, type InvocationCtx as a8, type Invoker as a9, type UiOngoingControl as aA, type ViewApi as aB, applyBooleanOp as aC, buildRuleCtx as aD, createDispatcher as aE, describeRule as aF, drawLayers as aG, enterTextEditAction as aH, evaluate as aI, evaluateEnabled as aJ, registerContentHandler as aK, index as aL, sliceAction as aM, specificity as aN, useAction as aO, useActionsRegistry as aP, useActiveToolContext as aQ, useDepRegistry as aR, useDepSource as aS, useOptionalActiveToolContext as aT, useOptionalDepRegistry as aU, usePointerContext as aV, useSelection as aW, type Conflict as aX, PREDICATE_TARGET as aY, type RegistryEntry as aZ, type ToolDef as a_, type LassoSelectDep as aa, type LayoutDep as ab, type MatchResult as ac, NEVER as ad, type NodeAtPointDep as ae, type OngoingHandle as af, type OngoingInvoker as ag, type OngoingOverlay as ah, type Point2 as ai, PointerContextProvider as aj, type PointerContextValue as ak, type PointerWorldPos as al, type ResizePolicy as am, type ResolveAllOptions as an, type ResolveOnlyResult as ao, type ResolvedCandidate as ap, type ScopedBinding as aq, type SelectionExtendKey as ar, type SelectionMode as as, type Selector as at, type SnapDep as au, type SvgUnpacker as av, type TextEditDep as aw, type ToolModifiers as ax, type ToolPresentation as ay, type ToolSlot as az, type RuleCtx as b, type ViewportToolDef as b0, buildRouteRegistry as b1, defineTool as b2, defineViewportTool as b3, findConflicts as b4, findScopedConflicts as b5, formatConflict as b6, reportRouteConflicts as b7, type ChromeCtx as c, type ChromeId as d, type DepSchema as e, type ActionsRegistry as f, type AffordanceHit as g, type Dispatcher as h, type AnyTool as i, type ToolCtx as j, type ToolKeybinding as k, type AffordanceBinding as l, type ChromeState as m, type SelectionApi as n, type ContentHandlerEntry as o, type SvgIngestOptions as p, type ActionsProp as q, type Affordance as r, type AffordanceRegion as s, type CommonAffordanceScratch as t, ALWAYS as u, type ActionDeps as v, ActionDisabledReason as w, type ActionEnabledResult as x, type ActionEntry as y, ActionsProvider as z };