@weasel-js/core 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/dist/{DrawCommand-Dl0bXNfS.d.ts → DrawCommand-CJtqqt8H.d.ts} +26 -2
  3. package/dist/{chunk-7V6JEOXE.js → chunk-7F3SDUJ4.js} +7884 -9167
  4. package/dist/chunk-7F3SDUJ4.js.map +1 -0
  5. package/dist/chunk-FSZEXVCR.js +171 -0
  6. package/dist/chunk-FSZEXVCR.js.map +1 -0
  7. package/dist/clone.d.ts +1 -1
  8. package/dist/geometry-D9BDMiQi.d.ts +114 -0
  9. package/dist/{grid-Cf87knjU.d.ts → grid-CaSK9bHV.d.ts} +1 -1
  10. package/dist/index-D31EADQG.d.ts +2847 -0
  11. package/dist/index.css +0 -35
  12. package/dist/index.css.map +1 -1
  13. package/dist/index.d.ts +913 -2546
  14. package/dist/index.js +2 -2
  15. package/dist/insert.d.ts +2 -2
  16. package/dist/move.d.ts +3 -3
  17. package/dist/{options-BPPBWMa7.d.ts → options-DMWeTELe.d.ts} +1 -1
  18. package/dist/{pointSnapToGrid-D7s7QmOF.d.ts → pointSnapToGrid-C3EruUwt.d.ts} +3 -54
  19. package/dist/renderer.css +0 -35
  20. package/dist/renderer.css.map +1 -1
  21. package/dist/renderer.d.ts +5 -4
  22. package/dist/renderer.js +2 -2
  23. package/dist/resize.d.ts +3 -3
  24. package/dist/routing.d.ts +10 -8
  25. package/dist/routing.js +1 -1
  26. package/dist/{types-BjUi2vA-.d.ts → types-Dcaa0tPq.d.ts} +1 -25
  27. package/dist/{registerFont-CP-wCsrz.d.ts → viewToMat3-D4lrBigW.d.ts} +2 -44
  28. package/package.json +6 -5
  29. package/dist/chunk-7V6JEOXE.js.map +0 -1
  30. package/dist/chunk-AM6ARSPN.js +0 -517
  31. package/dist/chunk-AM6ARSPN.js.map +0 -1
  32. package/dist/fitViewToBounds-evGsnR8Q.d.ts +0 -62
  33. package/dist/index-DZBYMsHI.d.ts +0 -1555
  34. package/dist/routing.css +0 -35
  35. package/dist/routing.css.map +0 -1
@@ -1,1555 +0,0 @@
1
- import { N as NodeId } from './types-Cpb4hii1.js';
2
- import { Op } from '@weasel-js/history';
3
- import * as React from 'react';
4
- import { ReactElement } from 'react';
5
- import { V as View } from './view-DSQgxBJB.js';
6
- import { A as ActionBehavior, M as ModifierState, c as ResizeAnchor } from './types-BjUi2vA-.js';
7
- import { B as Bounds } from './fitViewToBounds-evGsnR8Q.js';
8
- import { D as DrawCommand, F as FillStyle, a as Stroke } from './DrawCommand-Dl0bXNfS.js';
9
- import { D as DebugSink } from './types-BJ8_cyT7.js';
10
- import { GestureSpec, ModifierCombo, GestureName, ParsedModifiers, ChannelRef, DescribeRouteOptions, GESTURE_DESCRIPTORS, GestureArgSpec, GestureDescriptor, ModRequirement, ModifierKey, ParsedRoute, PhaseAtom, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, RouteDescriptionPart, RouteFieldName, RouteTermLabel, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, mods, parseRoute } from '@weasel-js/gestures';
11
- import { CapabilityTag } from '@weasel-js/modes';
12
-
13
- /**
14
- * Selection click policy. `single` always replaces; `multi` toggles when the
15
- * configured extend key is held, otherwise replaces.
16
- */
17
- type SelectionMode = 'single' | 'multi';
18
- /** Modifier key used to extend the selection in `multi` mode. */
19
- type SelectionExtendKey = 'shift' | 'meta' | 'ctrl';
20
- /** API returned by {@link useSelection}. */
21
- interface SelectionApi {
22
- /** Current selection. Re-renders trigger when this reference changes. */
23
- current: readonly NodeId[];
24
- /** Imperative read for use inside event callbacks (avoids stale closures). */
25
- get(): NodeId[];
26
- /** Replace selection. */
27
- set(ids: NodeId[]): void;
28
- /** Add id (multi-mode appends; single-mode replaces). */
29
- add(id: NodeId): void;
30
- /** Remove id from selection. */
31
- remove(id: NodeId): void;
32
- /** Toggle id in/out of selection. */
33
- toggle(id: NodeId): void;
34
- /** Clear selection. */
35
- clear(): void;
36
- /** True if id is selected. */
37
- contains(id: NodeId): boolean;
38
- /**
39
- * Apply a click to the selection per the configured mode/extend key.
40
- * - `single`: replaces selection with `[id]`, regardless of modifiers.
41
- * - `multi`: with the extend key held, toggles `id` in/out of the selection;
42
- * otherwise replaces with `[id]`.
43
- */
44
- applyClick(id: NodeId, modifiers: {
45
- shift: boolean;
46
- meta: boolean;
47
- ctrl: boolean;
48
- }): void;
49
- /** Pre-built methods for spreading into an adapter that needs them. */
50
- adapterMethods: {
51
- getSelection: () => NodeId[];
52
- setSelection: (ids: NodeId[]) => void;
53
- };
54
- }
55
- /** Options for {@link useSelection}. */
56
- interface UseSelectionOptions {
57
- /** Default `'single'`. */
58
- mode?: SelectionMode;
59
- /** Default `'shift'`. Ignored in single-mode. */
60
- extend?: SelectionExtendKey;
61
- /** Default `[]`. */
62
- initial?: readonly NodeId[];
63
- /** When `true`, every mutator (`set`/`add`/`remove`/`toggle`/`clear`/
64
- * `applyClick`) is a no-op — selection stays at whatever `initial`
65
- * pinned it to. Useful for demos that exist to showcase a single
66
- * pre-selected node (e.g. the bezier-edit curve) and don't want a
67
- * stray click to deselect. */
68
- lock?: boolean;
69
- }
70
- /**
71
- * Default implementation of the `getSelection` / `setSelection` adapter
72
- * contract every action hook (delete, duplicate, nudge, group, ...) requires.
73
- *
74
- * Owns selection state, exposes a click-policy helper (single vs multi with
75
- * an extend key), and pre-builds the two adapter methods consumers otherwise
76
- * hand-roll in every demo:
77
- *
78
- * ```tsx
79
- * const selection = useSelection({ mode: 'multi' });
80
- * const adapter = { ...arrayAdapter({...}), ...selection.adapterMethods };
81
- * ```
82
- */
83
- declare function useSelection(opts?: UseSelectionOptions): SelectionApi;
84
-
85
- /** Returned from a continuation closure or action handler. */
86
- type Result<TScratch> = {
87
- kind: 'apply';
88
- ops: Op[];
89
- label?: string;
90
- } | {
91
- kind: 'begin';
92
- spec: BeginSpec<TScratch>;
93
- } | {
94
- kind: 'hold';
95
- scratch: TScratch;
96
- } | {
97
- kind: 'commit';
98
- ops: Op[];
99
- label?: string;
100
- } | {
101
- kind: 'cancel';
102
- } | {
103
- kind: 'claim';
104
- } | {
105
- kind: 'none';
106
- };
107
- /** Spec for `begin()` — opens engaged phase with continuation handlers. */
108
- interface BeginSpec<TScratch> {
109
- scratch: TScratch;
110
- thresholdPx?: number;
111
- onMove?: (ctx: ToolCtx<TScratch>, event?: PointerEvent) => Result<TScratch>;
112
- onRelease?: (ctx: ToolCtx<TScratch>, event?: PointerEvent) => Result<TScratch>;
113
- onCancel?: (ctx: ToolCtx<TScratch>) => void | Result<TScratch>;
114
- }
115
- declare function apply<TScratch>(ops: Op[], label?: string): Result<TScratch>;
116
- declare function begin<TScratch>(spec: BeginSpec<TScratch>): Result<TScratch>;
117
- declare function hold<TScratch>(scratch: TScratch): Result<TScratch>;
118
- declare function commit<TScratch>(ops: Op[], label?: string): Result<TScratch>;
119
- declare function cancel<TScratch>(): Result<TScratch>;
120
- declare function claim<TScratch>(): Result<TScratch>;
121
- declare function none<TScratch>(): Result<TScratch>;
122
-
123
- /** A 2D point in either world or screen coordinates. */
124
- interface Point2 {
125
- x: number;
126
- y: number;
127
- }
128
- /**
129
- * Information about which UI affordance was hit at pointerdown.
130
- *
131
- * Populated by the dispatcher when the `affordanceAt` thunk is provided to
132
- * `useGestureDispatcher`. Tools / action invokers that only fire on a specific
133
- * affordance (e.g. a resize handle) use this field as a guard — if the
134
- * affordance is absent or is the wrong kind, they return `{}` and let other
135
- * bindings handle the drag.
136
- *
137
- * `kind` is a discriminator string:
138
- * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
139
- * `'handle:bottom-right'` — corner resize handles.
140
- * - `'rotate-handle'` — the rotation affordance.
141
- * - `'anchor:N'` — a path anchor at index N.
142
- *
143
- * `fixedPoint` is the world-space point that should remain stationary during
144
- * the gesture. For resize handles this is the opposite (diagonally fixed)
145
- * corner; for rotate it is the pivot.
146
- *
147
- * `targetIds` are the node ids this affordance belongs to.
148
- */
149
- interface AffordanceHit$1 {
150
- /** Discriminator string, e.g. `'handle:bottom-right'`. */
151
- kind: string;
152
- /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
153
- fixedPoint?: {
154
- x: number;
155
- y: number;
156
- };
157
- /** Which nodes this affordance belongs to. */
158
- targetIds?: string[];
159
- /** Set when `kind` matches `'handle:*'`. Identifies which corner stays
160
- * fixed during a resize so consumers (resizeAction) don't re-parse `kind`.
161
- * Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
162
- * leave this undefined. */
163
- anchor?: ResizeAnchor;
164
- /** CSS cursor to show while the pointer hovers this affordance (no
165
- * gesture in flight). Consumed by the hover-cursor pump in
166
- * `useGestureDispatcher`; unset = the pump falls through to
167
- * action-cursor prediction, then to the active tool's cursor. */
168
- cursor?: string;
169
- }
170
- /** Per-invocation runtime context the dispatcher hands to an Invoker.
171
- * Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
172
- * populated only for matching gesture kinds. */
173
- interface InvocationCtx {
174
- world: Point2;
175
- screen: Point2;
176
- modifiers: ModifierState;
177
- deps: ActionDeps;
178
- drag?: {
179
- start: Point2;
180
- current: Point2;
181
- delta: Point2;
182
- /**
183
- * Drag delta in client/screen coordinates (CSS pixels from the drag
184
- * origin). Use this — never `delta` — for any action whose effect
185
- * mutates the viewport itself (pan, view-zoom), because world-space
186
- * deltas become self-referential as the view shifts mid-drag.
187
- *
188
- * Populated when the dispatcher received `clientX`/`clientY` on the
189
- * underlying pointer events. Absent for legacy callers that don't
190
- * provide them.
191
- */
192
- screenDelta?: Point2;
193
- affordance?: AffordanceHit$1;
194
- /**
195
- * Full pointermove history for the current drag, in world space.
196
- * Accumulated by the dispatcher on every `pointermove` pump event.
197
- * Available only during `onMove` and `onEnd` calls (not on `start`).
198
- * Used by `lassoSelectAction` to build its polygon vertex list.
199
- */
200
- points?: Point2[];
201
- };
202
- wheel?: {
203
- deltaX: number;
204
- deltaY: number;
205
- deltaZ: number;
206
- };
207
- multiTouch?: {
208
- centroid: Point2;
209
- spread: number;
210
- rotation: number;
211
- /**
212
- * Pinch-zoom geometry. Populated by the dispatcher when a multitouch
213
- * handle is in flight and a pointermove-pump fires.
214
- * `startSpread` is the spread at the moment the gesture began.
215
- * `currentSpread` is the spread at the current frame.
216
- */
217
- pinch?: {
218
- startSpread: number;
219
- currentSpread: number;
220
- centroid: Point2;
221
- };
222
- };
223
- key?: {
224
- key: string;
225
- repeat: boolean;
226
- };
227
- /**
228
- * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
229
- * UI-driven ongoing actions (color picker, opacity slider) so handles can
230
- * read the current value on `start` and updated values on `onMove`. The
231
- * gesture dispatcher does not populate this field; gesture-driven actions
232
- * receive params via `BindingOpts.params` on `start` (the `opts` arg).
233
- */
234
- params?: Record<string, unknown>;
235
- }
236
- /** Per-invocation options the dispatcher reads from a `GestureBinding`'s
237
- * `opts` field and passes to `OngoingInvoker.start`. Today carries
238
- * behaviors; extensible. */
239
- interface BindingOpts {
240
- behaviors?: ActionBehavior<unknown, unknown, unknown>[];
241
- /** Per-binding action parameters. The action's invoker reads
242
- * these via the second arg to `run` (or via InvocationCtx for ongoing
243
- * invokers, when needed). Loose typing (Record<string, unknown>) for
244
- * now; consider per-action typing later via BindingOpts<A>.
245
- *
246
- * params may also be a thunk evaluated each time the
247
- * dispatcher (or invoker) needs the value. Thunks let tools close over
248
- * refs that mutate during a gesture (e.g. polygon `sides` adjusted
249
- * mid-drag via ArrowUp). For ongoing invokers that want the latest
250
- * values at commit, the invoker can re-call the thunk inside `onEnd`
251
- * via `resolveParams(opts?.params)`. */
252
- params?: Record<string, unknown> | (() => Record<string, unknown>);
253
- }
254
- /** Convention-shaped action dependencies bag. Actions declare which
255
- * contexts they consume; the dispatcher composes them per call.
256
- * Consumer-side contexts (e.g. ColorContext) plug in by extending. */
257
- interface ActionDeps {
258
- selection?: unknown;
259
- view?: unknown;
260
- scene?: unknown;
261
- pointer?: unknown;
262
- activeTool?: unknown;
263
- [k: string]: unknown;
264
- }
265
- /**
266
- * Discriminated overlay shape returned by `OngoingHandle.overlay()`.
267
- * Dispatcher-side chrome surface for in-flight
268
- * gestures that paint non-ghost visuals. The canvas's
269
- * `useDispatcherOverlayLayer` walks every in-flight handle, calls
270
- * `overlay()`, and dispatches on `kind` to draw the appropriate shape.
271
- *
272
- * `marquee` mirrors `AreaSelectOverlay`; `lasso` mirrors `LassoSelectOverlay`.
273
- * `commands` is the generic escape hatch — actions emit arbitrary
274
- * `DrawCommand[]` for previews the typed variants can't express (insert
275
- * shape outlines, paste ghosts of synthetic nodes, custom chrome). World-
276
- * space is the default; the layer wraps in `viewToMat3` so commands track
277
- * the camera. Set `space: 'screen'` for projections you've already done
278
- * yourself (rare).
279
- */
280
- type OngoingOverlay = {
281
- kind: 'marquee';
282
- start: {
283
- x: number;
284
- y: number;
285
- };
286
- current: {
287
- x: number;
288
- y: number;
289
- };
290
- shiftHeld: boolean;
291
- } | {
292
- kind: 'lasso';
293
- vertices: ReadonlyArray<{
294
- x: number;
295
- y: number;
296
- }>;
297
- current: {
298
- x: number;
299
- y: number;
300
- };
301
- shiftHeld: boolean;
302
- } | {
303
- kind: 'commands';
304
- commands: readonly DrawCommand[];
305
- /** Coordinate space the commands are authored in. Default `'world'`
306
- * — the layer wraps them in `viewToMat3(view)` so they track the
307
- * camera. `'screen'` emits them as-is (CSS pixels). */
308
- space?: 'world' | 'screen';
309
- } | {
310
- /**
311
- * Live insert-drag preview — dispatched by `insertAction` while the
312
- * user is dragging out a new shape. Pre-commit there is no scene node
313
- * to ghost via `previewIds()`/`previewPose()`, so insert paints its
314
- * preview through the dispatcher overlay layer instead.
315
- *
316
- * `shape` is the kit's built-in insert kind. `bounds` is the AABB of
317
- * the current drag (start/current normalized). `extras` is the
318
- * per-kind extras the action already collected — the overlay
319
- * renderer rebuilds the shape using the same path builders the
320
- * commit factory uses, so the preview matches the eventual node.
321
- *
322
- * `extras` is opaque (`unknown`) at the union level; the overlay
323
- * renderer narrows on `shape` and casts the field shape it expects.
324
- */
325
- kind: 'insertPreview';
326
- shape: 'rect' | 'ellipse' | 'line' | 'polygon' | 'star' | 'pencil';
327
- bounds: {
328
- x: number;
329
- y: number;
330
- width: number;
331
- height: number;
332
- };
333
- extras: unknown;
334
- /** World-space point to paint a small "anchor" dot at. Sells the
335
- * click point as the drag's anchor — particularly useful for
336
- * radial shapes (polygon/star) where no vertex sits on the
337
- * click point, and for any shape in center mode where the dot
338
- * marks the center the shape grows around. */
339
- anchorPoint?: {
340
- x: number;
341
- y: number;
342
- };
343
- };
344
- /** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
345
- * `onMove` on subsequent input events of the same gesture and calls
346
- * `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
347
- * on pointercancel / blur / escape). */
348
- interface OngoingHandle {
349
- /**
350
- * Optional logical action kind — a stable, human-readable tag the
351
- * dispatcher exposes via `getActiveAction()` for chrome-visibility
352
- * rules and any other surface that wants to react to "what action
353
- * is currently in flight" without inspecting handles directly.
354
- *
355
- * Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
356
- * `'pan'`, `'pinch'`.
357
- *
358
- * Distinct from the dispatcher's internal `gestureId` (`pointer-mouse`,
359
- * `key-held-Space`, etc.) which keys per-pointer state and is not
360
- * meaningful to consumers.
361
- *
362
- * When omitted, the action is "anonymous" — `getActiveAction().kind`
363
- * reports `null` even though a handle is in flight. This is fine for
364
- * actions that don't have visible chrome of their own.
365
- */
366
- kind?: string;
367
- onMove?(ctx: InvocationCtx): void;
368
- onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
369
- /**
370
- * Optional preview surface — dispatcher-side ghost overlay.
371
- *
372
- * An ongoing-action implementation may populate `previewIds()` +
373
- * `previewPose(id)` to expose its in-flight preview state for the
374
- * canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
375
- * top of the committed scene during the gesture.
376
- *
377
- * Returning `null` (or omitting the method entirely) means "no preview
378
- * this gesture" — the canvas will skip this handle as a source.
379
- *
380
- * Semantics mirror the tool-side `Tool.previewIds` / `Tool.previewPose`
381
- * pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
382
- * returns the interim pose for one of those ids (shape opaque — the
383
- * canvas casts to its `TPose` parameter). The preview-ghost layer
384
- * merges all sources via first-non-null semantics, with tool-side
385
- * previews taking precedence over dispatcher-side (preserves
386
- * backwards-compat during the registry-unification migration).
387
- */
388
- previewIds?(): Iterable<string> | null;
389
- previewPose?(id: string): unknown | null;
390
- /**
391
- * When `false`, the preview-ghost layer paints the ghost AND the
392
- * source node stays visible at its committed pose. Defaults to
393
- * `true` (move/resize/rotate semantics: ghost replaces the source
394
- * during the gesture). Clone overrides to `false` so the original
395
- * stays put and the ghost appears at the drag target.
396
- */
397
- previewHidesSource?: boolean;
398
- /**
399
- * Optional per-id preview *data*. Falls back to the committed
400
- * `node.data` when null/absent. Use when the gesture mutates
401
- * `node.data` (e.g. anchor-edit on nodes that store the polygon on
402
- * `data.path`) rather than (or in addition to) the pose. The preview-
403
- * ghost layer assembles a synthetic node from `{ ...node, pose:
404
- * previewPose ?? node.pose, data: previewData ?? node.data }` before
405
- * calling the scene slot's `drawOne`.
406
- *
407
- * Sources compose first-non-null per axis: an action can emit only
408
- * `previewPose` (translation), only `previewData` (data-only edit),
409
- * or both (pose + data both change, e.g. anchor drag on a data.path
410
- * node where the bounds shift).
411
- */
412
- previewData?(id: string): unknown | null;
413
- /**
414
- * Optional chrome surface — dispatcher-side overlay layer.
415
- *
416
- * An ongoing-action implementation may populate `overlay()` to expose a
417
- * non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
418
- * `useDispatcherOverlayLayer` to paint while the gesture is in flight.
419
- * Returning `null` (or omitting the method) means "no overlay this
420
- * gesture" — the canvas will skip this handle as a chrome source.
421
- *
422
- * Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
423
- * which paints displaced scene-node silhouettes. Marquee and lasso
424
- * gestures don't displace any node, but still need on-screen feedback.
425
- */
426
- overlay?(): OngoingOverlay | null;
427
- }
428
- /** Fire-once invocation. Runs to completion synchronously (or fires off an
429
- * async side-effect; the registry doesn't wait). */
430
- interface ImmediateInvoker {
431
- timing: 'immediate';
432
- /** `params` carries the matched binding's opts.params. When
433
- * invoked via the legacy `Action.run` bridge or from the command palette
434
- * with no per-binding context, `params` is undefined; descriptors should
435
- * default to a sensible variant. */
436
- run(deps: ActionDeps, params?: Record<string, unknown>): void;
437
- }
438
- /** Phase-machine invocation. `start` opens the phase and returns the handle
439
- * the dispatcher pumps. */
440
- interface OngoingInvoker {
441
- timing: 'ongoing';
442
- start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
443
- }
444
- /** Pluggable invocation strategy for an Action. Future variants
445
- * (`longPress`, `twoStage`, `modal`) extend this union without touching
446
- * the `Action` type. */
447
- type Invoker = ImmediateInvoker | OngoingInvoker;
448
-
449
- /**
450
- * GestureBinding — connects a GestureSpec to an Action id (with per-binding
451
- * options). Tools own arrays of these on their `bindings` field; ambient
452
- * gesture-bindings are registered globally.
453
- *
454
- * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
455
- */
456
-
457
- interface GestureBinding {
458
- spec: GestureSpec;
459
- actionId: string;
460
- opts?: BindingOpts;
461
- }
462
-
463
- /**
464
- * Configurable activation-key descriptor for tools that expose their
465
- * keybinding to the host (currently Lasso and Eyedropper). Captures
466
- * only the fields meaningful to a caller-supplied tool-select key —
467
- * dispatcher-internal fields (`skipInEditable`, `enabled`,
468
- * `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
469
- * not part of the configurable surface.
470
- */
471
- interface ToolKeybinding {
472
- /** Key or list of keys to match (case-insensitive against `event.key`). */
473
- key: string | readonly string[];
474
- /** Require Cmd (mac) / Ctrl (others). Default `false`. */
475
- mod?: boolean;
476
- /** Require Alt. Default `false`. */
477
- alt?: boolean;
478
- /**
479
- * Shift policy. `undefined`/`false` forbids shift, `true` requires
480
- * shift, `'optional'` allows either.
481
- */
482
- shift?: boolean | 'optional';
483
- }
484
- type ActionFn<TScratch> = (ctx: ToolCtx<TScratch>, event?: PointerEvent | KeyboardEvent | WheelEvent) => Result<TScratch>;
485
- type ModifierRoute<TScratch> = Partial<Record<ModifierCombo, ActionFn<TScratch>>>;
486
- type RouteEntry<TScratch> = ActionFn<TScratch> | ModifierRoute<TScratch>;
487
- type RouteTable<TScratch> = Partial<Record<string, RouteEntry<TScratch>>>;
488
- /** Map from key-route string (e.g. `"ArrowDown"`, `"ArrowDown?shift"`) to
489
- * action. Parsed by the reflection emitter and dispatcher. */
490
- type KeyRouteTable<TScratch> = Partial<Record<string, ActionFn<TScratch>>>;
491
- /** Wheel direction sub-table. Keys: `'up' | 'down' | '*'`. Function form
492
- * is sugar for `{ '*': fn }` (matches both directions). */
493
- type WheelTable<TScratch> = Partial<Record<'up' | 'down' | '*', ActionFn<TScratch>>>;
494
- /** MultiTouch tap fingers sub-table. Keys: `'2' | '3' | '4'`. */
495
- type MultiTouchTapTable<TScratch> = Partial<Record<'2' | '3' | '4', ActionFn<TScratch>>>;
496
- interface PhaseDef<TScratch> {
497
- click?: RouteTable<TScratch>;
498
- /** Pre-threshold classifier route. Runs synchronously on pointerdown,
499
- * before the dispatcher distinguishes click vs. drag. Use this for
500
- * classification gestures that need to mutate scratch *before* the
501
- * drag pipeline starts — e.g. select tool determining whether a hit
502
- * belongs to the existing selection ("drag will move all") or not
503
- * ("drag will move just this one").
504
- *
505
- * Semantics:
506
- * - Return `begin(spec)` to open engaged phase with scratch. The
507
- * spec's `onMove`/`onRelease` will fire if the dispatcher escalates
508
- * to drag; otherwise the next click handler runs normally with the
509
- * prepared scratch visible.
510
- * - Return `apply(ops)` or `commit(ops)` to finish the gesture
511
- * immediately (rare).
512
- * - Return `none()` or omit to pass through to threshold-gated
513
- * click/drag classification.
514
- *
515
- * Predates the imperative
516
- * `pointer.onDown` channel that useSelectTool used earlier. */
517
- pointerDown?: RouteTable<TScratch>;
518
- dblTap?: RouteTable<TScratch>;
519
- /** Right-click route table. Mirrors `click` semantics — keyed by hit-test
520
- * target, modifier-aware. The dispatcher calls `preventDefault()` on the
521
- * underlying `contextmenu` DOM event so tools fully own the menu. */
522
- contextMenu?: RouteTable<TScratch>;
523
- drag?: RouteTable<TScratch> | ActionFn<TScratch>;
524
- wheel?: WheelTable<TScratch> | ActionFn<TScratch>;
525
- keyDown?: KeyRouteTable<TScratch>;
526
- keyUp?: KeyRouteTable<TScratch>;
527
- /** Multi-finger tap. Synthesized by the dispatcher when a multitouch
528
- * gesture releases without movement past the tap threshold. Keys: the
529
- * fingers count as a string (`'2'`, `'3'`, `'4'`). */
530
- multiTouchTap?: MultiTouchTapTable<TScratch>;
531
- cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
532
- /** Optional overlay layer rendered while the tool is in any active slot
533
- * (active, hotkey, or ambient). The factory evaluates the thunk once
534
- * at translation time and emits the resulting RenderLayer on
535
- * Tool.overlay. The layer's `draw` closure should read dynamic state
536
- * (scratch, controller overlay snapshots) via refs/closures captured
537
- * in the enclosing render scope — same pattern hand-rolled
538
- * tools use today.
539
- *
540
- * Function form rather than a direct RenderLayer so consumers can
541
- * defer construction until inside a `useMemo` factory body, where
542
- * `useRef`-backed values are stable. Symmetric with `cursor`'s
543
- * function form.
544
- *
545
- * Note: only `initial.overlay` is read. If `engaged.overlay`
546
- * is set, it is ignored — phase-specific overlay routing is a future
547
- * enhancement. Tools that need engagement-aware previews should gate
548
- * inside the single overlay's `draw` body via `if (!scratch.somefield)
549
- * return []`, which is how every hand-rolled tool already
550
- * does it. */
551
- overlay?: () => RenderLayer<unknown>;
552
- /** Modal-claim predicate. When this resolves to `true`, the dispatcher
553
- * routes every pointerdown to this tool and bypasses the affordance-layer
554
- * hit-test pipeline — used by tools in modal states (pen mid-path, text
555
- * mid-edit) where affordance hits would otherwise interrupt the
556
- * in-progress gesture.
557
- *
558
- * Function form receives the live ToolCtx (scratch, view, modifiers,
559
- * target). Boolean form is sugar for `() => true` / `() => false` —
560
- * use the function form when the decision depends on scratch state
561
- * (e.g. `(ctx) => ctx.scratch?.midPath === true`).
562
- *
563
- * Resolved per-call by the factory — the function fires on every
564
- * pointerdown the dispatcher considers handing to this tool. Keep it
565
- * cheap (no allocations, just a scratch read). */
566
- claimsAll?: boolean | ((ctx: ToolCtx<TScratch>) => boolean);
567
- }
568
- interface ToolDef<TScratch = void> {
569
- id: string;
570
- /** Capability tags for modality eligibility. Forwarded onto `Tool.capabilities`. */
571
- capabilities?: CapabilityTag[];
572
- /** Hook name as exported from the kit barrel (e.g. `'useHandTool'`).
573
- * Set by built-in hooks for inspector / debugging. Consumer-authored
574
- * tools may set this to surface their hook name; omitted is fine.
575
- * Introspection-only — do not make this load-bearing in production.
576
- * Read off the def via `Tool.def` (the reflection escape hatch). */
577
- hookName?: string;
578
- presentation?: ToolPresentation<TScratch>;
579
- /** Optional caller-supplied activation key. Most built-in tools have their
580
- * activation key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`;
581
- * this field is for tools that want their activation key to be
582
- * configurable by the host (currently Lasso and Eyedropper). The dynamic
583
- * loop in `useKeybindings.ts` picks this up and appends a binding entry
584
- * to the consolidated `tool.activate` action (with `opts.params.toolId`
585
- * set so the invoker knows which tool to switch to). */
586
- keybinding?: ToolKeybinding;
587
- /** Declarative held-key trigger (reflection / inspector only). When set,
588
- * signals to the host that this tool can engage via a held key; the host
589
- * must register the activation via the consolidated `tool.offhand` action
590
- * (`makeToolOffhandAction` + `buildToolOffhandBindings`). Built-in tools
591
- * declare held keys in `BUILTIN_OFFHAND_ACTIONS`; configurable-hotkey
592
- * tools rely on the host to wire the binding. Setting this field does NOT
593
- * automatically engage the held-key behavior. */
594
- hotkey?: HotkeyTrigger;
595
- onActivate?: (ctx: ToolCtx<TScratch>) => void;
596
- onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
597
- cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
598
- /** Override the default scratch initializer. Default is `() => null`
599
- * cast to `TScratch`, which works for tools whose scratch is fresh
600
- * every gesture. Tools that need scratch identity to survive across
601
- * gesture boundaries (e.g. the pen tool's multi-click subpath state)
602
- * pass a stable-ref-returning thunk here. The factory forwards this
603
- * onto the returned `Tool.initScratch`. */
604
- initScratch?: () => TScratch;
605
- /** Optional hit-test override. Mirrors `Tool.hitOverride` — see that
606
- * field for full semantics. The factory forwards this directly onto the
607
- * returned Tool. */
608
- hitOverride?: (ctx: {
609
- worldX: number;
610
- worldY: number;
611
- scratch: TScratch;
612
- view: View;
613
- modifiers: ToolModifiers;
614
- }) => {
615
- target: string;
616
- extra?: unknown;
617
- } | null;
618
- /** Declarative gesture-bindings forwarded onto `Tool.bindings`.
619
- * The new dispatcher consults these while this tool is active. */
620
- bindings?: GestureBinding[];
621
- initial: PhaseDef<TScratch>;
622
- engaged?: PhaseDef<TScratch>;
623
- }
624
- /** Viewport-tool spec — strict subset of ToolDef. Drops click/dblTap,
625
- * narrows drag to plain ActionFn. Mechanically derived via Pick/Omit
626
- * so the subset relationship is compiler-enforced. */
627
- type ViewportPhaseDef<TScratch = void> = Pick<PhaseDef<TScratch>, 'wheel' | 'keyDown' | 'keyUp' | 'cursor' | 'overlay' | 'claimsAll'> & {
628
- drag?: ActionFn<TScratch>;
629
- };
630
- type ViewportToolDef<TScratch = void> = Omit<ToolDef<TScratch>, 'initial' | 'engaged'> & {
631
- initial: ViewportPhaseDef<TScratch>;
632
- engaged?: ViewportPhaseDef<TScratch>;
633
- };
634
-
635
- /** Common payload for any hit that references a scene node. */
636
- interface NodeRef {
637
- id: NodeId;
638
- pose: unknown;
639
- data: unknown;
640
- meta?: Record<string, unknown>;
641
- }
642
- /** No hit — pointer landed on the background. */
643
- interface EmptyHit {
644
- category: 'empty';
645
- kind: 'empty';
646
- }
647
- /** Hit on a scene node's body. */
648
- interface NodeHit extends NodeRef {
649
- category: 'node';
650
- kind: string;
651
- }
652
- /** Hit on a node's affordance chrome (handle, anchor, etc.). */
653
- interface AffordanceHit extends NodeRef {
654
- category: 'affordance';
655
- kind: string;
656
- }
657
- /** Hit on a tool-defined target (anchor, handle, segment, etc.) supplied
658
- * via the tool's `hitOverride`. The `kind` string is the tool's own
659
- * vocabulary; the dispatcher does not interpret it. */
660
- interface ToolHit {
661
- category: 'tool';
662
- kind: string;
663
- extra?: unknown;
664
- }
665
- /** Full discriminated union — every routed action's `ctx.target`. */
666
- type HitResult = EmptyHit | NodeHit | AffordanceHit | ToolHit;
667
- /** Convenience: any hit that references a node (i.e., not EmptyHit). */
668
- type NodeRefHit = NodeHit | AffordanceHit;
669
-
670
- /** Phase the route was resolved against. Mirrors the spec's two-phase
671
- * vocabulary — `initial` (idle, scratch null) or `engaged` (mid-gesture). */
672
- type RoutePhase = 'initial' | 'engaged';
673
- /** Gesture channel the route fired on. */
674
- type RouteGesture = GestureName;
675
- /** Snapshot of one route resolution, emitted by the factory on each
676
- * successful lookup. Captured by the dispatcher as the "last resolved
677
- * route" for debug-overlay consumers. */
678
- interface RouteResolvedInfo {
679
- toolId: string;
680
- phase: RoutePhase;
681
- gesture: RouteGesture;
682
- /** Argument captured at match time for arg-bearing gestures
683
- * (`wheel` direction, `keyDown`/`keyUp` key, `multiTouchTap` fingers).
684
- * Undefined for no-arg gestures. */
685
- arg: string | undefined;
686
- /** Route-table key that matched (post-precedence). E.g. 'rect:selected',
687
- * '*:selected', 'rect', '*', 'empty'. For function-form `drag` (no
688
- * table), this is '*'. For keyDown/keyUp, it's the key name ('Escape',
689
- * 'Enter', etc.). */
690
- matchedKey: string;
691
- modifiers: ModifierCombo;
692
- /** The full HitResult at resolution time (snapshot — safe to read). */
693
- target: HitResult;
694
- /** Monotonic timestamp (ms since page load via performance.now()).
695
- * Used for "resolved Nms ago" displays. */
696
- timestamp: number;
697
- }
698
- /** Render-friendly one-line string. Used by ToolDebugOverlay; exported
699
- * so non-React consumers can format the same way. */
700
- declare function formatRouteResolved(info: RouteResolvedInfo): string;
701
-
702
- /** Outcome of a channel handler. `'claim'` stops dispatch for this event;
703
- * `'pass'` lets the next slot try. Handlers that return nothing are
704
- * treated as `'pass'`. */
705
- type Decision = 'claim' | 'pass' | void;
706
- /** Modifier-key snapshot at event dispatch time. `space` is included
707
- * because tools commonly use space as a hotkey-slot trigger and may
708
- * also want to read it as a flag mid-gesture. */
709
- interface ToolModifiers {
710
- alt: boolean;
711
- shift: boolean;
712
- meta: boolean;
713
- ctrl: boolean;
714
- space: boolean;
715
- }
716
- /** Per-event context passed to every channel handler. `scratch` is typed
717
- * via the tool's `TScratch` parameter; it survives across a single
718
- * gesture (pointer-down through end/cancel) and is replaced on next
719
- * gesture start by `initScratch()`. */
720
- interface ToolCtx<TScratch = unknown> {
721
- worldX: number;
722
- worldY: number;
723
- modifiers: ToolModifiers;
724
- /** Hit-test result for the current event. Populated by the dispatcher
725
- * before each handler call. Tools that don't use declarative routing
726
- * can ignore this. Optional for migration; will become
727
- * required once the routing migration is complete. */
728
- target?: HitResult;
729
- selection: SelectionApi;
730
- /** Adapter/scene access — opaque at this layer; tools that need it
731
- * cast to a known shape. This layer doesn't constrain it. */
732
- adapter: unknown;
733
- applyOps: (ops: Op[], label: string) => void;
734
- /** Current viewport. Reflects camera-position semantics — see
735
- * `View` JSDoc. */
736
- view: View;
737
- /** Mutate the viewport. In controlled mode this calls the consumer's
738
- * `onViewChange`; in uncontrolled mode it updates Canvas's internal
739
- * state. View changes are not undoable. */
740
- setView: (next: View) => void;
741
- /** Bounding rect of the canvas element in viewport coords. Used by
742
- * zoom/pan tools to convert event clientX/clientY to canvas-relative
743
- * anchors. */
744
- canvasRect: DOMRect;
745
- /** Screen-space pointer coords relative to `canvasRect`. Useful for
746
- * viewport tools that pan/zoom in screen space (e.g. hand-pan
747
- * computes deltas in pixels, not world units). Optional — populated
748
- * by the dispatcher on pointer events; absent on keyboard events. */
749
- screenPoint?: {
750
- x: number;
751
- y: number;
752
- };
753
- /** Optional debug sink. When `<Canvas debug={...}>` is enabled, Canvas
754
- * threads its sink here so tool-internal hit math (handle hitboxes,
755
- * rotation handle, etc.) lands in the same overlay as Canvas's own
756
- * bounds/origin records. Tools should call this conditionally with `?.`. */
757
- debug?: DebugSink;
758
- /** Kit-internal: route-resolution reporter. The dispatcher populates
759
- * this; the declarative routing factory calls it after each successful
760
- * resolveRoute() hit so the dispatcher can publish the last-resolved
761
- * snapshot to debug-overlay consumers. Underscore prefix signals
762
- * "do not consume in tool code." */
763
- __reportRoute?: (info: RouteResolvedInfo) => void;
764
- scratch: TScratch;
765
- }
766
- interface PointerChannel<TScratch> {
767
- onDown?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
768
- onClick?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
769
- }
770
- interface DragChannel<TScratch> {
771
- onStart?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
772
- onMove?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
773
- onEnd?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
774
- onCancel?: (ctx: ToolCtx<TScratch>) => void;
775
- }
776
- interface KeyboardChannel<TScratch> {
777
- onDown?: (e: KeyboardEvent, ctx: ToolCtx<TScratch>) => Decision;
778
- onUp?: (e: KeyboardEvent, ctx: ToolCtx<TScratch>) => Decision;
779
- }
780
- interface WheelChannel<TScratch> {
781
- onWheel?: (e: WheelEvent, ctx: ToolCtx<TScratch>) => Decision;
782
- }
783
- /** Double-tap (double-click) channel. Fires on the pointerup of the second
784
- * sub-threshold tap when it follows a previous sub-threshold tap within the
785
- * dispatcher's `dblTap.windowMs` and `dblTap.maxDistance` (CSS px). The
786
- * scratch handed to the handler is fresh — `dblTap` is not part of a drag
787
- * pipeline, so `initScratch()` runs immediately before the call. A `'claim'`
788
- * return suppresses the regular `pointer.onClick` for this gesture. */
789
- interface DblTapChannel<TScratch> {
790
- onTap?: (e: PointerEvent, ctx: ToolCtx<TScratch>) => Decision;
791
- }
792
- /** Hotkey-slot trigger key. The slot is engaged while this key is held —
793
- * hence "hotkey": active as long as the key is hot. `null` (or omitted)
794
- * means the tool is not eligible for the hotkey slot. */
795
- type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift';
796
- /** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
797
- * `Bounds` type — the optional `rotation` field carries through so a tool
798
- * can report an oriented preview rect (e.g. mid-rotate). */
799
- type ToolBounds = Bounds;
800
- /** Presentation metadata for tool palettes / menus. Optional on every
801
- * tool — consumers that render a palette (`<ToolPalette>`) read these
802
- * fields to display the tool; consumers that don't can ignore them.
803
- *
804
- * Note: cursor is NOT here. The top-level `Tool.cursor` field below is
805
- * already plumbed through `<Canvas>` to `style.cursor` on the host. */
806
- interface ToolPresentation<TScratch = unknown> {
807
- /** Human-readable label, distinct from the `id`. Falls back to `id`. */
808
- label?: string;
809
- /** Inline-SVG icon component output. May be a static `ReactNode` or a
810
- * function of scratch state (rare; useful for shape-aware affordances). */
811
- icon?: React.ReactNode | ((scratch?: TScratch) => React.ReactNode);
812
- /** Palette grouping key. Tools sharing a group render contiguously
813
- * with separators between groups. Free-form string; the kit
814
- * recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
815
- group?: string;
816
- /** Display override for the keyboard shortcut. When omitted the palette
817
- * derives one from `Tool.keybinding` via its own formatter. */
818
- shortcut?: string;
819
- }
820
- /** Full Tool record. */
821
- interface Tool<TScratch = unknown> {
822
- id: string;
823
- /**
824
- * App-level capability tags for modality. The `weasel-modes` package's
825
- * `eligibleForMode(mode, capabilities)` predicate consumes these to decide
826
- * whether the tool is usable in the active mode. Tags are extensible
827
- * strings — apps can define their own. Untagged tools are treated as
828
- * ineligible by all modes except those whose `allows` list includes
829
- * every implicit-or-declared tag (i.e. `normal` in the default preset).
830
- */
831
- capabilities?: CapabilityTag[];
832
- /** Optional caller-supplied key. Most built-in tools have their activation
833
- * key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
834
- * is for tools that want their activation key to be configurable by the
835
- * host (currently Lasso and Eyedropper). The dynamic loop in
836
- * `useKeybindings.ts` picks this up and appends a binding entry to the
837
- * consolidated `tool.activate` action (with `opts.params.toolId` set so
838
- * the invoker knows which tool to switch to). */
839
- keybinding?: ToolKeybinding;
840
- initScratch?: () => TScratch;
841
- onActivate?: (ctx: ToolCtx<TScratch>) => void;
842
- onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
843
- pointer?: PointerChannel<TScratch>;
844
- drag?: DragChannel<TScratch>;
845
- keyboard?: KeyboardChannel<TScratch>;
846
- wheel?: WheelChannel<TScratch>;
847
- /** Double-tap channel — fires when two sub-threshold taps land within
848
- * `dblTap.windowMs` / `dblTap.maxDistance` of each other. Lets tools
849
- * enter modal modes (e.g. select → edit-anchors) without consumers
850
- * attaching `onDoubleClick` to a wrapper DOM node. */
851
- dblTap?: DblTapChannel<TScratch>;
852
- /**
853
- * State-aware predicate. When true, this tool claims every pointerdown
854
- * and bypasses the affordance layer hit-test pipeline. Used by tools
855
- * in modal states (pen mid-path, text mid-edit) where affordance hits
856
- * would otherwise interrupt the in-progress gesture.
857
- *
858
- * Default: undefined (treated as false). Called once per pointerdown
859
- * with the tool's current ctx (scratch + view + modifiers).
860
- */
861
- claimsAll?: (ctx: ToolCtx<TScratch>) => boolean;
862
- cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
863
- /** Presentation metadata for tool palettes. See `ToolPresentation`. */
864
- presentation?: ToolPresentation<TScratch>;
865
- /** Returns the in-flight preview pose for `id` if this tool is mid-gesture
866
- * on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
867
- * reflect live gesture state without reaching into hook internals. The
868
- * return type is `unknown` here because the Tool interface is pose-agnostic;
869
- * callers that know the pose shape (e.g. Canvas typed by `TPose`) cast at
870
- * the use site. */
871
- previewPose?: (id: string) => unknown;
872
- /** Returns the in-flight preview bounds for `id` if this tool is mid-gesture
873
- * on it; otherwise `null`. Optional companion to `previewPose` for tools that
874
- * can compute bounds without round-tripping through a geometry adapter. */
875
- previewBounds?: (id: string) => ToolBounds | null;
876
- /** Returns ids whose committed scene-render should be suppressed while this
877
- * tool is mid-gesture (e.g. cascade move's dragged + descendant ids whose
878
- * preview ghosts replace the committed pose). The standard scene slot
879
- * consults this alongside `previewPose` to avoid double-rendering. Returns
880
- * `null` when no gesture is in flight. */
881
- previewIds?: () => Iterable<string> | null;
882
- /** Optional overlay layer rendered on top of the scene/chrome whenever
883
- * this tool is in any active slot (active, hotkey, or ambient).
884
- * The layer's `draw` function reads from this tool's scratch via React
885
- * closure (re-evaluated each render). Return early from `draw` to render
886
- * nothing — typically gated on a scratch field like
887
- * `if (!scratch.overlay) return`. */
888
- overlay?: RenderLayer<unknown>;
889
- /** Declarative gesture-bindings the
890
- * dispatcher consults while this tool is active. Empty/undefined keeps
891
- * legacy imperative-channel behavior. See
892
- * `docs/superpowers/specs/2026-05-16-registry-unification-design.md`. */
893
- bindings?: GestureBinding[];
894
- /**
895
- * Optional. When set, the dispatcher consults this before its built-in
896
- * node/empty hit-test. If it returns a value, that target replaces the
897
- * default `target` on the routed action's ctx. The string `target` is
898
- * the tool's own vocabulary — the dispatcher does not interpret it.
899
- *
900
- * Used for tools that need richer sub-object hit categories (e.g., pen
901
- * edit-mode's anchor/handle/segment vs the default node/empty).
902
- */
903
- hitOverride?(ctx: {
904
- worldX: number;
905
- worldY: number;
906
- scratch: TScratch;
907
- view: View;
908
- modifiers: ToolModifiers;
909
- }): {
910
- target: string;
911
- extra?: unknown;
912
- } | null;
913
- /** Reflection escape hatch: when this `Tool` was produced by `defineTool`,
914
- * the source `ToolDef` is attached here so introspection consumers
915
- * (`buildActionRegistry`, `findConflicts`, the toolkit-builder UI, the
916
- * reflection demo) can walk the declarative source rather than the
917
- * translated runtime channels. Tools constructed without `defineTool`
918
- * may leave this undefined. Typed as `unknown` to keep this file from
919
- * importing the routing types — consumers cast at the use site. */
920
- def?: unknown;
921
- }
922
- /** Internal — which slot a tool occupies in the dispatch order. */
923
- type ToolSlot = 'hotkey' | 'active' | 'ambient';
924
- /** Internal alias for "a Tool of any scratch type" — used in registries and
925
- * dispatchers that hold tools of heterogeneous scratch shapes. `any` is
926
- * intentional: `Tool<TScratch>` is invariant in TScratch, so `Tool<unknown>`
927
- * is too strict for containers that accept any concrete `Tool<T>`. */
928
- type AnyTool = Tool<any>;
929
-
930
- /**
931
- * Read-only state that affordances consult on every render and hit-test
932
- * call. Built once per Canvas render via `buildChromeState`; affordances
933
- * must not cache it across calls.
934
- */
935
- interface ChromeState {
936
- /** Currently selected ids. Live; reflects useSelection's React state. */
937
- readonly selection: readonly NodeId[];
938
- /** True when the canvas is in multi-mode AND >= 2 ids are selected. */
939
- readonly multiActive: boolean;
940
- /** Bounds for any selection member id. Honors active-tool overlay state
941
- * (move/resize/rotate ghosts → ghost bounds; otherwise → committed
942
- * pose bounds). Returns null for unknown ids or ids whose bounds aren't
943
- * computable. */
944
- boundsOf(id: string): Bounds | null;
945
- /** Multi-union AABB when `multiActive`. Computed lazily from `boundsOf`
946
- * over every selected id; null otherwise. */
947
- readonly unionBounds: Bounds | null;
948
- /** Active modifier state at the moment of the call. */
949
- readonly modifiers: ModifierState;
950
- /** True iff the node's pose-descriptor declares it can carry a rotation.
951
- * Affordances consult this to decide whether to expose rotate cursors /
952
- * drag-bands. Defaults to `true` when the descriptor doesn't declare
953
- * (back-compat) or when the id is unknown — the rotation gesture will
954
- * no-op visually for poses without AABB fields, but the affordance
955
- * doesn't lie about the cursor. Optional on the interface so unit-test
956
- * call sites that construct `ChromeState` by hand keep compiling;
957
- * affordances should treat an absent predicate as "true". */
958
- canRotate?(id: string): boolean;
959
- }
960
-
961
- /**
962
- * @experimental
963
- * A single interactive piece of chrome. Pure functions; the kit composes
964
- * multiple affordances into a single RenderLayer per tool via
965
- * `composeAffordanceLayer`.
966
- *
967
- * Affordances declare interactive regions in a target's *local* frame.
968
- * The framework (`composeAffordanceLayer`) composes the target's bounds
969
- * transform (rotation around the AABB center, when present) for both paint
970
- * and hit-test, so the affordance never touches rotation, view.scale, or
971
- * world↔screen math.
972
- */
973
- interface Affordance {
974
- /** Stable id for debug overlays + visibility maps. */
975
- id: string;
976
- /** Enumerate this affordance's interactive regions. Each region lives in
977
- * some target id's local frame (or in the world frame when `targetId`
978
- * is `null`). Returning `[]` means "no chrome for this state" (no
979
- * selection, multi-mode disabled, etc.). */
980
- regions(state: ChromeState): readonly AffordanceRegion[];
981
- /** Optional non-interactive decoration (e.g., a leader line drawn from
982
- * a bounds edge to a handle — visual only, not draggable). Receives
983
- * raw state + view because the decoration may live outside any single
984
- * target's local frame. Most affordances leave this undefined. */
985
- decorate?(state: ChromeState, view: View): DrawCommand[];
986
- }
987
- /**
988
- * @experimental
989
- * One interactive region produced by an affordance. The framework owns
990
- * the local↔world transform for `targetId` (when non-null), so `shape`,
991
- * `paint.sizePx`, and `hitRadiusPx` are always specified in coordinates
992
- * the affordance can reason about directly.
993
- */
994
- interface AffordanceRegion<TScratch = unknown> {
995
- /** Stable id, e.g. `corner-min-min`. Used for debug overlays + a11y. */
996
- id: string;
997
- /** Target id whose `state.boundsOf(targetId)` defines this region's
998
- * local frame. `bounds.rotation` (if present) is the only transform
999
- * applied — translation is the AABB origin; scale is identity. Pass
1000
- * `null` for affordances anchored to the viewport / world frame
1001
- * (identity transform). */
1002
- targetId: string | null;
1003
- /** Region geometry, expressed in the target's local frame.
1004
- *
1005
- * - `point` — circular hit (`hitRadiusPx` is screen-space).
1006
- * - `rect` — axis-aligned rect (target rotation applies).
1007
- * - `annulus` — outer ellipse minus inner rect cutout. Used for
1008
- * invisible zones that sit *around* the AABB (e.g. rotate-on-
1009
- * hover band). The outer ellipse is defined by world-space
1010
- * semi-axes `rx` / `ry` around `(cx, cy)`; the inner rect is the
1011
- * same target-local rect that defines the AABB. Hit-test:
1012
- * inside outer ellipse AND outside inner rect. */
1013
- shape: {
1014
- kind: 'point';
1015
- x: number;
1016
- y: number;
1017
- hitRadiusPx: number;
1018
- } | {
1019
- kind: 'rect';
1020
- x: number;
1021
- y: number;
1022
- width: number;
1023
- height: number;
1024
- } | {
1025
- kind: 'annulus';
1026
- /** Outer-ellipse center (target-local). */
1027
- cx: number;
1028
- cy: number;
1029
- /** Outer-ellipse semi-axes (target-local). */
1030
- rx: number;
1031
- ry: number;
1032
- /** Inner rect (target-local) — the cutout. Typically the
1033
- * selection's AABB. */
1034
- innerX: number;
1035
- innerY: number;
1036
- innerWidth: number;
1037
- innerHeight: number;
1038
- };
1039
- /** Optional paint. World position is derived from `shape` + target
1040
- * transform; visual size stays in screen pixels (so handles don't
1041
- * warp under zoom or non-uniform scale). Omit for hit-only regions.
1042
- *
1043
- * - `square` — small fixed-size square (only valid over `point` shapes).
1044
- * - `annulus` — fill + stroke the annulus ring (only valid over
1045
- * `annulus` shapes). Uses even-odd fill rule to punch the inner-rect
1046
- * cutout.
1047
- * - `custom` — emit arbitrary draw commands; receives a {@link CustomPaintContext}. */
1048
- paint?: {
1049
- kind: 'square';
1050
- sizePx: number;
1051
- fill?: FillStyle;
1052
- stroke?: Stroke;
1053
- } | {
1054
- kind: 'annulus';
1055
- fill?: FillStyle;
1056
- stroke?: Stroke;
1057
- insetPx?: number;
1058
- } | {
1059
- kind: 'custom';
1060
- draw: (ctx: CustomPaintContext) => DrawCommand[];
1061
- };
1062
- /** Declared CSS cursor for this region. NOTE: nothing consumes this
1063
- * field yet — the hover-cursor pump lives on the dispatcher pipeline
1064
- * (`useGestureDispatcher`) and reads `AffordanceHit.cursor` from
1065
- * `buildAffordanceAt`'s synthesized hits, which do not go through
1066
- * region objects. Honoring this field is part of the pending
1067
- * affordance-layer unification (see the selection-overlay note in
1068
- * `docs/taxonomy.md`). */
1069
- cursor?: string;
1070
- /** Drag binding produced when this region is hit. Lazily called so
1071
- * affordances don't pay binding-construction cost on every paint frame —
1072
- * state snapshots (e.g., capturing per-leaf poses at click time) belong
1073
- * inside `bind()`, not inside `regions()`. */
1074
- bind(): AffordanceBinding<TScratch>;
1075
- }
1076
- /** Context passed to a region's `paint.kind === 'custom'` draw callback.
1077
- * Provides both the world-space anchor (already transformed) and the
1078
- * original local shape, for affordances that want to do additional
1079
- * geometry themselves. */
1080
- interface CustomPaintContext {
1081
- /** World-space mapping of `shape`. For `point`, only `x`/`y` are set.
1082
- * For `rect`, all four fields are set. */
1083
- world: {
1084
- x: number;
1085
- y: number;
1086
- width?: number;
1087
- height?: number;
1088
- };
1089
- /** The original local shape (same object identity as `region.shape`). */
1090
- local: AffordanceRegion['shape'];
1091
- view: View;
1092
- state: ChromeState;
1093
- }
1094
- /**
1095
- * @experimental
1096
- * Result of an affordance hit — what the dispatcher wires up as the gesture.
1097
- * Nominates the drag channel and (optionally) initial scratch state.
1098
- */
1099
- interface AffordanceBinding<TScratch = unknown> {
1100
- drag: DragChannel<TScratch>;
1101
- /** Initial scratch passed to drag.onStart. Lets the affordance pre-fill
1102
- * state from what its region's binding already computed (anchor: 'br',
1103
- * targetId: 'g1', etc.) so the tool's onStart doesn't re-derive it. */
1104
- initialScratch?: TScratch;
1105
- }
1106
-
1107
- /**
1108
- * Canvas size in CSS pixels — passed to `draw` for layers that anchor to
1109
- * canvas edges (e.g. the debug overlay's layer-list panel). The GL backend
1110
- * supplies it explicitly so layers don't have to know about DPR.
1111
- */
1112
- interface Dims {
1113
- width: number;
1114
- height: number;
1115
- }
1116
- /**
1117
- * A single named render sub-layer within a canvas renderer.
1118
- *
1119
- * @template TData - The data object passed to each draw call.
1120
- */
1121
- interface RenderLayer<TData> {
1122
- /** Unique identifier used in visibility maps and ordering arrays. */
1123
- id: string;
1124
- /** Human-readable name for UI toggles. */
1125
- label: string;
1126
- /**
1127
- * Emit a DrawCommand tree for the GL backend to dispatch.
1128
- *
1129
- * For world-space layers (the default), emit commands in WORLD COORDS —
1130
- * `drawLayers` automatically wraps them in `{ kind: 'group', transform:
1131
- * viewToMat3(view), ... }` before handing them to the renderer. Do NOT
1132
- * apply the view transform yourself.
1133
- *
1134
- * For screen-space layers (`space: 'screen'`), emit commands in CSS-pixel
1135
- * coords directly; `drawLayers` passes them through unchanged. If part
1136
- * of a screen-space layer's output needs to track the view, wrap that
1137
- * subset manually with `viewToMat3(view)`.
1138
- */
1139
- draw: (data: TData, view: View, dims: Dims) => DrawCommand[];
1140
- /**
1141
- * Whether the layer is shown when no explicit visibility entry exists.
1142
- * Defaults to `true` when absent.
1143
- */
1144
- defaultVisible?: boolean;
1145
- /**
1146
- * When true, the layer is always drawn regardless of the visibility map.
1147
- * Useful for layers that must never be hidden (e.g. base grid).
1148
- */
1149
- alwaysOn?: boolean;
1150
- /**
1151
- * Coordinate space the layer draws in.
1152
- *
1153
- * - `'world'` (default): the layer's `draw` returns world-space commands;
1154
- * `drawLayers` wraps them in a `kind: 'group'` with `viewToMat3(view)`
1155
- * automatically.
1156
- * - `'screen'`: the layer's `draw` returns screen-space (CSS-pixel)
1157
- * commands; `drawLayers` passes them through unchanged. World-anchored
1158
- * chrome inside a screen-space layer must call `worldToScreen` or wrap
1159
- * the relevant subset with `viewToMat3(view)` manually.
1160
- */
1161
- space?: 'world' | 'screen';
1162
- /**
1163
- * Optional hit-test. When defined, the dispatcher consults this on
1164
- * pointerdown (top-down layer order) before falling through to the
1165
- * active tool's slot walk. First non-null result wins; null means
1166
- * "I don't claim this hit, try the next layer."
1167
- *
1168
- * Coordinates are world-space. The `data` arg is the layer's
1169
- * configured data slot (same as `draw`); `view` and `dims` mirror
1170
- * `draw`'s arguments.
1171
- */
1172
- hitTest?: (worldX: number, worldY: number, data: TData, view: View, dims: Dims,
1173
- /** Chrome-caps visibility predicate. When supplied, the layer must
1174
- * not return a hit from any chrome element whose id reports
1175
- * `false`. Absent → every element is hittable. */
1176
- isVisible?: (id: string) => boolean) => AffordanceBinding | null;
1177
- /**
1178
- * Called on every pointermove when no gesture is currently captured.
1179
- * Lets layers (e.g. HUD widgets) track hover state without participating
1180
- * in the drag pipeline. Coords are world-space; the layer is responsible
1181
- * for any further conversion (e.g. world→screen for screen-space layers)
1182
- * and for its own throttling.
1183
- */
1184
- onUncapturedMove?: (worldX: number, worldY: number, evt: PointerEvent, view: View, dims: Dims) => void;
1185
- /**
1186
- * Called when the cursor leaves the canvas element. Lets layers clear
1187
- * any hover state they're holding.
1188
- */
1189
- onUncapturedLeave?: () => void;
1190
- }
1191
- /**
1192
- * Walk visible layers and concatenate their emitted DrawCommand arrays into
1193
- * one flat list, ready to feed to `WeaselRenderer.render(commands)`.
1194
- *
1195
- * Visibility resolution order:
1196
- * 1. `alwaysOn` — always drawn, ignores visibility map.
1197
- * 2. Explicit entry in `visibility` map — overrides default.
1198
- * 3. `layer.defaultVisible` — falls back to `true` when absent.
1199
- *
1200
- * Transform composition: world-space layers (the default; `space` unset or
1201
- * `'world'`) have their commands wrapped in a `kind: 'group'` with
1202
- * `viewToMat3(view)` before they reach the renderer. Screen-space layers
1203
- * (`space: 'screen'`) pass through unchanged.
1204
- */
1205
- declare function drawLayers<TData>(layers: RenderLayer<TData>[], data: TData, visibility: Record<string, boolean>, order: string[] | undefined, view: View | undefined, dims: Dims): DrawCommand[];
1206
-
1207
- interface SlotsState {
1208
- hotkey: AnyTool | null;
1209
- active: AnyTool | null;
1210
- ambient: AnyTool[];
1211
- /** Optional fallback tool consulted ONLY for `pointer.onClick` when neither
1212
- * the active slot nor any ambient tool claimed the click. Lets a non-select
1213
- * tool (pen, ellipse, etc.) stay active while letting unhandled clicks fall
1214
- * through to the select tool for click-to-select. Not consulted for
1215
- * pointerdown, drag, keyboard, wheel, or dblTap. */
1216
- fallback?: AnyTool | null;
1217
- }
1218
- /** @internal */
1219
- interface ToolsDispatcherOptions {
1220
- /** Called on every event to read the current slot occupants. The
1221
- * dispatcher keeps no copy — `useTools` owns slot state and updates
1222
- * it as the user activates / engages tools. */
1223
- getSlots: () => SlotsState;
1224
- /** Called once per channel-handler invocation to construct the ctx
1225
- * the handler receives. `<Canvas>` supplies world coords, modifiers,
1226
- * selection, adapter, and applyOps; the dispatcher injects scratch. */
1227
- getCtx: (overrides?: {
1228
- clientX?: number;
1229
- clientY?: number;
1230
- modifiers?: {
1231
- alt: boolean;
1232
- shift: boolean;
1233
- meta: boolean;
1234
- ctrl: boolean;
1235
- };
1236
- }) => Omit<ToolCtx, 'scratch'>;
1237
- /** Pixel distance the pointer must travel before a click is reclassified
1238
- * as a drag. Default 4. */
1239
- threshold?: number;
1240
- /** Optional callback fired whenever an in-flight gesture starts or ends
1241
- * (including phase transitions pending → drag → end/cancel). `useTools`
1242
- * uses this to bump a render tick so consumers reading
1243
- * `dispatcher.getActiveScratch()` (e.g. function-form `cursor`) re-resolve
1244
- * on real DOM events. */
1245
- onGestureChange?: () => void;
1246
- /** Called whenever a declarative route resolves to an ActionFn. The
1247
- * dispatcher caches the most recent invocation; consumers read it via
1248
- * `getLastRoute()`. Useful for the kit's ToolDebugOverlay. */
1249
- onRouteResolved?: (info: RouteResolvedInfo) => void;
1250
- /** Time source for double-tap detection. Defaults to `Date.now`. Override
1251
- * in tests to drive the clock deterministically. */
1252
- now?: () => number;
1253
- /** Double-tap detection thresholds. */
1254
- dblTap?: {
1255
- /** Maximum interval between the two taps (ms). Default 300. */
1256
- windowMs?: number;
1257
- /** Maximum CSS-px distance between the two tap positions. Default 8. */
1258
- maxDistance?: number;
1259
- };
1260
- /**
1261
- * Optional. Returns the hit-test pipeline inputs:
1262
- * - `layers`: visible RenderLayers ordered TOP-DOWN (highest z-index first).
1263
- * - `chromeState`: passed as the `data` arg to each layer's hitTest call.
1264
- * - `view` / `dims`: passed to layer.hitTest for screen-space conversion.
1265
- *
1266
- * When supplied, the dispatcher consults each layer's hitTest on
1267
- * pointerdown (top-down order) before the existing slot walk. Returns
1268
- * null (or omit the callback entirely) to disable the layer pipeline —
1269
- * legacy behavior.
1270
- */
1271
- getHitTestContext?: () => {
1272
- layers: readonly RenderLayer<unknown>[];
1273
- chromeState: ChromeState;
1274
- view: View;
1275
- dims: {
1276
- width: number;
1277
- height: number;
1278
- };
1279
- /** Chrome-caps visibility predicate, keyed by affordance id. When
1280
- * supplied, the dispatcher passes it through to each layer's
1281
- * `hitTest` so a hidden chrome element is also unhittable. Absent
1282
- * → all affordances are hittable (legacy behavior). */
1283
- isVisible?: (id: string) => boolean;
1284
- } | null;
1285
- /** Optional. Returns the scene node at the given world coords, or null
1286
- * for empty. The dispatcher uses this to populate `ctx.target` on
1287
- * pointer events (down/move/up/click/dblTap), so declarative routing
1288
- * factories can dispatch on target.kind ('rect'/'text'/'path'/'empty').
1289
- * Omit to leave the dispatcher with the always-empty fallback — see
1290
- * `nodeHitFor` for the rationale. */
1291
- getNodeAtPoint?: (worldX: number, worldY: number) => {
1292
- id: NodeId;
1293
- kind: string;
1294
- pose: unknown;
1295
- data: unknown;
1296
- meta?: Record<string, unknown>;
1297
- } | null;
1298
- }
1299
- interface ToolsDispatcher {
1300
- onPointerDown: (e: PointerEvent) => void;
1301
- onPointerMove: (e: PointerEvent) => void;
1302
- onPointerUp: (e: PointerEvent) => void;
1303
- onKeyDown: (e: KeyboardEvent) => void;
1304
- onKeyUp: (e: KeyboardEvent) => void;
1305
- onWheel: (e: WheelEvent) => void;
1306
- /** Force-cancel any in-flight gesture (used on explicit tool switch). */
1307
- cancelGesture: () => void;
1308
- /** Whether a gesture is currently in flight. Used by `useTools` to
1309
- * decide whether a modifier-key press should engage the hotkey slot
1310
- * (no, if mid-gesture). */
1311
- hasActiveGesture: () => boolean;
1312
- /** Scratch of the in-flight gesture, or `null` when idle. Exposed so
1313
- * consumers (cursor resolution, debug overlays) can read what the active
1314
- * tool is currently tracking. Read-only — do NOT mutate via this getter. */
1315
- getActiveScratch: () => unknown;
1316
- /** Most recent route resolution emitted by a declarative tool, or null
1317
- * if none has fired yet. Snapshot — safe to read on every render. */
1318
- getLastRoute: () => RouteResolvedInfo | null;
1319
- /** Resolve a synthetic (phase, gesture, hit, modifiers) query against the
1320
- * current slot occupants WITHOUT executing the matched action. Walks slots
1321
- * in real precedence order (hotkey > active > ambient) and consults each
1322
- * tool's attached `def` (the declarative `ToolDef` produced by
1323
- * `defineTool`). Tools without an attached `def` (imperative-only) are
1324
- * invisible to this query. Returns the first match, or null if no slot
1325
- * resolves the query. Pure: no scratch mutation, no scene mutation, no
1326
- * RouteResolvedInfo emission. */
1327
- resolveOnly: (query: ResolveQuery) => ResolveResult | null;
1328
- }
1329
- /** Synthetic resolution query — what the static widget asks "if a pointer
1330
- * event landed on `hit` with these `modifiers`, which declarative route
1331
- * would the dispatcher fire in this phase + gesture?" */
1332
- interface ResolveQuery {
1333
- phase: 'initial' | 'engaged';
1334
- gesture: 'click' | 'drag' | 'pointerDown' | 'dblTap' | 'wheel';
1335
- hit: HitResult;
1336
- modifiers: ToolModifiers;
1337
- }
1338
- /** Successful resolution: which tool, in which slot, matched which route-table
1339
- * key. `matchedKey` is `'*'` for function-form drag (no table to discriminate)
1340
- * and for wheel routes (single ActionFn). */
1341
- interface ResolveResult {
1342
- toolId: string;
1343
- slot: 'hotkey' | 'active' | 'ambient';
1344
- gesture: 'click' | 'drag' | 'pointerDown' | 'dblTap' | 'wheel';
1345
- phase: 'initial' | 'engaged';
1346
- matchedKey: string;
1347
- }
1348
- declare function createToolsDispatcher(opts: ToolsDispatcherOptions): ToolsDispatcher;
1349
-
1350
- /** Result of a successful `resolveRoute` lookup: the resolved `ActionFn`
1351
- * plus the route-table key that matched after precedence resolution.
1352
- * Consumers (currently only `defineTool`) use `matchedKey` for debug
1353
- * reflection — answering "why did this route fire?" by surfacing the
1354
- * exact post-precedence key. */
1355
- interface RouteMatch<TScratch> {
1356
- action: ActionFn<TScratch>;
1357
- matchedKey: string;
1358
- }
1359
- /** Resolve a route entry to an `ActionFn` (or undefined) given the current
1360
- * hit-test result and modifier snapshot. Implements the four-level
1361
- * target precedence (exact → subkind-wildcard → base-kind → universal)
1362
- * and the modifier sub-table exact-match + 'default' fallback. Returns
1363
- * the matched route-table key alongside the action so callers can attach
1364
- * it to debug-overlay reflection. */
1365
- declare function resolveRoute<TScratch>(table: RouteTable<TScratch>, hit: HitResult, modifiers: ToolModifiers): RouteMatch<TScratch> | undefined;
1366
-
1367
- /** Translate a declarative `ToolDef<TScratch>` into the existing imperative
1368
- * `Tool<TScratch>` shape. The dispatcher consumes the resulting Tool as
1369
- * it does today; declarative authoring is the only change. */
1370
- declare function defineTool<TScratch = void>(def: ToolDef<TScratch>): Tool<TScratch>;
1371
-
1372
- /** Translate a viewport-tool spec. Viewport tools have no targets,
1373
- * so route tables aren't allowed — drag is a function-form ActionFn.
1374
- * This factory delegates to defineTool after lifting the ViewportPhaseDef
1375
- * shape to the more permissive PhaseDef shape (which accepts both
1376
- * function-form and route-table drags). */
1377
- declare function defineViewportTool<TScratch = void>(def: ViewportToolDef<TScratch>): Tool<TScratch>;
1378
-
1379
- /**
1380
- * Build an `ActionFn` that forwards `ctx` + the raw DOM event to a
1381
- * consumer-supplied callback and claims the gesture.
1382
- *
1383
- * Useful for tools that want to expose an escape-hatch hook for app
1384
- * code without losing routing: route the gesture declaratively, then
1385
- * fall through to the consumer when the kit doesn't have a better
1386
- * default. The consumer-payload shape is whatever the tool wants —
1387
- * `buildPayload` produces it from `ctx` and the raw event.
1388
- *
1389
- * When `callbackRef.current` is null/undefined, returns `none()` so
1390
- * the routing engine continues to the next route (e.g. an ambient
1391
- * tool's handler).
1392
- *
1393
- * Typical use, inside a `useFooTool` hook:
1394
- *
1395
- * const onDoubleTapRef = useRef(options.onDoubleTap);
1396
- * onDoubleTapRef.current = options.onDoubleTap;
1397
- * const forwardDblTap = forwardActionTo<FooScratch, DblTapPayload>(
1398
- * onDoubleTapRef,
1399
- * (ctx, e) => ({
1400
- * worldX: ctx.worldX,
1401
- * worldY: ctx.worldY,
1402
- * event: e as PointerEvent,
1403
- * }),
1404
- * );
1405
- * // ...later, in the route table:
1406
- * dblTap: { '*': forwardDblTap, empty: forwardDblTap }
1407
- */
1408
- declare function forwardActionTo<TScratch, TPayload>(callbackRef: {
1409
- readonly current: ((payload: TPayload) => void) | null | undefined;
1410
- }, buildPayload: (ctx: Parameters<ActionFn<TScratch>>[0], event: Parameters<ActionFn<TScratch>>[1]) => TPayload): ActionFn<TScratch>;
1411
-
1412
- /** One row in the action registry — uniquely identifies a routing slot
1413
- * on one tool. Multiple rows can share (gesture, arg, target, modifiers)
1414
- * if different tools declare them; consumers walk the list and group
1415
- * client-side. */
1416
- interface RegistryEntry {
1417
- toolId: string;
1418
- phase: RoutePhase;
1419
- /** Structured v3 modifier requirements. Empty object = "no modifiers
1420
- * held" (the strict default). */
1421
- modifiers: ParsedModifiers;
1422
- gesture: GestureName;
1423
- /** Resolved arg value for arg-bearing gestures (wheel direction,
1424
- * keyDown/keyUp key, multiTouchTap fingers). Undefined for gestures
1425
- * whose descriptor has no `arg`. */
1426
- arg: string | undefined;
1427
- /** Route-table key for hit-test gestures (click/dblTap/drag/contextMenu/
1428
- * pointerDown). Undefined when the descriptor has `hasTarget: false`. */
1429
- target: string | undefined;
1430
- }
1431
- declare function buildActionRegistry(tools: readonly ToolDef<unknown>[]): RegistryEntry[];
1432
-
1433
- /** Two or more tools declare the same exact (phase, gesture, arg, target,
1434
- * modifiers) tuple — the dispatcher's slot precedence picks one
1435
- * arbitrarily (well, deterministically by slot order, but the author
1436
- * probably didn't intend the duplication). */
1437
- interface Conflict {
1438
- phase: RoutePhase;
1439
- gesture: RouteGesture;
1440
- arg: string | undefined;
1441
- target: string | undefined;
1442
- modifiers: ParsedModifiers;
1443
- /** All tool ids that registered the same tuple. At least 2 by
1444
- * construction. Order matches the input tools[] order. */
1445
- toolIds: string[];
1446
- }
1447
- /** Detect exact-tuple overlaps across a tool registration set.
1448
- *
1449
- * Intentionally NOT flagged:
1450
- * - Wildcard vs. specific (e.g., `click['*']` + `click['rect']`) — that's
1451
- * the cascading-fallback pattern; the lookup engine resolves cleanly.
1452
- * - Different modifier sub-keys on the same target — they fire on
1453
- * different inputs.
1454
- * - Ambient stacking where a tool returns `pass`/`none()` from its
1455
- * ActionFn to intentionally let another ambient tool run. We'd need
1456
- * to evaluate the ActionFn to detect intent; consumers can suppress
1457
- * known-intentional compositions in their UI layer.
1458
- */
1459
- declare function findConflicts(tools: readonly ToolDef<unknown>[]): Conflict[];
1460
-
1461
- /** Reactive snapshot of the most recently resolved route. Re-renders
1462
- * the consuming component whenever a new route resolves. Returns null
1463
- * when no route has resolved yet (or after `cancelGesture()`).
1464
- *
1465
- * Pass the kit's ToolsDispatcher reference. The hook subscribes via
1466
- * the dispatcher's existing onRouteResolved option — if the consumer's
1467
- * dispatcher wasn't constructed with this hook in mind, the consumer
1468
- * must thread an extra subscription. (Future improvement: kit-shipped
1469
- * dispatcher context.) */
1470
- declare function useToolDebugInfo(dispatcher: ToolsDispatcher): RouteResolvedInfo | null;
1471
-
1472
- interface ToolDebugOverlayProps {
1473
- /** Most recent route resolution. Pass the result of useToolDebugInfo(). */
1474
- info: RouteResolvedInfo | null;
1475
- /** Optional override for the "idle" placeholder text shown when info is null. */
1476
- emptyLabel?: string;
1477
- }
1478
- /** Dev-tools-style panel showing the most recently resolved route.
1479
- * Mount in a corner of the viewport (typically bottom-right). Pure
1480
- * presentation — no dispatcher coupling; the parent threads the info. */
1481
- declare function ToolDebugOverlay({ info, emptyLabel, }: ToolDebugOverlayProps): ReactElement;
1482
-
1483
- type index_ActionFn<TScratch> = ActionFn<TScratch>;
1484
- type index_AffordanceHit = AffordanceHit;
1485
- type index_BeginSpec<TScratch> = BeginSpec<TScratch>;
1486
- declare const index_ChannelRef: typeof ChannelRef;
1487
- type index_Conflict = Conflict;
1488
- declare const index_DescribeRouteOptions: typeof DescribeRouteOptions;
1489
- type index_EmptyHit = EmptyHit;
1490
- declare const index_GESTURE_DESCRIPTORS: typeof GESTURE_DESCRIPTORS;
1491
- declare const index_GestureArgSpec: typeof GestureArgSpec;
1492
- declare const index_GestureDescriptor: typeof GestureDescriptor;
1493
- declare const index_GestureName: typeof GestureName;
1494
- type index_HitResult = HitResult;
1495
- declare const index_ModRequirement: typeof ModRequirement;
1496
- declare const index_ModifierCombo: typeof ModifierCombo;
1497
- declare const index_ModifierKey: typeof ModifierKey;
1498
- type index_ModifierRoute<TScratch> = ModifierRoute<TScratch>;
1499
- type index_NodeHit = NodeHit;
1500
- type index_NodeRef = NodeRef;
1501
- type index_NodeRefHit = NodeRefHit;
1502
- declare const index_ParsedModifiers: typeof ParsedModifiers;
1503
- declare const index_ParsedRoute: typeof ParsedRoute;
1504
- declare const index_PhaseAtom: typeof PhaseAtom;
1505
- type index_PhaseDef<TScratch> = PhaseDef<TScratch>;
1506
- declare const index_RESERVED_ID_NAMES: typeof RESERVED_ID_NAMES;
1507
- declare const index_RESERVED_ID_PREFIXES: typeof RESERVED_ID_PREFIXES;
1508
- declare const index_ROUTE_FIELD_DEFINITIONS: typeof ROUTE_FIELD_DEFINITIONS;
1509
- declare const index_ROUTE_TERMS: typeof ROUTE_TERMS;
1510
- type index_RegistryEntry = RegistryEntry;
1511
- type index_Result<TScratch> = Result<TScratch>;
1512
- declare const index_RouteDescriptionPart: typeof RouteDescriptionPart;
1513
- type index_RouteEntry<TScratch> = RouteEntry<TScratch>;
1514
- declare const index_RouteFieldName: typeof RouteFieldName;
1515
- type index_RouteGesture = RouteGesture;
1516
- type index_RoutePhase = RoutePhase;
1517
- type index_RouteResolvedInfo = RouteResolvedInfo;
1518
- type index_RouteTable<TScratch> = RouteTable<TScratch>;
1519
- declare const index_RouteTermLabel: typeof RouteTermLabel;
1520
- declare const index_ToolDebugOverlay: typeof ToolDebugOverlay;
1521
- type index_ToolDebugOverlayProps = ToolDebugOverlayProps;
1522
- type index_ToolDef<TScratch = void> = ToolDef<TScratch>;
1523
- type index_ToolKeybinding = ToolKeybinding;
1524
- type index_ViewportPhaseDef<TScratch = void> = ViewportPhaseDef<TScratch>;
1525
- type index_ViewportToolDef<TScratch = void> = ViewportToolDef<TScratch>;
1526
- declare const index_apply: typeof apply;
1527
- declare const index_begin: typeof begin;
1528
- declare const index_buildActionRegistry: typeof buildActionRegistry;
1529
- declare const index_cancel: typeof cancel;
1530
- declare const index_canonicalModifiers: typeof canonicalModifiers;
1531
- declare const index_claim: typeof claim;
1532
- declare const index_collapseShiftPairs: typeof collapseShiftPairs;
1533
- declare const index_commit: typeof commit;
1534
- declare const index_defineTool: typeof defineTool;
1535
- declare const index_defineViewportTool: typeof defineViewportTool;
1536
- declare const index_describeRoute: typeof describeRoute;
1537
- declare const index_describeRouteParts: typeof describeRouteParts;
1538
- declare const index_findConflicts: typeof findConflicts;
1539
- declare const index_formatPhaseAtom: typeof formatPhaseAtom;
1540
- declare const index_formatRoute: typeof formatRoute;
1541
- declare const index_formatRouteResolved: typeof formatRouteResolved;
1542
- declare const index_forwardActionTo: typeof forwardActionTo;
1543
- declare const index_getGestureDescriptor: typeof getGestureDescriptor;
1544
- declare const index_hold: typeof hold;
1545
- declare const index_isKnownGestureName: typeof isKnownGestureName;
1546
- declare const index_mods: typeof mods;
1547
- declare const index_none: typeof none;
1548
- declare const index_parseRoute: typeof parseRoute;
1549
- declare const index_resolveRoute: typeof resolveRoute;
1550
- declare const index_useToolDebugInfo: typeof useToolDebugInfo;
1551
- declare namespace index {
1552
- export { type index_ActionFn as ActionFn, type index_AffordanceHit as AffordanceHit, type index_BeginSpec as BeginSpec, index_ChannelRef as ChannelRef, type index_Conflict as Conflict, index_DescribeRouteOptions as DescribeRouteOptions, type index_EmptyHit as EmptyHit, index_GESTURE_DESCRIPTORS as GESTURE_DESCRIPTORS, index_GestureArgSpec as GestureArgSpec, index_GestureDescriptor as GestureDescriptor, index_GestureName as GestureName, type index_HitResult as HitResult, index_ModRequirement as ModRequirement, index_ModifierCombo as ModifierCombo, index_ModifierKey as ModifierKey, type index_ModifierRoute as ModifierRoute, type index_NodeHit as NodeHit, type index_NodeRef as NodeRef, type index_NodeRefHit as NodeRefHit, index_ParsedModifiers as ParsedModifiers, index_ParsedRoute as ParsedRoute, index_PhaseAtom as PhaseAtom, type index_PhaseDef as PhaseDef, 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, type index_Result as Result, index_RouteDescriptionPart as RouteDescriptionPart, type index_RouteEntry as RouteEntry, index_RouteFieldName as RouteFieldName, type index_RouteGesture as RouteGesture, type index_RoutePhase as RoutePhase, type index_RouteResolvedInfo as RouteResolvedInfo, type index_RouteTable as RouteTable, index_RouteTermLabel as RouteTermLabel, index_ToolDebugOverlay as ToolDebugOverlay, type index_ToolDebugOverlayProps as ToolDebugOverlayProps, type index_ToolDef as ToolDef, type index_ToolKeybinding as ToolKeybinding, type index_ViewportPhaseDef as ViewportPhaseDef, type index_ViewportToolDef as ViewportToolDef, index_apply as apply, index_begin as begin, index_buildActionRegistry as buildActionRegistry, index_cancel as cancel, index_canonicalModifiers as canonicalModifiers, index_claim as claim, index_collapseShiftPairs as collapseShiftPairs, index_commit as commit, 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_formatRouteResolved as formatRouteResolved, index_forwardActionTo as forwardActionTo, index_getGestureDescriptor as getGestureDescriptor, index_hold as hold, index_isKnownGestureName as isKnownGestureName, index_mods as mods, index_none as none, index_parseRoute as parseRoute, index_resolveRoute as resolveRoute, index_useToolDebugInfo as useToolDebugInfo };
1553
- }
1554
-
1555
- export { type NodeHit as $, type ActionDeps as A, type BindingOpts as B, type ChromeState as C, type Dims as D, type ToolSlot as E, createToolsDispatcher as F, type GestureBinding as G, type HotkeyTrigger as H, type Invoker as I, drawLayers as J, type KeyboardChannel as K, index as L, useSelection as M, type ActionFn as N, type OngoingHandle as O, type Point2 as P, type AffordanceHit as Q, type RenderLayer as R, type SelectionApi as S, type Tool as T, type UseSelectionOptions as U, type BeginSpec as V, type WheelChannel as W, type Conflict as X, type EmptyHit as Y, type HitResult as Z, type ModifierRoute as _, type AffordanceHit$1 as a, type NodeRef as a0, type NodeRefHit as a1, type PhaseDef as a2, type RegistryEntry as a3, type Result as a4, type RouteEntry as a5, type RouteGesture as a6, type RoutePhase as a7, type RouteResolvedInfo as a8, type RouteTable as a9, ToolDebugOverlay as aa, type ToolDebugOverlayProps as ab, type ToolDef as ac, type ViewportPhaseDef as ad, type ViewportToolDef as ae, apply as af, begin as ag, buildActionRegistry as ah, cancel as ai, claim as aj, commit as ak, defineTool as al, defineViewportTool as am, findConflicts as an, formatRouteResolved as ao, forwardActionTo as ap, hold as aq, none as ar, resolveRoute as as, useToolDebugInfo as at, type AnyTool as b, type ToolsDispatcher as c, type ToolCtx as d, type ToolsDispatcherOptions as e, type ToolKeybinding as f, type Affordance as g, type AffordanceBinding as h, type AffordanceRegion as i, type CustomPaintContext as j, type DblTapChannel as k, type Decision as l, type DragChannel as m, type ImmediateInvoker as n, type InvocationCtx as o, type OngoingInvoker as p, type OngoingOverlay as q, type PointerChannel as r, type ResolveQuery as s, type ResolveResult as t, type RouteMatch as u, type SelectionExtendKey as v, type SelectionMode as w, type ToolHit as x, type ToolModifiers as y, type ToolPresentation as z };