@weasel-js/core 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +937 -2202
  2. package/README.md +118 -75
  3. package/dist/{autoPoseDescriptor-DF1SnnSx.d.ts → autoPoseDescriptor-CvjflWJK.d.ts} +29 -26
  4. package/dist/{chunk-2VXGHUVL.js → chunk-BDWAA634.js} +4 -22
  5. package/dist/chunk-BDWAA634.js.map +1 -0
  6. package/dist/{chunk-R3AWPTLZ.js → chunk-MG7OXCAI.js} +2760 -4491
  7. package/dist/chunk-MG7OXCAI.js.map +1 -0
  8. package/dist/{chunk-PRGBGMH3.js → chunk-MQI4PIX3.js} +3 -3
  9. package/dist/chunk-MQI4PIX3.js.map +1 -0
  10. package/dist/{chunk-WPM42WJP.js → chunk-UCPV7JXC.js} +201 -256
  11. package/dist/chunk-UCPV7JXC.js.map +1 -0
  12. package/dist/clipboard.d.ts +2 -3
  13. package/dist/clone.d.ts +3 -2
  14. package/dist/depSchema-nMqj_qTM.d.ts +3490 -0
  15. package/dist/{grid-0Pbn5B2C.d.ts → grid-BrIa38gG.d.ts} +7 -10
  16. package/dist/index.d.ts +1996 -1229
  17. package/dist/index.js +4 -5
  18. package/dist/insert.d.ts +4 -4
  19. package/dist/insert.js +1 -1
  20. package/dist/move.d.ts +5 -6
  21. package/dist/move.js +3 -6
  22. package/dist/move.js.map +1 -1
  23. package/dist/{options-DbYLImvq.d.ts → options-BDyCnrp8.d.ts} +3 -2
  24. package/dist/poseDescriptor-CGOgIgf8.d.ts +134 -0
  25. package/dist/renderer.d.ts +10 -4
  26. package/dist/renderer.js +4 -5
  27. package/dist/resize.d.ts +10 -12
  28. package/dist/resize.js +2 -2
  29. package/dist/routing.d.ts +1 -142
  30. package/dist/routing.js +1 -1
  31. package/dist/routing.js.map +1 -1
  32. package/dist/{types-ei3UMl9R.d.ts → types-DMyo7dnM.d.ts} +12 -41
  33. package/dist/{types-DEALFt5F.d.ts → types-DtjCJA5r.d.ts} +9 -3
  34. package/package.json +13 -10
  35. package/dist/DrawCommand-CD-ug3d9.d.ts +0 -332
  36. package/dist/builtins-BXFBXegF.d.ts +0 -840
  37. package/dist/chunk-2VXGHUVL.js.map +0 -1
  38. package/dist/chunk-BL65SHCX.js +0 -573
  39. package/dist/chunk-BL65SHCX.js.map +0 -1
  40. package/dist/chunk-PRGBGMH3.js.map +0 -1
  41. package/dist/chunk-R3AWPTLZ.js.map +0 -1
  42. package/dist/chunk-WPM42WJP.js.map +0 -1
  43. package/dist/geometry-6fCNhAux.d.ts +0 -114
  44. package/dist/path-JEV2c5If.d.ts +0 -48
  45. package/dist/registry-BY-wI9gm.d.ts +0 -4003
  46. package/dist/types-BHK2dkMu.d.ts +0 -172
  47. package/dist/types-bcc7jcUy.d.ts +0 -594
  48. package/dist/view-DSQgxBJB.d.ts +0 -63
@@ -1,4003 +0,0 @@
1
- import { N as NodeId, R as RectPose, S as Scene } from './types-bcc7jcUy.js';
2
- import { M as ModifierState, A as ActionBehavior, c as ResizeAnchor, R as ResizePose, B as BoundsConstraint, P as PointSnapBehavior } from './types-ei3UMl9R.js';
3
- import { V as View } from './view-DSQgxBJB.js';
4
- import { CapabilityTag } from '@weasel-js/modes';
5
- import * as React from 'react';
6
- import { MutableRefObject, ReactNode, ReactElement } from 'react';
7
- import { GestureSpec, IngestItem, InputEvent } from '@weasel-js/gestures';
8
- import { D as DrawCommand, E as Effect } from './DrawCommand-CD-ug3d9.js';
9
- import { B as Bounds, P as PoseProjection, F as FitViewToBoundsOptions, V as ViewportDims } from './geometry-6fCNhAux.js';
10
- import { FillStyle, Stroke } from '@weasel-js/paint';
11
- import { CursorSpec } from '@weasel-js/cursor';
12
- import * as react_jsx_runtime from 'react/jsx-runtime';
13
- import { P as Path } from './path-JEV2c5If.js';
14
- import { Op, History } from '@weasel-js/history';
15
- import { a as SceneAdapter, L as LayoutStrategy, I as InsertAdapter } from './types-DEALFt5F.js';
16
- import { D as DebugSink } from './types-BHK2dkMu.js';
17
- import { Mat3 } from '@weasel-js/geom';
18
-
19
- interface ChromeState {
20
- /** Currently selected ids. Live; reflects useSelection's React state. */
21
- readonly selection: readonly NodeId[];
22
- /** True when the canvas is in multi-mode AND >= 2 ids are selected. */
23
- readonly multiActive: boolean;
24
- /** Bounds for any selection member id. Honors active-tool overlay state
25
- * (move/resize/rotate ghosts → ghost bounds; otherwise → committed
26
- * pose bounds). Returns null for unknown ids or ids whose bounds aren't
27
- * computable. */
28
- boundsOf(id: string): Bounds | null;
29
- /** Multi-union AABB when `multiActive`. Computed lazily from `boundsOf`
30
- * over every selected id, expanding rotated members to the extent of
31
- * their ink; null otherwise. */
32
- readonly unionBounds: Bounds | null;
33
- /** Active modifier state at the moment of the call. */
34
- readonly modifiers: ModifierState;
35
- /** True iff the node's pose-descriptor declares it can carry a rotation.
36
- * Affordances consult this to decide whether to expose rotate cursors /
37
- * drag-bands. Defaults to `true` when the descriptor doesn't declare
38
- * (back-compat) or when the id is unknown — the rotation gesture will
39
- * no-op visually for poses without AABB fields, but the affordance
40
- * doesn't lie about the cursor. Optional on the interface so unit-test
41
- * call sites that construct `ChromeState` by hand keep compiling;
42
- * affordances should treat an absent predicate as "true". */
43
- canRotate?(id: string): boolean;
44
- }
45
-
46
- /**
47
- * @experimental
48
- * A single interactive piece of chrome. Pure functions; the kit composes
49
- * multiple affordances into a single RenderLayer per tool via
50
- * `composeAffordanceLayer`.
51
- *
52
- * Affordances declare interactive regions in a target's *local* frame.
53
- * The framework (`composeAffordanceLayer`) composes the target's bounds
54
- * transform (rotation around the AABB center, when present) for both paint
55
- * and hit-test, so the affordance never touches rotation, view.scale, or
56
- * world↔screen math.
57
- */
58
- interface Affordance {
59
- /** Stable id for debug overlays + visibility maps. */
60
- id: string;
61
- /** Enumerate this affordance's interactive regions. Each region lives in
62
- * some target id's local frame (or in the world frame when `targetId`
63
- * is `null`). Returning `[]` means "no chrome for this state" (no
64
- * selection, multi-mode disabled, etc.). */
65
- regions(state: ChromeState): readonly AffordanceRegion[];
66
- /** Optional non-interactive decoration (e.g., a leader line drawn from
67
- * a bounds edge to a handle — visual only, not draggable). Receives
68
- * raw state + view because the decoration may live outside any single
69
- * target's local frame. Most affordances leave this undefined. */
70
- decorate?(state: ChromeState, view: View): DrawCommand[];
71
- }
72
- /**
73
- * @experimental
74
- * One interactive region produced by an affordance. The framework owns
75
- * the local↔world transform for `targetId` (when non-null), so `shape`,
76
- * `paint.sizePx`, and `hitRadiusPx` are always specified in coordinates
77
- * the affordance can reason about directly.
78
- */
79
- interface AffordanceRegion<TScratch = unknown> {
80
- /** Stable id, e.g. `corner-min-min`. Used for debug overlays + a11y. */
81
- id: string;
82
- /** Target id whose `state.boundsOf(targetId)` defines this region's
83
- * local frame. `bounds.rotation` (if present) is the only transform
84
- * applied — translation is the AABB origin; scale is identity. Pass
85
- * `null` for affordances anchored to the viewport / world frame
86
- * (identity transform). */
87
- targetId: string | null;
88
- /** Region geometry, expressed in the target's local frame.
89
- *
90
- * - `point` — circular hit (`hitRadiusPx` is screen-space).
91
- * - `rect` — axis-aligned rect (target rotation applies).
92
- * - `annulus` — outer ellipse minus inner rect cutout. Used for
93
- * invisible zones that sit *around* the AABB (e.g. rotate-on-
94
- * hover band). The outer ellipse is defined by world-space
95
- * semi-axes `rx` / `ry` around `(cx, cy)`; the inner rect is the
96
- * same target-local rect that defines the AABB. Hit-test:
97
- * inside outer ellipse AND outside inner rect. */
98
- shape: {
99
- kind: 'point';
100
- x: number;
101
- y: number;
102
- hitRadiusPx: number;
103
- } | {
104
- kind: 'rect';
105
- x: number;
106
- y: number;
107
- width: number;
108
- height: number;
109
- } | {
110
- kind: 'annulus';
111
- /** Outer-ellipse center (target-local). */
112
- cx: number;
113
- cy: number;
114
- /** Outer-ellipse semi-axes (target-local). */
115
- rx: number;
116
- ry: number;
117
- /** Inner rect (target-local) — the cutout. Typically the
118
- * selection's AABB. */
119
- innerX: number;
120
- innerY: number;
121
- innerWidth: number;
122
- innerHeight: number;
123
- /** Minimum band thickness outside the inner rect, in **screen**
124
- * pixels. The framework widens `rx`/`ry` to at least
125
- * `innerHalfExtent + minBandPx / meanScale(view.scale)` for both
126
- * paint and hit-test.
127
- *
128
- * This exists because the clamp has to know the view and the
129
- * affordance doesn't: `ChromeState` carries no scale. Expressing the
130
- * floor in world units instead (which is what the rotate ring used
131
- * to do) makes the band shrink on screen as you zoom in, until the
132
- * ring around a small shape is too thin to hover. */
133
- minBandPx?: number;
134
- };
135
- /** Optional paint. World position is derived from `shape` + target
136
- * transform; visual size stays in screen pixels (so handles don't
137
- * warp under zoom or non-uniform scale). Omit for hit-only regions.
138
- *
139
- * - `square` — small fixed-size square (only valid over `point` shapes).
140
- * - `annulus` — fill + stroke the annulus ring (only valid over
141
- * `annulus` shapes). Uses even-odd fill rule to punch the inner-rect
142
- * cutout.
143
- * - `custom` — emit arbitrary draw commands; receives a {@link CustomPaintContext}. */
144
- paint?: {
145
- kind: 'square';
146
- sizePx: number;
147
- fill?: FillStyle;
148
- stroke?: Stroke;
149
- } | {
150
- kind: 'annulus';
151
- fill?: FillStyle;
152
- stroke?: Stroke;
153
- insetPx?: number;
154
- } | {
155
- kind: 'custom';
156
- draw: (ctx: CustomPaintContext) => DrawCommand[];
157
- };
158
- /** Discriminator a press on this region reports as `AffordanceHit.kind` —
159
- * the string routing specs match on (`'handle:top-left'`,
160
- * `'rotate-handle'`, `'anchor:3'`). Omit for regions that only exist
161
- * inside a consumer-registered layer, where the layer id is the
162
- * discriminator; `buildAffordanceAt` falls back to
163
- * `<affordanceId>:<regionId>`. */
164
- hitKind?: string;
165
- /** Cursor to show while hovering this region. Read by the hover-cursor
166
- * pump in `useGestureDispatcher` via `AffordanceHit.cursor`, which
167
- * `buildAffordanceAt` fills in from the region the walk landed on. */
168
- cursor?: CursorSpec;
169
- /** `'exclusive'` bars every binding whose target doesn't consult the
170
- * affordance. Read only when the affordance is composed into a
171
- * consumer-registered layer; kit chrome routes through
172
- * `buildAffordanceAt`, which reports `'shared'`. */
173
- strength?: 'exclusive' | 'shared';
174
- /** Which gestures an exclusive claim bars. Omitted bars all of them. */
175
- claimedKinds?: readonly ClaimableGesture[];
176
- /** Drag binding produced when this region is hit. Lazily called so
177
- * affordances don't pay binding-construction cost on every paint frame —
178
- * state snapshots (e.g., capturing per-leaf poses at click time) belong
179
- * inside `bind()`, not inside `regions()`. */
180
- bind(): AffordanceBinding<TScratch>;
181
- }
182
- /** Context passed to a region's `paint.kind === 'custom'` draw callback.
183
- * Provides both the world-space anchor (already transformed) and the
184
- * original local shape, for affordances that want to do additional
185
- * geometry themselves. */
186
- interface CustomPaintContext {
187
- /** World-space mapping of `shape`. For `point`, only `x`/`y` are set.
188
- * For `rect`, all four fields are set. */
189
- world: {
190
- x: number;
191
- y: number;
192
- width?: number;
193
- height?: number;
194
- };
195
- /** The original local shape (same object identity as `region.shape`). */
196
- local: AffordanceRegion['shape'];
197
- view: View;
198
- state: ChromeState;
199
- }
200
- /**
201
- * @experimental
202
- * Result of an affordance hit — what the region computed about itself.
203
- *
204
- * `initialScratch` is the payload: what the region already knows (which
205
- * corner, which target id) so the action that picks up the drag doesn't
206
- * re-derive it. `<SceneCanvas>` reads it out of the layer hit-test and packs
207
- * it into `AffordanceHit`, which flows to the matching action through
208
- * `InvocationCtx.drag.affordance`.
209
- *
210
- * This used to also carry a `drag: DragChannel` naming the handlers the
211
- * tool-routing dispatcher should wire up. Every implementation supplied a
212
- * no-op stub that claimed, because the real routing had already moved to
213
- * bindings; the field went with that dispatcher.
214
- */
215
- interface AffordanceBinding<TScratch = unknown> {
216
- initialScratch?: TScratch;
217
- }
218
- /**
219
- * The fields `buildAffordanceAt` lifts out of a region's `initialScratch`
220
- * when it turns a region hit into an `AffordanceHit`.
221
- *
222
- * Scratch is otherwise opaque — whatever the affordance wants to hand the
223
- * action that picks up the drag. These few names are the exception: they mean
224
- * the same thing to every affordance, and the actions that consume them
225
- * (`resizeAction`, `rotateAction`) read them off `AffordanceHit` rather than
226
- * out of scratch. An affordance that doesn't set them simply produces a hit
227
- * without those fields.
228
- */
229
- interface CommonAffordanceScratch {
230
- /** The node (or `MULTI_RESIZE_TARGET_ID`) this chrome acts on. Becomes
231
- * `AffordanceHit.targetIds`. */
232
- targetId?: string;
233
- /** For resize chrome: which corner stays pinned. Mirrors the kit's
234
- * `ResizeAnchor`, spelled inline so `affordances/` doesn't take a type
235
- * dependency on the gesture layer for one field. */
236
- anchor?: {
237
- x: 'min' | 'max' | 'free';
238
- y: 'min' | 'max' | 'free';
239
- };
240
- /** World-space invariant point of the transform — the fixed corner for a
241
- * resize, the pivot for a rotation. */
242
- fixedPoint?: {
243
- x: number;
244
- y: number;
245
- };
246
- }
247
- /**
248
- * What a **registered layer's** `hitTest` returns. Extends `AffordanceBinding`
249
- * so existing implementations keep typechecking; the added fields are how a
250
- * consumer's own chrome says the things kit chrome says through
251
- * `AffordanceRegion` — which cursor to show, and whether it owns the point
252
- * outright.
253
- */
254
- interface LayerHit<TScratch = unknown> extends AffordanceBinding<TScratch> {
255
- /** Cursor while the pointer is over this hit. Reaches the hover-cursor
256
- * pump as `AffordanceHit.cursor`, the same path kit chrome uses. */
257
- cursor?: CursorSpec;
258
- /** `'exclusive'` bars every binding whose target doesn't consult the
259
- * affordance. Omitted means `'shared'` — today's behavior. Same name and
260
- * meaning as `AffordanceHit.strength`, which it becomes. */
261
- strength?: 'exclusive' | 'shared';
262
- /** Which gestures an exclusive claim bars. Omitted bars all of them. */
263
- claimedKinds?: readonly ClaimableGesture[];
264
- }
265
- /**
266
- * Gesture kinds an affordance claim can bar, in the spec vocabulary bindings
267
- * are written in. `'pointer'` is one token because `pointerDown` / `click` /
268
- * `drag` are a single press protocol — at the event level the first two are
269
- * the same `kind: 'pointerdown'`, told apart only by `stage`.
270
- */
271
- type ClaimableGesture = 'pointer' | 'doubleClick' | 'contextMenu' | 'longPress' | 'wheel';
272
-
273
- /**
274
- * Canvas size in CSS pixels — passed to `draw` for layers that anchor to
275
- * canvas edges (e.g. the debug overlay's layer-list panel). The GL backend
276
- * supplies it explicitly so layers don't have to know about DPR.
277
- */
278
- interface Dims {
279
- width: number;
280
- height: number;
281
- }
282
- /**
283
- * What a layer's `draw` threw, and which layer threw it.
284
- *
285
- * A `draw` runs on the frame loop, so a throw that escapes it surfaces as an
286
- * uncaught `requestAnimationFrame` error on the window and takes the whole
287
- * frame with it — every other layer included. One broken layer painting
288
- * nothing, named in the console, is the lesser wrong.
289
- */
290
- interface LayerDrawFailure {
291
- layerId: string;
292
- error: unknown;
293
- }
294
- /**
295
- * Several consecutive layers composited as one.
296
- *
297
- * A layer's own `effects` run over that layer alone, which is the wrong
298
- * picture whenever a pass reads neighboring pixels: `blur(A over B)` is not
299
- * `blur(A) over blur(B)`, and it costs a buffer and a pass chain per layer. A
300
- * group draws its members into one buffer, runs one chain over it, and
301
- * composites the result back once.
302
- *
303
- * Membership is by `RenderLayer.id` — the same names `layerOrder` and
304
- * `layerVisibility` use, not the `layers` map's slot keys.
305
- *
306
- * Only *consecutive* members share a buffer, because anything drawn between
307
- * two members has to land between them. A group whose members are separated in
308
- * the render order is drawn as one bracket per run, with a warning: the
309
- * picture is right, the declaration almost certainly is not.
310
- *
311
- * This is a render-stack bracket, not a scene `ContainerNode` — it holds no
312
- * ids, survives no reload, and nothing in the scene knows about it.
313
- */
314
- interface LayerGroup {
315
- /** Names the group in warnings; not a layer id and never drawn. */
316
- id: string;
317
- /** Member layer ids. Order here is ignored — the render order decides. */
318
- layers: readonly string[];
319
- /**
320
- * Passes over the group's combined pixels, in order. A thunk is re-read on
321
- * every frame, so an animating radius costs no React render; an array is
322
- * read once per frame either way.
323
- */
324
- effects?: readonly Effect[] | ((view: View, dims: Dims) => readonly Effect[]);
325
- /** Opacity applied to the group's composited result, not to each member. */
326
- alpha?: number;
327
- /** 4×5 color matrix (row-major, 20 numbers) applied to the composited
328
- * result. See `GroupDrawCommand.colorMatrix`. */
329
- colorMatrix?: number[];
330
- }
331
- /** One layer's memoized output, keyed by layer id. Owned by the canvas that
332
- * calls `drawLayers`, not by `drawLayers` itself — the function is pure. */
333
- type LayerCommandCache = Map<string, {
334
- deps: readonly unknown[];
335
- cmds: DrawCommand[];
336
- }>;
337
- /**
338
- * A single named render sub-layer within a canvas renderer.
339
- *
340
- * @template TData - The data object passed to each draw call.
341
- */
342
- interface RenderLayer<TData> {
343
- /** Unique identifier used in visibility maps and ordering arrays. When a
344
- * cache is in use, an id must identify the same logical layer across
345
- * frames — reusing it for a different layer can serve cross-layer commands. */
346
- id: string;
347
- /** Human-readable name for UI toggles. */
348
- label: string;
349
- /**
350
- * Emit a DrawCommand tree for the GL backend to dispatch.
351
- *
352
- * For world-space layers (the default), emit commands in WORLD COORDS —
353
- * `drawLayers` automatically wraps them in `{ kind: 'group', transform:
354
- * viewToMat3(view), ... }` before handing them to the renderer. Do NOT
355
- * apply the view transform yourself.
356
- *
357
- * For screen-space layers (`space: 'screen'`), emit commands in CSS-pixel
358
- * coords directly; `drawLayers` passes them through unchanged. If part
359
- * of a screen-space layer's output needs to track the view, wrap that
360
- * subset manually with `viewToMat3(view)`.
361
- */
362
- draw: (data: TData, view: View, dims: Dims) => DrawCommand[];
363
- /**
364
- * Optional cache key. When present and a `LayerCommandCache` is supplied to
365
- * `drawLayers`, the layer's previous `DrawCommand[]` is reused as long as
366
- * every entry is `Object.is`-equal to the previous call's. A layer with no
367
- * `deps` rebuilds on every frame.
368
- *
369
- * **The returned commands must be treated as immutable.** A cached tree is
370
- * handed to the renderer again on later frames, so mutating a tree you
371
- * previously returned corrupts the cache silently rather than erroring.
372
- *
373
- * **Screen-space layers are not protected against a stale `view`/`dims`
374
- * the way world-space layers are** (see `space` below) — include them in
375
- * `deps` if `draw` reads them.
376
- */
377
- deps?: (data: TData, view: View, dims: Dims) => readonly unknown[];
378
- /**
379
- * Whether the layer is shown when no explicit visibility entry exists.
380
- * Defaults to `true` when absent.
381
- */
382
- defaultVisible?: boolean;
383
- /**
384
- * When true, the layer is always drawn regardless of the visibility map.
385
- * Useful for layers that must never be hidden (e.g. base grid).
386
- */
387
- alwaysOn?: boolean;
388
- /**
389
- * Coordinate space the layer draws in.
390
- *
391
- * - `'world'` (default): the layer's `draw` returns world-space commands;
392
- * `drawLayers` wraps them in a `kind: 'group'` with `viewToMat3(view)`
393
- * automatically.
394
- * - `'screen'`: the layer's `draw` returns screen-space (CSS-pixel)
395
- * commands; `drawLayers` passes them through unchanged. World-anchored
396
- * chrome inside a screen-space layer must call `worldToScreen` or wrap
397
- * the relevant subset with `viewToMat3(view)` manually.
398
- */
399
- space?: 'world' | 'screen';
400
- /**
401
- * Full-screen passes run over this layer's own pixels before it joins the
402
- * frame — a blur here blurs the world and leaves the HUD drawn above it
403
- * sharp, which is the thing a CSS `filter` on the `<canvas>` cannot do.
404
- *
405
- * Costs nothing while empty: the renderer allocates no offscreen buffer
406
- * until a layer actually declares one. See `GroupDrawCommand.effects` for
407
- * what a pass may read, and {@link LayerGroup} to run one chain over
408
- * several layers at once instead of one chain each.
409
- */
410
- effects?: readonly Effect[];
411
- /**
412
- * Optional hit-test for **consumer-attached** layers.
413
- *
414
- * Only layers registered through `CanvasExtensionApi.registerLayer` are
415
- * hit-tested: `hitTestExtras` walks them last-registered-first on
416
- * pointerdown, and `<SceneCanvas>` folds the result into its `affordanceAt`
417
- * thunk ahead of the kit's own selection chrome. First non-null result
418
- * wins; null means "I don't claim this hit, try the next layer."
419
- *
420
- * Layers that reach the draw stack some other way — a `Tool.overlay`, an
421
- * entry in the `layers` map — are painted but never hit-tested, so defining
422
- * `hitTest` on one has no effect. (The kit's own chrome doesn't need it: it
423
- * goes through `buildAffordanceAt`.)
424
- *
425
- * Coordinates are world-space. The `data` arg is the layer's
426
- * configured data slot (same as `draw`); `view` and `dims` mirror
427
- * `draw`'s arguments.
428
- */
429
- hitTest?: (worldX: number, worldY: number, data: TData, view: View, dims: Dims,
430
- /** Chrome-caps visibility predicate. When supplied, the layer must
431
- * not return a hit from any chrome element whose id reports
432
- * `false`. Absent → every element is hittable. */
433
- isVisible?: (id: string) => boolean) => LayerHit | null;
434
- /**
435
- * Called on every pointermove when no gesture is currently captured.
436
- * Lets layers (e.g. HUD widgets) track hover state without participating
437
- * in the drag pipeline. Coords are world-space; the layer is responsible
438
- * for any further conversion (e.g. world→screen for screen-space layers)
439
- * and for its own throttling.
440
- */
441
- onUncapturedMove?: (worldX: number, worldY: number, evt: PointerEvent, view: View, dims: Dims) => void;
442
- /**
443
- * Called when the cursor leaves the canvas element. Lets layers clear
444
- * any hover state they're holding.
445
- */
446
- onUncapturedLeave?: () => void;
447
- }
448
- /**
449
- * Walk visible layers and concatenate their emitted DrawCommand arrays into
450
- * one flat list, ready to feed to `WeaselRenderer.render(commands)`.
451
- *
452
- * Visibility resolution order:
453
- * 1. `alwaysOn` — always drawn, ignores visibility map.
454
- * 2. Explicit entry in `visibility` map — overrides default.
455
- * 3. `layer.defaultVisible` — falls back to `true` when absent.
456
- *
457
- * Transform composition: world-space layers (the default; `space` unset or
458
- * `'world'`) have their commands wrapped in a `kind: 'group'` with
459
- * `viewToMat3(view)` before they reach the renderer. Screen-space layers
460
- * (`space: 'screen'`) pass through unchanged.
461
- *
462
- * A layer whose `draw` throws is dropped for the frame and reported through
463
- * `onLayerError` — the rest of the frame still paints.
464
- *
465
- * `groups` brackets runs of consecutive layers so they composite as one — see
466
- * `LayerGroup`. A layer named by no group is emitted exactly as it was before
467
- * groups existed, and a frame that declares none allocates nothing.
468
- */
469
- declare function drawLayers<TData>(layers: RenderLayer<TData>[], data: TData, visibility: Record<string, boolean>, order: string[] | undefined, view: View | undefined, dims: Dims, cache?: LayerCommandCache, onLayerError?: (failure: LayerDrawFailure) => void, groups?: readonly LayerGroup[]): DrawCommand[];
470
- /**
471
- * Resolve one layer's visibility: `alwaysOn` wins, then an explicit entry in
472
- * `visibility`, then `defaultVisible`, defaulting to shown.
473
- */
474
- declare function isLayerVisible<TData>(layer: RenderLayer<TData>, visibility: Record<string, boolean>): boolean;
475
- /**
476
- * Does this layer reach the screen at all — both gates, in the order
477
- * `drawLayers` applies them.
478
- *
479
- * **A listed `order` is the whole list**, so omission from it drops a layer
480
- * that `visibility` would have shown, and `alwaysOn` does not rescue it.
481
- * Hit-testing asks this, not `isLayerVisible`: a layer that is not painted
482
- * but still claims pointer events is a pointer landing on nothing the user
483
- * can see.
484
- */
485
- declare function isLayerPainted<TData>(layer: RenderLayer<TData>, visibility: Record<string, boolean>, order: string[] | undefined): boolean;
486
- /**
487
- * Draw one layer and put its commands in the space its `space` declares:
488
- * world-space output wrapped in a `viewToMat3(view)` group, screen-space
489
- * output passed through.
490
- *
491
- * Anything rendering layers through a view — the canvas itself, a viewport
492
- * node's inner pass — goes through here. A second copy of this rule that
493
- * forgets the wrap draws world content at raw world coords, which looks
494
- * plausible at the identity view and wrong everywhere else.
495
- *
496
- * A `draw` that throws yields no commands, and `onLayerError` is told which
497
- * layer it was. The layer's cache entry goes with it, so the next frame is a
498
- * real re-attempt rather than a stale tree served under fresh deps.
499
- */
500
- declare function drawOneLayer<TData>(layer: RenderLayer<TData>, data: TData, view: View, dims: Dims, cache?: LayerCommandCache, onLayerError?: (failure: LayerDrawFailure) => void): DrawCommand[];
501
-
502
- /**
503
- * Facts about the device the canvas is running on.
504
- *
505
- * One object, recomputed when the underlying media queries change, read by
506
- * two consumers: the chrome-caps rule layer (via `RuleCtx.device`) and the
507
- * handle-sizing constants (via `targetScale`).
508
- *
509
- * Deliberately NOT a form-factor concept. There is no `isPhone` here and
510
- * there should never be one: chrome layout is the consuming app's decision.
511
- * The kit's job is to stop assuming a mouse.
512
- */
513
- interface DeviceProfile {
514
- /** `matchMedia('(pointer: coarse)')` — the primary pointer is imprecise. */
515
- readonly coarsePointer: boolean;
516
- /** `matchMedia('(hover: hover)')` — the primary pointer can hover. */
517
- readonly canHover: boolean;
518
- /** Live device pixel ratio. */
519
- readonly dpr: number;
520
- /** Multiplier for handle sizes and hit radii. Derived from
521
- * `coarsePointer` unless explicitly overridden. */
522
- readonly targetScale: number;
523
- }
524
- /** The detected half of a profile — everything except the derived scale. */
525
- type DetectedDeviceFacts = Omit<DeviceProfile, 'targetScale'>;
526
-
527
- /**
528
- * Live state read by rule evaluation. Built once per frame on the consuming
529
- * surface — chrome-caps, the affordance pipeline, the dispatcher's
530
- * eligibility filter — and discarded.
531
- *
532
- * Adding a new field is additive: existing rules don't change, new
533
- * selector atoms can read it.
534
- */
535
- interface RuleCtx {
536
- readonly focused: boolean;
537
- readonly selection: readonly NodeId[];
538
- readonly multiActive: boolean;
539
- readonly modifiers: ModifierState;
540
- readonly action: {
541
- readonly kind: string | null;
542
- readonly id: string | null;
543
- };
544
- readonly hover: NodeId | null;
545
- readonly view: View;
546
- /** Active mode id. `'normal'` when no non-default mode is engaged. */
547
- readonly mode: string;
548
- /** Capability tags allowed by the active mode (the union of
549
- * `ModeDefinition.allows` plus implicit tags). The `capability:`
550
- * selector reads this to determine whether a tag is permitted. */
551
- readonly allowedCapabilities: ReadonlySet<CapabilityTag>;
552
- /** Whether the current selection may be resized. `<SceneCanvas>` folds
553
- * `selectTool.resize.resizable` over the selection (true only when every
554
- * selected node is resizable). Read by the `resizable:` selector to gate
555
- * `selection.resize-handles`. Absent (legacy ctx builders) is treated as
556
- * resizable — back-compat: handles show unless a consumer opts a node out. */
557
- readonly selectionResizable?: boolean;
558
- /** Whether a path is currently in anchor-edit mode. Read by the
559
- * `editingAnchors:` selector, which gates the path-edit chrome.
560
- *
561
- * This is deliberately a fact about state, not about permission: the
562
- * anchor overlay and the anchor hit-test must agree, and the thing they
563
- * must agree on is "is there an edited path right now", which no
564
- * capability or mode id answers. A mode that allows `edits-anchors`
565
- * with nothing being edited should draw no anchors. Absent is treated
566
- * as false. */
567
- readonly editingAnchors?: boolean;
568
- /** Device facts — pointer coarseness, hover capability, density.
569
- *
570
- * Absent (legacy ctx builders) is treated as
571
- * {@link DEFAULT_DEVICE_PROFILE}: a fine pointer that can hover, at
572
- * density 1. That is what the kit assumed before this field existed, so
573
- * an absent profile is behavior-preserving by construction. */
574
- readonly device?: DeviceProfile;
575
- }
576
- /** The live state a `RuleCtx` is assembled from — the chrome context plus
577
- * what the active mode allows. */
578
- interface BuildRuleCtxArgs {
579
- focused: boolean;
580
- selection: readonly NodeId[];
581
- multiActive: boolean;
582
- modifiers: ModifierState;
583
- action: {
584
- kind: string | null;
585
- id: string | null;
586
- };
587
- hover: NodeId | null;
588
- view: View;
589
- mode: string;
590
- allowedCapabilities: ReadonlySet<CapabilityTag>;
591
- /** Optional — omitted means "resizable" (handles show). See {@link RuleCtx}. */
592
- selectionResizable?: boolean;
593
- /** Optional — omitted means "no path is being anchor-edited". */
594
- editingAnchors?: boolean;
595
- /** Optional — omitted means {@link DEFAULT_DEVICE_PROFILE}. */
596
- device?: DeviceProfile;
597
- }
598
- /** Gather the current canvas and mode state into the context that action
599
- * eligibility and chrome-visibility rules are evaluated against. */
600
- declare function buildRuleCtx(args: BuildRuleCtxArgs): RuleCtx;
601
-
602
- /**
603
- * A selector is a conjunction of key/value tests. Multiple keys at the same
604
- * level AND together. Each key maps to a selector primitive in the evaluator.
605
- */
606
- interface Selector {
607
- selection?: {
608
- is?: number;
609
- atLeast?: number;
610
- empty?: boolean;
611
- };
612
- mode?: string | {
613
- not: string;
614
- } | {
615
- in: readonly string[];
616
- };
617
- capability?: CapabilityTag | readonly CapabilityTag[] | {
618
- in: readonly CapabilityTag[];
619
- } | {
620
- not: CapabilityTag;
621
- };
622
- gesturing?: boolean;
623
- actionIs?: string;
624
- modifierHeld?: keyof ModifierState;
625
- focused?: boolean;
626
- hovering?: boolean;
627
- hoveringSelected?: boolean;
628
- zoomAtLeast?: number;
629
- /** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
630
- * mode. Absent flag is treated as `false`. */
631
- editingAnchors?: boolean;
632
- /** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
633
- * (resizable), so `{ resizable: true }` passes for legacy ctx builders
634
- * that don't compute it. */
635
- resizable?: boolean;
636
- /** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
637
- * (touch, most styluses). Absent device is treated as `false`. */
638
- coarsePointer?: boolean;
639
- /** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
640
- * device is treated as `true`. */
641
- canHover?: boolean;
642
- }
643
- /**
644
- * Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
645
- * over `Selector` leaves. `when` is the escape hatch — its closure is
646
- * opaque to introspection and should be avoided when a declarative form
647
- * exists. Empty `all` is true; empty `any` is false.
648
- */
649
- type Rule = Selector | {
650
- all: readonly Rule[];
651
- } | {
652
- any: readonly Rule[];
653
- } | {
654
- not: Rule;
655
- } | {
656
- when: (ctx: RuleCtx) => boolean;
657
- };
658
- /** Constant rules. Kept here so they have a single source. */
659
- declare const ALWAYS: Rule;
660
- /** A rule that never passes. `any` of nothing is false. */
661
- declare const NEVER: Rule;
662
- /**
663
- * Render a {@link Rule} as a short human-readable string, for inspectors and
664
- * diagnostics that need to say WHICH rule decided something.
665
- *
666
- * ```
667
- * { capability: 'edits-page' } → capability:edits-page
668
- * { all: [{ mode: 'normal' }, { focused: true }] } → all(mode:normal, focused:true)
669
- * { not: { selection: { empty: true } } } → not(selection:empty=true)
670
- * { when: function hasPath() { … } } → when(hasPath)
671
- * ```
672
- *
673
- * Never throws and always terminates — unlike `JSON.stringify`, which throws
674
- * on a cyclic value and renders the `when` arm as a useless `{}`.
675
- */
676
- declare function describeRule(rule: Rule): string;
677
- /** Evaluate a rule against a context. Pure, and cheap enough to run per
678
- * frame for every piece of chrome. */
679
- declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
680
-
681
- /**
682
- * Live state read by chrome-visibility {@link Condition}s. Backward-compat
683
- * alias for the legacy ChromeCtx shape — kept for consumers that still
684
- * import `ChromeCtx`. Subset of `RuleCtx`: legacy ChromeCtx didn't carry
685
- * mode/capability info. The resolver builds a `RuleCtx` for evaluation;
686
- * surfaces that still operate in `ChromeCtx` shape supply defaults
687
- * (mode='normal', empty allowedCapabilities) at the construction site.
688
- */
689
- interface ChromeCtx {
690
- readonly focused: boolean;
691
- readonly selection: readonly NodeId[];
692
- readonly multiActive: boolean;
693
- readonly modifiers: ModifierState;
694
- readonly action: {
695
- readonly kind: string | null;
696
- readonly id: string | null;
697
- };
698
- readonly hover: NodeId | null;
699
- readonly view: View;
700
- }
701
- /**
702
- * Composable visibility predicate with fluent surface. Carries its underlying
703
- * `Rule` tree at `.rule` so the resolver can introspect / share trees with
704
- * the affordance pipeline and the dispatcher's eligibility filter.
705
- *
706
- * Callable form `cond(ctx)` evaluates the tree against ctx. The fluent
707
- * methods return new Conditions wrapping new trees.
708
- *
709
- * **Chain semantics: strict left-to-right, no precedence.**
710
- * `a.and(b).or(c)` is `(a && b) || c`; `a.or(b).and(c)` is
711
- * `(a || b) && c`. Mix `.and` and `.or` only when you mean
712
- * left-to-right evaluation. For grouped disjunction, name the
713
- * subexpression or use the top-level `or(...)`.
714
- */
715
- interface Condition {
716
- (ctx: RuleCtx): boolean;
717
- readonly rule: Rule;
718
- /** `this && other` */
719
- and(other: Condition | Rule): Condition;
720
- /** `this || other` */
721
- or(other: Condition | Rule): Condition;
722
- /** `this && !other` */
723
- andNot(other: Condition | Rule): Condition;
724
- /** `this || !other` */
725
- orNot(other: Condition | Rule): Condition;
726
- }
727
- /**
728
- * Stable identifier for one user-visible chrome element. The same id
729
- * gates both paint and hit-test — there is no separate
730
- * `affordance.X` / `selection.X` split — so toggling a rule cannot
731
- * leave a visually-present but un-hittable handle (or vice versa).
732
- *
733
- * Naming convention by lifecycle:
734
- *
735
- * - `selection.*` — chrome reflecting committed selection state
736
- * (persists between actions).
737
- * - `action.*` — chrome that only exists during an in-flight
738
- * action (vanishes on commit / cancel).
739
- * - `snap.*` — snapping system chrome (guides, target highlights).
740
- * - `grid`, `debug.*` — environment chrome.
741
- *
742
- * The intersection `(string & {})` keeps the union open so consumers
743
- * can register their own ids; the kit's built-ins are listed
744
- * explicitly for autocomplete.
745
- */
746
- type ChromeId = 'selection.outline' | 'selection.resize-handles' | 'selection.rotation-handle' | 'action.marquee' | 'action.lasso' | 'action.move-ghosts' | 'action.insert-preview' | 'action.commands' | 'snap.guides' | 'snap.targets' | 'grid' | (string & {});
747
- /**
748
- * Consumer override map. Merged on top of the kit's
749
- * `defaultVisibilityRules`; absent keys fall through to defaults,
750
- * absent ids fall through to `always`. Entries may be either fluent
751
- * `Condition` instances OR raw `Rule` trees — the resolver normalizes.
752
- */
753
- type VisibilityRules = Partial<Record<ChromeId, Condition | Rule>>;
754
-
755
- /**
756
- * The kit's built-in shape kinds — one table, every other spelling derived.
757
- *
758
- * Lives in `core/` rather than beside the shape tools because both layers ask
759
- * questions of the same set: the interactions layer decides whether
760
- * `insertAction` can paint a live preview for a kind, and the canvas layer
761
- * decides which shape tools `useBuiltinShapeTools` mounts and what
762
- * `BUNDLE_TOOLS` / `defaultNodeRouting` / `defaultNodeProperties` enumerate.
763
- *
764
- * This module imports nothing on purpose: `useBuiltinShapeTools` imports the
765
- * package barrel, so anything barrel-reachable that needs these lists at
766
- * module-evaluation time must not route through it.
767
- */
768
- /** What the kit knows about one built-in shape kind. */
769
- interface ShapeKindDescriptor {
770
- /** Mounted as a built-in shape tool by `useBuiltinShapeTools`, and so a
771
- * member of `BuiltinShapeToolId` / `KIT_SHAPE_KINDS`. `image` is false:
772
- * `useImageTool` needs a `src` and can't be auto-mounted. */
773
- readonly tool: boolean;
774
- /** `insertAction` emits an `insertPreview` overlay for this kind, and the
775
- * dispatcher overlay layer knows how to draw it. Kinds without one still
776
- * commit; they just have no live drag preview (`pen` and `lasso` don't
777
- * route through `insertAction` at all). */
778
- readonly insertPreview: boolean;
779
- }
780
- /**
781
- * Declaration order is the enumeration order of every derived list —
782
- * `KIT_SHAPE_KINDS`, and through it `defaultNodeRouting` /
783
- * `defaultNodeProperties` / `BUNDLE_TOOLS.exhaustive`.
784
- */
785
- declare const SHAPE_KINDS: {
786
- readonly rect: {
787
- readonly tool: true;
788
- readonly insertPreview: true;
789
- };
790
- readonly ellipse: {
791
- readonly tool: true;
792
- readonly insertPreview: true;
793
- };
794
- readonly line: {
795
- readonly tool: true;
796
- readonly insertPreview: true;
797
- };
798
- readonly polygon: {
799
- readonly tool: true;
800
- readonly insertPreview: true;
801
- };
802
- readonly star: {
803
- readonly tool: true;
804
- readonly insertPreview: true;
805
- };
806
- readonly pen: {
807
- readonly tool: true;
808
- readonly insertPreview: false;
809
- };
810
- readonly pencil: {
811
- readonly tool: true;
812
- readonly insertPreview: true;
813
- };
814
- readonly lasso: {
815
- readonly tool: true;
816
- readonly insertPreview: false;
817
- };
818
- readonly text: {
819
- readonly tool: true;
820
- readonly insertPreview: true;
821
- };
822
- readonly image: {
823
- readonly tool: false;
824
- readonly insertPreview: true;
825
- };
826
- };
827
- /** The keys of a shape-kind table whose descriptor sets `F` to `true`. */
828
- type ShapeKindsWhere<T, F extends keyof ShapeKindDescriptor> = {
829
- [K in keyof T]: T[K] extends Record<F, true> ? K : never;
830
- }[keyof T];
831
- /**
832
- * Built-in shape tool ids handled by `useBuiltinShapeTools`. Each maps to a
833
- * kit tool hook + a default `create` that produces a leaf node compatible
834
- * with `PATH_PAINTER`.
835
- */
836
- type BuiltinShapeToolId = ShapeKindsWhere<typeof SHAPE_KINDS, 'tool'>;
837
- /** The insert kinds the kit's dispatcher overlay layer knows how to render.
838
- * Consumer-defined kinds fall outside it: no live preview, commit unaffected. */
839
- type KitInsertShape = ShapeKindsWhere<typeof SHAPE_KINDS, 'insertPreview'>;
840
- /** Runtime, iterable list of the shape-tool ids in `BuiltinShapeToolId`.
841
- * Surfaced so consumers (e.g. the Bundle Inspector) can enumerate the
842
- * builtin shape kinds without re-encoding the union. */
843
- declare const KIT_SHAPE_KINDS: readonly BuiltinShapeToolId[];
844
-
845
- /** A 2D point in either world or screen coordinates. */
846
- interface Point2 {
847
- x: number;
848
- y: number;
849
- }
850
- /**
851
- * Information about which UI affordance was hit at pointerdown.
852
- *
853
- * Populated by the dispatcher when the `affordanceAt` thunk is provided to
854
- * `useGestureDispatcher`. Tools / action invokers that only fire on a specific
855
- * affordance (e.g. a resize handle) use this field as a guard — if the
856
- * affordance is absent or is the wrong kind, they return `{}` and let other
857
- * bindings handle the drag.
858
- *
859
- * `kind` is a discriminator string:
860
- * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
861
- * `'handle:bottom-right'` — corner resize handles.
862
- * - `'rotate-handle'` — the rotation affordance.
863
- * - `'anchor:N'` — a path anchor at index N.
864
- *
865
- * `fixedPoint` is the world-space point that should remain stationary during
866
- * the gesture. For resize handles this is the opposite (diagonally fixed)
867
- * corner; for rotate it is the pivot.
868
- *
869
- * `targetIds` are the node ids this affordance belongs to.
870
- */
871
- interface AffordanceHit {
872
- /** Discriminator string, e.g. `'handle:bottom-right'`. */
873
- kind: string;
874
- /** Id of whatever produced this hit — a kit affordance's `id`, or the
875
- * registered layer's id. Read only by the dispatcher's dead-claim warning today. */
876
- owner?: string;
877
- /** `'exclusive'` means no binding may act on this point unless its target
878
- * consults the affordance. `'shared'` (the default) competes on scope and
879
- * specificity as bindings always have. */
880
- strength?: 'exclusive' | 'shared';
881
- /** Which gestures an exclusive claim bars. Omitted bars all of them. */
882
- claimedKinds?: readonly ClaimableGesture[];
883
- /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
884
- fixedPoint?: {
885
- x: number;
886
- y: number;
887
- };
888
- /** Which nodes this affordance belongs to. */
889
- targetIds?: string[];
890
- /** Set when `kind` matches `'handle:*'`. Identifies which corner stays
891
- * fixed during a resize so consumers (resizeAction) don't re-parse `kind`.
892
- * Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
893
- * leave this undefined. */
894
- anchor?: ResizeAnchor;
895
- /** Cursor to show while the pointer hovers this affordance (no
896
- * gesture in flight). Consumed by the hover-cursor pump in
897
- * `useGestureDispatcher`; unset = the pump falls through to
898
- * action-cursor prediction, then to the active tool's cursor. */
899
- cursor?: CursorSpec;
900
- /**
901
- * Free-form payload from whatever produced the hit, carried through to the
902
- * matching action untouched.
903
- *
904
- * Kit affordances describe themselves fully in the fields above and leave
905
- * this unset. It exists for affordances the kit doesn't know the shape of —
906
- * a registered layer's own chrome, where the hit-test already resolved
907
- * *which* of its pieces was hit and the action would otherwise have to
908
- * redo that work. `@weasel-js/hud` passes the hit widget here.
909
- */
910
- payload?: unknown;
911
- }
912
- /**
913
- * One accumulated point on a drag trail: world-space position plus whatever
914
- * stylus state the originating `PointerEvent` carried.
915
- *
916
- * The stylus fields are absent for mouse/touch on browsers that don't report
917
- * them, and for synthetic events. Consumers that want pressure-driven output
918
- * (e.g. `Stroke.vertexWidths` from a pencil stroke) read them off the samples
919
- * their `insert` dep receives — see `apps/site/demos/VertexWidthsDemo.tsx`.
920
- */
921
- interface DragSample extends Point2 {
922
- /** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
923
- pressure?: number;
924
- /** Degrees, ±90. Zero for mouse/touch. */
925
- tiltX?: number;
926
- /** Degrees, ±90. Zero for mouse/touch. */
927
- tiltY?: number;
928
- }
929
- /** Per-invocation runtime context the dispatcher hands to an Invoker.
930
- * Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
931
- * populated only for matching gesture kinds. */
932
- interface InvocationCtx {
933
- world: Point2;
934
- screen: Point2;
935
- modifiers: ModifierState;
936
- deps: ActionDeps;
937
- drag?: {
938
- start: Point2;
939
- current: Point2;
940
- delta: Point2;
941
- /**
942
- * Drag delta in client/screen coordinates (CSS pixels from the drag
943
- * origin). Use this — never `delta` — for any action whose effect
944
- * mutates the viewport itself (pan, view-zoom), because world-space
945
- * deltas become self-referential as the view shifts mid-drag.
946
- *
947
- * Populated when the dispatcher received `clientX`/`clientY` on the
948
- * underlying pointer events. Absent for legacy callers that don't
949
- * provide them.
950
- */
951
- screenDelta?: Point2;
952
- affordance?: AffordanceHit;
953
- /**
954
- * Full pointermove history for the current drag, in world space, with
955
- * per-sample stylus state when the browser reported it.
956
- * Accumulated by the dispatcher on every `pointermove` pump event.
957
- * Available only during `onMove` and `onEnd` calls (not on `start`).
958
- * Used by `lassoSelectAction` to build its polygon vertex list and by
959
- * `insertAction`'s pencil kind to carry the freehand stroke.
960
- */
961
- points?: DragSample[];
962
- };
963
- wheel?: {
964
- deltaX: number;
965
- deltaY: number;
966
- deltaZ: number;
967
- };
968
- multiTouch?: {
969
- centroid: Point2;
970
- spread: number;
971
- rotation: number;
972
- /**
973
- * Pinch-zoom geometry. Populated by the dispatcher when a multitouch
974
- * handle is in flight and a pointermove-pump fires.
975
- * `startSpread` is the spread at the moment the gesture began.
976
- * `currentSpread` is the spread at the current frame.
977
- */
978
- pinch?: {
979
- startSpread: number;
980
- currentSpread: number;
981
- centroid: Point2;
982
- };
983
- };
984
- key?: {
985
- key: string;
986
- repeat: boolean;
987
- };
988
- /**
989
- * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
990
- * UI-driven ongoing actions (color picker, opacity slider) so handles can
991
- * read the current value on `start` and updated values on `onMove`. The
992
- * gesture dispatcher does not populate this field; gesture-driven actions
993
- * receive params via `BindingOpts.params` on `start` (the `opts` arg).
994
- */
995
- params?: Record<string, unknown>;
996
- }
997
- /** Per-invocation options the dispatcher reads from a `GestureBinding`'s
998
- * `opts` field and passes to `OngoingInvoker.start`. Today carries
999
- * behaviors; extensible. */
1000
- interface BindingOpts {
1001
- behaviors?: ActionBehavior<unknown, unknown, unknown>[];
1002
- /** Per-binding action parameters. The action's invoker reads
1003
- * these via the second arg to `run` (or via InvocationCtx for ongoing
1004
- * invokers, when needed). Loose typing (Record<string, unknown>) for
1005
- * now; consider per-action typing later via BindingOpts<A>.
1006
- *
1007
- * params may also be a thunk evaluated each time the
1008
- * dispatcher (or invoker) needs the value. Thunks let tools close over
1009
- * refs that mutate during a gesture (e.g. polygon `sides` adjusted
1010
- * mid-drag via ArrowUp). For ongoing invokers that want the latest
1011
- * values at commit, the invoker can re-call the thunk inside `onEnd`
1012
- * via `resolveParams(opts?.params)`. */
1013
- params?: Record<string, unknown> | (() => Record<string, unknown>);
1014
- }
1015
- /** Resolve `BindingOpts.params` to a concrete record (calling the thunk if
1016
- * needed). Returns `undefined` for absent params. */
1017
- declare function resolveParams(params: BindingOpts['params']): Record<string, unknown> | undefined;
1018
- /** Convention-shaped action dependencies bag. Actions declare which
1019
- * contexts they consume; the dispatcher composes them per call.
1020
- * Consumer-side contexts (e.g. ColorContext) plug in by extending. */
1021
- interface ActionDeps {
1022
- selection?: unknown;
1023
- view?: unknown;
1024
- scene?: unknown;
1025
- pointer?: unknown;
1026
- activeTool?: unknown;
1027
- [k: string]: unknown;
1028
- }
1029
- /**
1030
- * Discriminated overlay shape returned by `OngoingHandle.overlay()`.
1031
- * Dispatcher-side chrome surface for in-flight
1032
- * gestures that paint non-ghost visuals. The canvas's
1033
- * `useDispatcherOverlayLayer` walks every in-flight handle, calls
1034
- * `overlay()`, and dispatches on `kind` to draw the appropriate shape.
1035
- *
1036
- * `marquee` mirrors `AreaSelectOverlay`; `lasso` mirrors `LassoSelectOverlay`.
1037
- * `commands` is the generic escape hatch — actions emit arbitrary
1038
- * `DrawCommand[]` for previews the typed variants can't express (insert
1039
- * shape outlines, paste ghosts of synthetic nodes, custom chrome). World-
1040
- * space is the default; the layer wraps in `viewToMat3` so commands track
1041
- * the camera. Set `space: 'screen'` for projections you've already done
1042
- * yourself (rare).
1043
- */
1044
- type OngoingOverlay = {
1045
- kind: 'marquee';
1046
- start: {
1047
- x: number;
1048
- y: number;
1049
- };
1050
- current: {
1051
- x: number;
1052
- y: number;
1053
- };
1054
- shiftHeld: boolean;
1055
- } | {
1056
- kind: 'lasso';
1057
- vertices: ReadonlyArray<{
1058
- x: number;
1059
- y: number;
1060
- }>;
1061
- current: {
1062
- x: number;
1063
- y: number;
1064
- };
1065
- shiftHeld: boolean;
1066
- } | {
1067
- kind: 'commands';
1068
- commands: readonly DrawCommand[];
1069
- /** Coordinate space the commands are authored in. Default `'world'`
1070
- * — the layer wraps them in `viewToMat3(view)` so they track the
1071
- * camera. `'screen'` emits them as-is (CSS pixels). */
1072
- space?: 'world' | 'screen';
1073
- } | {
1074
- /**
1075
- * Live insert-drag preview — dispatched by `insertAction` while the
1076
- * user is dragging out a new shape. Pre-commit there is no scene node
1077
- * to ghost via `previewIds()`/`previewPose()`, so insert paints its
1078
- * preview through the dispatcher overlay layer instead.
1079
- *
1080
- * `shape` is the kit's built-in insert kind. `bounds` is the AABB of
1081
- * the current drag (start/current normalized). `extras` is the
1082
- * per-kind extras the action already collected — the overlay
1083
- * renderer rebuilds the shape using the same path builders the
1084
- * commit factory uses, so the preview matches the eventual node.
1085
- *
1086
- * `extras` is opaque (`unknown`) at the union level; the overlay
1087
- * renderer narrows on `shape` and casts the field shape it expects.
1088
- */
1089
- kind: 'insertPreview';
1090
- shape: KitInsertShape;
1091
- bounds: {
1092
- x: number;
1093
- y: number;
1094
- width: number;
1095
- height: number;
1096
- };
1097
- extras: unknown;
1098
- /** World-space point to paint a small "anchor" dot at. Sells the
1099
- * click point as the drag's anchor — particularly useful for
1100
- * radial shapes (polygon/star) where no vertex sits on the
1101
- * click point, and for any shape in center mode where the dot
1102
- * marks the center the shape grows around. */
1103
- anchorPoint?: {
1104
- x: number;
1105
- y: number;
1106
- };
1107
- };
1108
- /** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
1109
- * `onMove` on subsequent input events of the same gesture and calls
1110
- * `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
1111
- * on pointercancel / blur / escape). */
1112
- interface OngoingHandle {
1113
- /**
1114
- * Optional logical action kind — a stable, human-readable tag the
1115
- * dispatcher exposes via `getActiveAction()` for chrome-visibility
1116
- * rules and any other surface that wants to react to "what action
1117
- * is currently in flight" without inspecting handles directly.
1118
- *
1119
- * Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
1120
- * `'pan'`, `'pinch'`.
1121
- *
1122
- * Distinct from the dispatcher's internal `gestureId` (`pointer-mouse`,
1123
- * `key-held-Space`, etc.) which keys per-pointer state and is not
1124
- * meaningful to consumers.
1125
- *
1126
- * When omitted, the action is "anonymous" — `getActiveAction().kind`
1127
- * reports `null` even though a handle is in flight. This is fine for
1128
- * actions that don't have visible chrome of their own.
1129
- */
1130
- kind?: string;
1131
- onMove?(ctx: InvocationCtx): void;
1132
- onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
1133
- /**
1134
- * Optional preview surface — dispatcher-side ghost overlay.
1135
- *
1136
- * An ongoing-action implementation may populate `previewIds()` +
1137
- * `previewPose(id)` to expose its in-flight preview state for the
1138
- * canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
1139
- * top of the committed scene during the gesture.
1140
- *
1141
- * Returning `null` (or omitting the method entirely) means "no preview
1142
- * this gesture" — the canvas will skip this handle as a source.
1143
- *
1144
- * Semantics mirror the tool-side `Tool.previewIds` / `Tool.previewPose`
1145
- * pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
1146
- * returns the interim pose for one of those ids (shape opaque — the
1147
- * canvas casts to its `TPose` parameter). The preview-ghost layer
1148
- * merges all sources via first-non-null semantics, with tool-side
1149
- * previews taking precedence over dispatcher-side (preserves
1150
- * backwards-compat during the registry-unification migration).
1151
- */
1152
- previewIds?(): Iterable<string> | null;
1153
- previewPose?(id: string): unknown | null;
1154
- /**
1155
- * Subset of `previewIds()` the ghost layer paints at full opacity. The
1156
- * ghost alpha says "this is in flight under the pointer"; a node the
1157
- * gesture merely displaces — a layout sibling reflowing into its
1158
- * destination slot — is not, and reads better settled. Honored at
1159
- * subtree-root granularity.
1160
- */
1161
- previewOpaqueIds?(): Iterable<string> | null;
1162
- /**
1163
- * When `false`, the preview-ghost layer paints the ghost AND the
1164
- * source node stays visible at its committed pose. Defaults to
1165
- * `true` (move/resize/rotate semantics: ghost replaces the source
1166
- * during the gesture). Clone overrides to `false` so the original
1167
- * stays put and the ghost appears at the drag target.
1168
- */
1169
- previewHidesSource?: boolean;
1170
- /**
1171
- * Optional per-id preview *data*. Falls back to the committed
1172
- * `node.data` when null/absent. Use when the gesture mutates
1173
- * `node.data` (e.g. anchor-edit on nodes that store the polygon on
1174
- * `data.path`) rather than (or in addition to) the pose. The preview-
1175
- * ghost layer assembles a synthetic node from `{ ...node, pose:
1176
- * previewPose ?? node.pose, data: previewData ?? node.data }` before
1177
- * calling the scene slot's `drawOne`.
1178
- *
1179
- * Sources compose first-non-null per axis: an action can emit only
1180
- * `previewPose` (translation), only `previewData` (data-only edit),
1181
- * or both (pose + data both change, e.g. anchor drag on a data.path
1182
- * node where the bounds shift).
1183
- */
1184
- previewData?(id: string): unknown | null;
1185
- /**
1186
- * Optional chrome surface — dispatcher-side overlay layer.
1187
- *
1188
- * An ongoing-action implementation may populate `overlay()` to expose a
1189
- * non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
1190
- * `useDispatcherOverlayLayer` to paint while the gesture is in flight.
1191
- * Returning `null` (or omitting the method) means "no overlay this
1192
- * gesture" — the canvas will skip this handle as a chrome source.
1193
- *
1194
- * Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
1195
- * which paints displaced scene-node silhouettes. Marquee and lasso
1196
- * gestures don't displace any node, but still need on-screen feedback.
1197
- */
1198
- overlay?(): OngoingOverlay | null;
1199
- }
1200
- /** Fire-once invocation. Runs to completion synchronously (or fires off an
1201
- * async side-effect; the registry doesn't wait). */
1202
- interface ImmediateInvoker {
1203
- timing: 'immediate';
1204
- /** `params` carries the matched binding's opts.params. Invoked from the
1205
- * command palette, or anywhere else with no per-binding context, `params`
1206
- * is undefined; descriptors should default to a sensible variant. */
1207
- run(deps: ActionDeps, params?: Record<string, unknown>): void;
1208
- }
1209
- /** Phase-machine invocation. `start` opens the phase and returns the handle
1210
- * the dispatcher pumps. */
1211
- interface OngoingInvoker {
1212
- timing: 'ongoing';
1213
- start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
1214
- }
1215
- /** Pluggable invocation strategy for an Action. Future variants
1216
- * (`longPress`, `twoStage`, `modal`) extend this union without touching
1217
- * the `Action` type. */
1218
- type Invoker = ImmediateInvoker | OngoingInvoker;
1219
-
1220
- /**
1221
- * GestureBinding — connects a GestureSpec to an Action id (with per-binding
1222
- * options). Tools own arrays of these on their `bindings` field; ambient
1223
- * gesture-bindings are registered globally.
1224
- *
1225
- * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
1226
- */
1227
-
1228
- /** An interaction: a gesture spec composed with the id of the action it
1229
- * invokes. Tools declare arrays of these; the dispatcher matches an incoming
1230
- * input event against them and runs the winner's action. */
1231
- interface GestureBinding {
1232
- spec: GestureSpec;
1233
- actionId: string;
1234
- opts?: BindingOpts;
1235
- }
1236
-
1237
- /**
1238
- * Pose composition for hierarchical scene graphs.
1239
- *
1240
- * As of the nesting change, `getPose(id)` on adapters returns the
1241
- * **local** pose — relative to the object's direct parent. Anything in the
1242
- * kit that needs to draw, hit-test, snap, or otherwise reason about world
1243
- * coordinates routes through `composeWorldPose`, which walks the parent
1244
- * chain and folds local poses together via a consumer-supplied `compose`.
1245
- *
1246
- * Pose shape is generic, so the compose strategy is too. For the common
1247
- * `{x, y, width, height}` axis-aligned rect, use `composeRectPose` —
1248
- * translation only, child dimensions preserved. Custom pose shapes (paths,
1249
- * matrix transforms) supply their own.
1250
- *
1251
- * The inverse — `rebaseLocalPose` — converts a world-space pose into a
1252
- * local pose under a target parent. Used when reparenting so the visual
1253
- * world position of a child is preserved across the parent change.
1254
- */
1255
- /** Re-exported; the declaration lives in `core/scene/types.ts`, which names
1256
- * it and may not import from features. */
1257
-
1258
- /** Minimal adapter needed by `composeWorldPose` and friends — pose lookup plus parent walk. */
1259
- interface PoseAdapter<TPose> {
1260
- getPose(id: string): TPose;
1261
- getParent(id: string): string | null;
1262
- }
1263
- /** Consumer's pose-composition strategy for hierarchical scenes. `compose`
1264
- * folds a child's pose (in parent's frame) up to the next frame; `decompose`
1265
- * is its inverse. Default is IDENTITY — an absolute-pose scene where every
1266
- * node already stores world coords (parent is grouping-only, no transform). */
1267
- interface PoseComposition<TPose> {
1268
- compose: (parent: TPose, child: TPose) => TPose;
1269
- decompose: (parent: TPose, world: TPose) => TPose;
1270
- }
1271
- /** Default pose-composition strategy: IDENTITY. Both `compose` and
1272
- * `decompose` return the child/world pose unchanged, modeling an
1273
- * absolute-pose scene where every node stores world coords and parents are
1274
- * grouping-only (no transform). With this strategy `composeWorldPose`
1275
- * returns a node's own raw pose and `rebaseLocalPose` is a no-op. */
1276
- declare const IDENTITY_POSE_COMPOSITION: PoseComposition<unknown>;
1277
- /**
1278
- * Walk `id`'s parent chain (root first to id last) and fold local poses into
1279
- * a world pose via `compose`. Returns the world pose for `id`. Cycle-safe:
1280
- * a visited-set guard breaks if the chain ever loops back to itself.
1281
- *
1282
- * `compose(parent, child)` interprets `child` as expressed *in `parent`'s
1283
- * local frame* and returns the equivalent pose in the next frame up. For a
1284
- * standard translation-only rect: `world = { x: p.x + c.x, y: p.y + c.y,
1285
- * width: c.width, height: c.height }`.
1286
- */
1287
- declare function composeWorldPose<TPose>(adapter: PoseAdapter<TPose>, id: string, compose: (parent: TPose, child: TPose) => TPose): TPose;
1288
- /**
1289
- * Default `compose` for axis-aligned rectangles. Adds translation; preserves
1290
- * child width/height. Treat as the canonical compose for any
1291
- * `{x, y, width, height}` pose under a translation-only hierarchy.
1292
- *
1293
- * Generic over the concrete pose type so callers with a wider pose
1294
- * (e.g. `RectPose & { rotation }`) can pass it through; the extra fields
1295
- * are taken from the child unchanged.
1296
- */
1297
- declare function composeRectPose<TPose extends RectPose>(parent: TPose, child: TPose): TPose;
1298
- /**
1299
- * Translate a `RectPose`-shaped pose by `(dx, dy)`. Suitable as the default
1300
- * `translatePose` for `useMove` when poses carry top-level `x`/`y`. Other
1301
- * fields (width/height, plus any extra props on `TPose`) are preserved.
1302
- */
1303
- declare function translateRectPose<TPose extends RectPose>(pose: TPose, dx: number, dy: number): TPose;
1304
- /**
1305
- * Convert `worldPose` into a local pose expressed under `newParentId`'s
1306
- * frame. Used when reparenting so the child's visual world position is
1307
- * preserved despite the change of frame. Inverse of one `compose` step.
1308
- *
1309
- * `decompose(parent, world)` returns the local pose `child` such that
1310
- * `compose(parent, child) === world`. For axis-aligned rects:
1311
- * `child = { ...world, x: world.x - parent.x, y: world.y - parent.y }`.
1312
- *
1313
- * Pass `newParentId === null` for the root frame; the function returns
1314
- * `worldPose` unchanged.
1315
- */
1316
- declare function rebaseLocalPose<TPose>(adapter: PoseAdapter<TPose>, worldPose: TPose, newParentId: string | null, compose: (parent: TPose, child: TPose) => TPose, decompose: (parent: TPose, world: TPose) => TPose): TPose;
1317
- /** Inverse of `composeRectPose` — subtracts parent translation. */
1318
- declare function decomposeRectPose<TPose extends RectPose>(parent: TPose, world: TPose): TPose;
1319
- /**
1320
- * Build a `(id) => world pose | null` callback over a `PoseAdapter`.
1321
- * Convenience for RenderLayers that take a `getPose` callback (selection
1322
- * overlays, debug layers, etc.) so consumers don't hand-write a
1323
- * `composeWorldPose` call per layer.
1324
- *
1325
- * Returns `null` when `adapter.getPose` or `adapter.getParent` throws — the
1326
- * common case is an id removed mid-render between selection state and the
1327
- * next paint. Layers should treat `null` as "skip this id."
1328
- */
1329
- declare function worldPoseLookup<TPose>(adapter: PoseAdapter<TPose>, compose: (parent: TPose, child: TPose) => TPose): (id: string) => TPose | null;
1330
-
1331
- /** Boolean op identifiers — five Pathfinder primaries plus Crop. */
1332
- type BooleanOp = 'union' | 'intersect' | 'subtract' | 'exclude' | 'divide' | 'crop';
1333
- /**
1334
- * z-position descriptor for a path node. `parentId` is the direct parent
1335
- * (or `null` for a top-level node); `index` is the position within that
1336
- * parent's child order. Used by the optional `getZOrder` hook below to
1337
- * reposition the result of a boolean op at the topmost source's slot.
1338
- */
1339
- /** @internal */
1340
- interface BooleanZOrder {
1341
- parentId: string | null;
1342
- index: number;
1343
- }
1344
- /** Adapter the hook and the pure core both consume. */
1345
- interface BooleansAdapter {
1346
- getSelection(): NodeId[];
1347
- getWorldPath(id: NodeId): Path | undefined;
1348
- compareZ(a: NodeId, b: NodeId): number;
1349
- /**
1350
- * Mint a new node from a boolean-op result `Path`. `producedBy` names the
1351
- * op that synthesized it — adapters that store provenance (e.g. for a
1352
- * layer-panel icon) record it; others ignore the arg.
1353
- */
1354
- createPathNode(path: Path, producedBy: BooleanOp): {
1355
- id: string;
1356
- };
1357
- /**
1358
- * Optional: return the full object for an id, used by the delete ops so
1359
- * their `invert` (an insert) can restore the complete object on undo.
1360
- * If omitted, a `{ id }` stub is captured — undo will reinstate the id
1361
- * but consumers reading other fields (path, fill, etc.) will see them as
1362
- * undefined. Mirrors `DeleteAdapter.getNode`; should be provided whenever
1363
- * undo over boolean ops is expected to be lossless.
1364
- */
1365
- getNode?(id: NodeId): {
1366
- id: string;
1367
- } | undefined | null;
1368
- /**
1369
- * Optional: return the parent + child-index of `id` so the result of a
1370
- * boolean op can be placed in the topmost source's z-slot. Adapters that
1371
- * also expose `getChildren`/`setChildOrder` (the `ReorderAdapter`
1372
- * contract) will have the kit emit a `createMoveToIndexOp` after the
1373
- * inserts. Adapters that omit this method get v1 behavior — the result
1374
- * lands wherever the adapter's plain `insertNode` defaults to.
1375
- */
1376
- getZOrder?(id: NodeId): BooleanZOrder | undefined;
1377
- applyOps?(ops: Op[], label?: string): void;
1378
- setSelection?(ids: NodeId[]): void;
1379
- insertNode?(node: {
1380
- id: string;
1381
- }): void;
1382
- removeNode?(id: string): void;
1383
- }
1384
- /** Outcome reported back to callers (lets the hook surface no-op signals). */
1385
- type BooleanOpResult = {
1386
- kind: 'applied';
1387
- resultIds: string[];
1388
- } | {
1389
- kind: 'noop';
1390
- reason: 'no-paths' | 'too-few-for-subtract' | 'empty-result';
1391
- };
1392
- /**
1393
- * Run one Boolean operation over the selected paths and commit the result as a
1394
- * single undoable batch.
1395
- *
1396
- * Operands are ordered back-to-front, which is what makes `subtract` mean
1397
- * "everything in front removed from the backmost shape". Returns without
1398
- * mutating anything when the selection holds no paths, or too few for the
1399
- * requested operation.
1400
- */
1401
- declare function applyBooleanOp(adapter: BooleansAdapter, op: BooleanOp): BooleanOpResult;
1402
-
1403
- /**
1404
- * Selection click policy. `single` always replaces; `multi` toggles when the
1405
- * configured extend key is held, otherwise replaces.
1406
- */
1407
- type SelectionMode = 'single' | 'multi';
1408
- /** Modifier key used to extend the selection in `multi` mode. */
1409
- type SelectionExtendKey = 'shift' | 'meta' | 'ctrl';
1410
- /** API returned by {@link useSelection}. */
1411
- interface SelectionApi {
1412
- /** Current selection. Re-renders trigger when this reference changes. */
1413
- current: readonly NodeId[];
1414
- /** Imperative read for use inside event callbacks (avoids stale closures). */
1415
- get(): NodeId[];
1416
- /** Replace selection. */
1417
- set(ids: NodeId[]): void;
1418
- /** Add id (multi-mode appends; single-mode replaces). */
1419
- add(id: NodeId): void;
1420
- /** Remove id from selection. */
1421
- remove(id: NodeId): void;
1422
- /** Toggle id in/out of selection. */
1423
- toggle(id: NodeId): void;
1424
- /** Clear selection. */
1425
- clear(): void;
1426
- /** True if id is selected. */
1427
- contains(id: NodeId): boolean;
1428
- /**
1429
- * Apply a click to the selection per the configured mode/extend key.
1430
- * - `single`: replaces selection with `[id]`, regardless of modifiers.
1431
- * - `multi`: with the extend key held, toggles `id` in/out of the selection;
1432
- * otherwise replaces with `[id]`.
1433
- */
1434
- applyClick(id: NodeId, modifiers: {
1435
- shift: boolean;
1436
- meta: boolean;
1437
- ctrl: boolean;
1438
- }): void;
1439
- /** Pre-built methods for spreading into an adapter that needs them. */
1440
- adapterMethods: {
1441
- getSelection: () => NodeId[];
1442
- setSelection: (ids: NodeId[]) => void;
1443
- };
1444
- }
1445
- /** Somewhere selection can live outside this hook. `Scene` satisfies it;
1446
- * so does any store with the same three methods. */
1447
- interface SelectionStore {
1448
- getSelection(): readonly NodeId[];
1449
- setSelection(ids: readonly NodeId[]): void;
1450
- subscribe(listener: () => void): () => void;
1451
- }
1452
- /** Options for {@link useSelection}. */
1453
- interface UseSelectionOptions {
1454
- /** Default `'single'`. */
1455
- mode?: SelectionMode;
1456
- /** Default `'shift'`. Ignored in single-mode. */
1457
- extend?: SelectionExtendKey;
1458
- /** Default `[]`. */
1459
- initial?: readonly NodeId[];
1460
- /** Keep the selection on this store rather than in the hook, so every
1461
- * consumer of the same scene shares one selection and undo / redo can
1462
- * restore it. `initial` then only seeds a store that has none yet.
1463
- * Omit it and the hook owns a selection nobody else sees. */
1464
- scene?: SelectionStore;
1465
- /** When `true`, every mutator (`set`/`add`/`remove`/`toggle`/`clear`/
1466
- * `applyClick`) is a no-op — selection stays at whatever `initial`
1467
- * pinned it to. Useful for demos that exist to showcase a single
1468
- * pre-selected node (e.g. the bezier-edit curve) and don't want a
1469
- * stray click to deselect. */
1470
- lock?: boolean;
1471
- }
1472
- /**
1473
- * Default implementation of the `getSelection` / `setSelection` adapter
1474
- * contract every action hook (delete, duplicate, nudge, group, ...) requires.
1475
- *
1476
- * Owns selection state, exposes a click-policy helper (single vs multi with
1477
- * an extend key), and pre-builds the two adapter methods consumers otherwise
1478
- * hand-roll in every demo:
1479
- *
1480
- * ```tsx
1481
- * const selection = useSelection({ mode: 'multi' });
1482
- * const adapter = { ...arrayAdapter({...}), ...selection.adapterMethods };
1483
- * ```
1484
- */
1485
- declare function useSelection(opts?: UseSelectionOptions): SelectionApi;
1486
-
1487
- /** Context handed to every content handler for one ingest event. */
1488
- interface IngestCtx {
1489
- /** World-space arrival point (drop / pointed imperative ingest); `null`
1490
- * for paste and point-less calls — handlers pick their own policy
1491
- * (the kit image handler centers on the viewport). */
1492
- point: {
1493
- x: number;
1494
- y: number;
1495
- } | null;
1496
- /** Visible canvas area in world coordinates. */
1497
- viewportWorldRect(): {
1498
- x: number;
1499
- y: number;
1500
- width: number;
1501
- height: number;
1502
- };
1503
- /** The kit insert dep — id/layer/undoable-op supplied; the canonical way
1504
- * for a handler to mint a node (`insert.commit(bounds, { kind, ... })`). */
1505
- insert: InsertDep;
1506
- /** Raw op commit for handlers that build their own ops. */
1507
- applyOps(ops: Op[], label?: string): void;
1508
- scene: Scene<unknown, string, unknown>;
1509
- selection: SelectionApi;
1510
- /** Consumer file→src resolver (SceneCanvas `ingestion.resolveSrc`).
1511
- * When absent, the kit image handler embeds as a `data:` URI. */
1512
- resolveSrc?: (file: File) => Promise<string>;
1513
- /** Kit SVG-handler options (SceneCanvas `ingestion.svg`) — e.g.
1514
- * `{ unpack: unpackSvgFiles }` (from `@weasel-js/svg`) to parse SVG files
1515
- * into scene nodes. */
1516
- svg?: SvgIngestOptions;
1517
- /** Clipboard-paste seam — present when the hosting `SceneCanvas` supplied
1518
- * an adapter with `commitPaste`. `reviver` comes from
1519
- * `SceneCanvasProps.ingestion.clipboard`. Absent ⇒ the kit weasel-JSON
1520
- * handler declines inert (dwarn, nothing ingested) — its matched items
1521
- * were already consumed at match time, so they do NOT fall through;
1522
- * only match-level misses flow on to other handlers. */
1523
- clipboard?: ClipboardIngestCtx;
1524
- /** Set to `true` by the kit weasel-JSON handler when it successfully
1525
- * pastes a payload in this event. The `ctx` object is shared across all
1526
- * handlers in one `runIngest` call, and higher-priority handlers' `handle`
1527
- * bodies run (synchronously) before lower ones — so `kit:svg`'s
1528
- * `text/plain` SVG fallback reads this to decline the SVG flavor of a copy
1529
- * whose canonical weasel-JSON flavor already ingested (avoids a
1530
- * double-paste when both flavors ride one clipboard event). */
1531
- consumedWeaselPayload?: boolean;
1532
- /** Full action-deps bag, for consumer handlers that need more. */
1533
- deps: ActionDeps;
1534
- }
1535
- /** A handler for content arriving by paste, drop or file picker. Handlers are
1536
- * matched by MIME glob or predicate and run highest-priority first; the kit's
1537
- * own register at a low priority so a consumer's handler wins by default. */
1538
- interface ContentHandlerEntry {
1539
- /** Stable identifier — used for unregistration and debugging
1540
- * (`'kit:image'`, `'app:csv'`). */
1541
- id: string;
1542
- /** MIME glob(s) (`'image/*'`, `'text/csv'`) or an item predicate. */
1543
- match: string | string[] | ((item: IngestItem) => boolean);
1544
- /** Higher runs earlier. Kit defaults register at -100 so any consumer
1545
- * handler (default 0) beats them. */
1546
- priority?: number;
1547
- handle(items: IngestItem[], ctx: IngestCtx): void | Promise<void>;
1548
- }
1549
- /** Register a content handler. Returns a disposer that removes it. */
1550
- declare function registerContentHandler(entry: ContentHandlerEntry): () => void;
1551
-
1552
- /** Per-axis limits on a view's position. Any side may be left open. */
1553
- interface PanBounds {
1554
- minX?: number;
1555
- maxX?: number;
1556
- minY?: number;
1557
- maxY?: number;
1558
- }
1559
- /** Momentum settings for a pan: how quickly a flung view slows, when it stops,
1560
- * and what happens at the pan limits. The `DecayLoopConfig` fields a caller
1561
- * chooses up front, without the per-gesture `velocity` / `onTick`. */
1562
- interface InertiaConfig {
1563
- friction?: number;
1564
- minSpeed?: number;
1565
- /** What to do when inertial pan reaches `bounds`. Default: no clamping. */
1566
- boundary?: 'stop' | 'bounce' | 'spring';
1567
- /** View-coordinate limits for boundary clamping. Requires `boundary` to take effect. */
1568
- bounds?: PanBounds;
1569
- }
1570
- /** How a decay should run: its starting velocity, how fast it slows, and what
1571
- * happens if it reaches the pan limits. */
1572
- interface DecayLoopConfig {
1573
- velocity: {
1574
- vx: number;
1575
- vy: number;
1576
- };
1577
- friction?: number;
1578
- minSpeed?: number;
1579
- /** Bounds for boundary clamping. Requires `boundary` to take effect. */
1580
- viewBounds?: PanBounds;
1581
- /**
1582
- * What to do when the accumulated position hits `viewBounds`. Default: no clamping.
1583
- * - `'stop'`: clamp at boundary, kill velocity component.
1584
- * - `'bounce'`: linear reflection — flip velocity sign, magnitude preserved.
1585
- * - `'spring'`: damped reflection — flip velocity sign and shrink magnitude
1586
- * by `SPRING_DAMPING` per bounce so the motion settles naturally.
1587
- */
1588
- boundary?: 'stop' | 'bounce' | 'spring';
1589
- /** Starting position for internal boundary tracking. Required when `viewBounds` is set. */
1590
- initialPosition?: {
1591
- x: number;
1592
- y: number;
1593
- };
1594
- onTick: (dx: number, dy: number) => void;
1595
- onEnd?: () => void;
1596
- }
1597
- /** A rAF loop that coasts a value to a stop under friction, reporting the
1598
- * per-frame delta. What turns a released pan drag into momentum scrolling. */
1599
- declare function useDecayLoop(): {
1600
- start: (config: DecayLoopConfig) => void;
1601
- cancel: () => void;
1602
- };
1603
-
1604
- /** Which of a node's two per-anchor color arrays an override applies to. */
1605
- type VertexColorChannel = 'fill' | 'stroke';
1606
- /** Function-form override: receives the consumer-supplied base color
1607
- * array and the current animation timestamp (ms, from the animator's
1608
- * clock). Returns a flat RGBA float array (values in 0..1, matching
1609
- * the renderer's `stroke.vertexColors` / `PathDrawCommand.vertexColors`
1610
- * color space) of the same length as `base`. */
1611
- type ColorOverrideFn = (base: readonly number[], tMs: number) => number[];
1612
- /** Either a static per-anchor RGBA float array (0..1) or a function-form
1613
- * override (see {@link ColorOverrideFn}). */
1614
- type ColorOverride = readonly number[] | ColorOverrideFn;
1615
- /** Per-node, per-channel store of color overrides consulted by `createPathLayer`
1616
- * before falling back to the consumer's `getVertexColors` / `getStrokeVertexColors`
1617
- * accessor. Attached to `useAnimator` as `animator.colorOverrides`. */
1618
- declare class ColorOverrideRegistry {
1619
- private readonly map;
1620
- private _version;
1621
- set(id: string, channel: VertexColorChannel, override: ColorOverride): void;
1622
- clear(id: string, channel: VertexColorChannel): void;
1623
- clearAll(): void;
1624
- get(id: string, channel: VertexColorChannel): ColorOverride | undefined;
1625
- version(): number;
1626
- }
1627
-
1628
- /** One keyframe. `easing` shapes the approach INTO this key from the previous
1629
- * one, so the first key's easing is never consulted. */
1630
- interface Keyframe<T> {
1631
- /** Time within the track's timeline, in ms. */
1632
- t: number;
1633
- value: T;
1634
- /** A function, the name of a built-in, or cubic-bezier control points. */
1635
- easing?: EasingSpec;
1636
- }
1637
- /** A track sampled as a pure function of the playhead. Scrubbing one is free
1638
- * and order-independent. */
1639
- interface SampledTrack<T> {
1640
- kind: 'sampled';
1641
- label?: string;
1642
- /** Sorted ascending by `t`. `sampleTrack` assumes this and does not sort. */
1643
- keys: Keyframe<T>[];
1644
- /** Required when T is not `number`; defaults to numeric lerp otherwise. */
1645
- interpolate?: Interpolate<T>;
1646
- /** Built once per segment and cached. Takes precedence over `interpolate`. */
1647
- interpolator?: InterpolatorFactory<T>;
1648
- onTick: (value: T) => void;
1649
- }
1650
- /** A track of edge crossings. Fires only when the playhead advances forward
1651
- * under playback — never on `seek`. */
1652
- interface EventTrack {
1653
- kind: 'event';
1654
- label?: string;
1655
- /** Sorted ascending by `t`. `fire` is told how far behind the frame its edge
1656
- * was crossed, in ms — never negative, and measured against `duration` on
1657
- * the loop seam, where the outgoing lap's tail fires after the wrap. */
1658
- events: {
1659
- t: number;
1660
- fire: (lateBy: number) => void;
1661
- }[];
1662
- }
1663
- /** A nested timeline, evaluated at `playhead - at`. Children are NOT registered
1664
- * with the animator separately; the parent evaluates them. */
1665
- interface TimelineTrack {
1666
- kind: 'timeline';
1667
- label?: string;
1668
- at: number;
1669
- timeline: NestedTimeline;
1670
- }
1671
- type Track = SampledTrack<any> | EventTrack | TimelineTrack;
1672
- /** What a child timeline may declare. The parent owns playback, so `loop`,
1673
- * `autoplay`, `onDone` and `cancelKey` have no meaning below the root. */
1674
- interface NestedTimeline {
1675
- tracks: Track[];
1676
- /** Defaults to the largest end time across `tracks`. */
1677
- duration?: number;
1678
- }
1679
- interface TimelineOptions extends NestedTimeline {
1680
- /** `true` loops forever, `n` loops n additional times. Default false. */
1681
- loop?: boolean | number;
1682
- /** Default true. When false the timeline registers but holds at t=0 until resumed. */
1683
- autoplay?: boolean;
1684
- onDone?: () => void;
1685
- cancelKey?: string;
1686
- }
1687
- interface TimelineHandle extends AnimationHandle {
1688
- /** Move the playhead. Never fires event tracks, at any depth. */
1689
- seek(t: number): void;
1690
- /** Change the loop policy. `true` loops forever, `n` allows n more laps,
1691
- * `false` stops at `duration`. Sets policy only — a timeline already parked
1692
- * at `duration` does not restart, because `rearm` declines to revive one.
1693
- * Rewind it with `seek(0)` and `resume()` to play it again. */
1694
- setLoop(loop: boolean | number): void;
1695
- /** The loop policy as it now stands: `true` endless, `false` stopping at
1696
- * `duration`, `n` for n laps still allowed. A finite count falls as laps
1697
- * are consumed, matching what `setLoop` takes. */
1698
- loop(): boolean | number;
1699
- /** Current playhead in ms. */
1700
- time(): number;
1701
- duration(): number;
1702
- tracks(): readonly Track[];
1703
- /** Run `fn`, then recompute duration, drop cached interpolators, and notify.
1704
- * Every mutation must go through this — an edited keyframe otherwise keeps
1705
- * interpolating toward its old value with no visible error. */
1706
- edit(fn: () => void): void;
1707
- /** Notified after each `edit`. Returns an unsubscribe. */
1708
- subscribe(cb: () => void): () => void;
1709
- }
1710
-
1711
- /** No easing: constant rate from start to finish. */
1712
- declare const linear: EasingFn;
1713
- /** Accelerates from a standstill, gently. */
1714
- declare const easeInQuad: EasingFn;
1715
- /** Decelerates to a stop, gently. The safe default for UI motion. */
1716
- declare const easeOutQuad: EasingFn;
1717
- /** Accelerates then decelerates, gently. */
1718
- declare const easeInOutQuad: EasingFn;
1719
- /** Accelerates from a standstill, moderately. */
1720
- declare const easeInCubic: EasingFn;
1721
- /** Decelerates to a stop, moderately. */
1722
- declare const easeOutCubic: EasingFn;
1723
- /** Accelerates then decelerates, moderately. */
1724
- declare const easeInOutCubic: EasingFn;
1725
- /** Accelerates from a standstill, sharply. */
1726
- declare const easeInQuart: EasingFn;
1727
- /** Decelerates to a stop, sharply. */
1728
- declare const easeOutQuart: EasingFn;
1729
- /** Accelerates then decelerates, sharply. */
1730
- declare const easeInOutQuart: EasingFn;
1731
- /** Accelerates from a standstill, very sharply. */
1732
- declare const easeInQuint: EasingFn;
1733
- /** Decelerates to a stop, very sharply. */
1734
- declare const easeOutQuint: EasingFn;
1735
- /** Accelerates then decelerates, very sharply. */
1736
- declare const easeInOutQuint: EasingFn;
1737
- /** Accelerates from a standstill along a sine curve — the mildest
1738
- * acceleration of the built-ins. */
1739
- declare const easeInSine: EasingFn;
1740
- /** Decelerates to a stop along a sine curve — the mildest
1741
- * deceleration of the built-ins. */
1742
- declare const easeOutSine: EasingFn;
1743
- /** Accelerates then decelerates along a sine curve. */
1744
- declare const easeInOutSine: EasingFn;
1745
- /** Accelerates exponentially: barely moves at first, then rushes. */
1746
- declare const easeInExpo: EasingFn;
1747
- /** Decelerates exponentially: leaps away, then creeps in. */
1748
- declare const easeOutExpo: EasingFn;
1749
- /** Exponential at both ends — a very fast middle between two
1750
- * near-still extremes. */
1751
- declare const easeInOutExpo: EasingFn;
1752
- /** Accelerates along a circular arc: slow start, abrupt arrival. */
1753
- declare const easeInCirc: EasingFn;
1754
- /** Decelerates along a circular arc: abrupt start, slow arrival. */
1755
- declare const easeOutCirc: EasingFn;
1756
- /** Circular arcs at both ends. */
1757
- declare const easeInOutCirc: EasingFn;
1758
- /** Pulls back past the start before moving forward. Overshoots below 0. */
1759
- declare const easeInBack: EasingFn;
1760
- /** Overshoots the target, then settles back onto it. Exceeds 1. */
1761
- declare const easeOutBack: EasingFn;
1762
- /** Overshoots at both ends. Leaves the 0–1 range on each side. */
1763
- declare const easeInOutBack: EasingFn;
1764
- /** Oscillates around the start with growing amplitude, then snaps away. */
1765
- declare const easeInElastic: EasingFn;
1766
- /** Springs past the target and wobbles into it. Exceeds 1. */
1767
- declare const easeOutElastic: EasingFn;
1768
- /** Wobbles at both ends. Leaves the 0–1 range on each side. */
1769
- declare const easeInOutElastic: EasingFn;
1770
- /** Lands on the target and bounces, in hops of decreasing height. */
1771
- declare const easeOutBounce: EasingFn;
1772
- /** Bounces up to the start before departing — `easeOutBounce` reversed. */
1773
- declare const easeInBounce: EasingFn;
1774
- /** Bounces at both ends. */
1775
- declare const easeInOutBounce: EasingFn;
1776
- /** Alias for `easeInQuad`, kept for call sites that predate the
1777
- * named-curve library. */
1778
- declare const easeIn: EasingFn;
1779
- /** Alias for `easeOutQuad`, kept for call sites that predate the
1780
- * named-curve library. */
1781
- declare const easeOut: EasingFn;
1782
- /** Alias for `easeInOutQuad`, kept for call sites that predate the
1783
- * named-curve library. */
1784
- declare const easeInOut: EasingFn;
1785
- /** All easings in one bag — useful for demos / pickers. */
1786
- declare const EASINGS: {
1787
- readonly linear: EasingFn;
1788
- readonly easeInQuad: EasingFn;
1789
- readonly easeOutQuad: EasingFn;
1790
- readonly easeInOutQuad: EasingFn;
1791
- readonly easeInCubic: EasingFn;
1792
- readonly easeOutCubic: EasingFn;
1793
- readonly easeInOutCubic: EasingFn;
1794
- readonly easeInQuart: EasingFn;
1795
- readonly easeOutQuart: EasingFn;
1796
- readonly easeInOutQuart: EasingFn;
1797
- readonly easeInQuint: EasingFn;
1798
- readonly easeOutQuint: EasingFn;
1799
- readonly easeInOutQuint: EasingFn;
1800
- readonly easeInSine: EasingFn;
1801
- readonly easeOutSine: EasingFn;
1802
- readonly easeInOutSine: EasingFn;
1803
- readonly easeInExpo: EasingFn;
1804
- readonly easeOutExpo: EasingFn;
1805
- readonly easeInOutExpo: EasingFn;
1806
- readonly easeInCirc: EasingFn;
1807
- readonly easeOutCirc: EasingFn;
1808
- readonly easeInOutCirc: EasingFn;
1809
- readonly easeInBack: EasingFn;
1810
- readonly easeOutBack: EasingFn;
1811
- readonly easeInOutBack: EasingFn;
1812
- readonly easeInElastic: EasingFn;
1813
- readonly easeOutElastic: EasingFn;
1814
- readonly easeInOutElastic: EasingFn;
1815
- readonly easeInBounce: EasingFn;
1816
- readonly easeOutBounce: EasingFn;
1817
- readonly easeInOutBounce: EasingFn;
1818
- };
1819
- /** The name of one of the built-in easing curves. */
1820
- type EasingName = keyof typeof EASINGS;
1821
- /** Named spring tunings, from softest to firmest. Springs settle on a target
1822
- * rather than running for a fixed duration, so these are an alternative to an
1823
- * easing curve, not a modifier on one. */
1824
- declare const SPRING_PRESETS: Record<SpringPresetName, SpringPreset>;
1825
-
1826
- /** Cubic-bezier control points, CSS `cubic-bezier()` order. The curve's two
1827
- * endpoints are implicit at (0,0) and (1,1). */
1828
- interface BezierEasing {
1829
- /** `readonly` so an `as const` preset is assignable; nothing ever writes it. */
1830
- bezier: readonly [number, number, number, number];
1831
- }
1832
- /** An easing curve as a value: a function, the name of a built-in, or control
1833
- * points. Anything an editor has to name, show or serialize must not be a bare
1834
- * function, which is why the union exists. */
1835
- type EasingSpec = EasingFn | EasingName | BezierEasing;
1836
- /** Build the easing curve for four cubic-bezier control points. `x1`/`x2` are
1837
- * clamped to [0,1] — CSS `cubic-bezier()`'s constraint for a monotone x(t),
1838
- * which both `solveForX` root-finders assume. `y1`/`y2` are unclamped: an
1839
- * overshoot easing (back, elastic) needs them outside 0..1. */
1840
- declare function cubicBezierEasing(x1: number, y1: number, x2: number, y2: number): EasingFn;
1841
- /** Resolve a spec to the function that shapes progress. `undefined` is linear. */
1842
- declare function resolveEasing(spec?: EasingSpec): EasingFn;
1843
-
1844
- /** An easing curve: maps normalized progress `t ∈ [0, 1]` to eased progress.
1845
- * Curves may leave the 0–1 range in the middle (back, elastic) but should
1846
- * pass through 0 at 0 and 1 at 1. */
1847
- type EasingFn = (t: number) => number;
1848
-
1849
- /** Blends two `T` values at eased progress `t`. Called once per frame; see
1850
- * {@link InterpolatorFactory} when the blend has setup worth hoisting. */
1851
- type Interpolate<T> = (from: T, to: T, t: number) => T;
1852
- /** Factory interpolator: built ONCE at tween start with (from, to), the returned
1853
- * function is called with `t ∈ [0, 1]` each frame. Use for interpolators with
1854
- * expensive setup (color-space conversion, path-string parsing) — d3-interpolate's
1855
- * shape exactly. For cheap interpolations the per-tick `Interpolate<T>` form is
1856
- * fine; this is the escape hatch when setup-per-tick is wasteful. */
1857
- type InterpolatorFactory<T> = (from: T, to: T) => (t: number) => T;
1858
- /** A spring's physical parameters. Higher stiffness settles faster, higher
1859
- * damping overshoots less, higher mass makes both sluggish. */
1860
- interface SpringPreset {
1861
- stiffness: number;
1862
- damping: number;
1863
- mass: number;
1864
- }
1865
- /** One of the tunings in `SPRING_PRESETS`. */
1866
- type SpringPresetName = 'gentle' | 'wobbly' | 'stiff' | 'slow';
1867
- /** A running animation. Cancel it, or bend its time — pausing and time-scaling
1868
- * act on this animation's own virtual clock, independent of the animator's. */
1869
- interface AnimationHandle {
1870
- /** Monotonic id assigned by the animator. */
1871
- id: number;
1872
- /** Cancel this animation. Idempotent — no-op once already finished/canceled. */
1873
- cancel(): void;
1874
- /** Freeze this animation's virtual clock. Idempotent. */
1875
- pause(): void;
1876
- /** Resume this animation's virtual clock. Idempotent. */
1877
- resume(): void;
1878
- /** Multiply this animation's virtual-clock rate by `scale`. 1 = normal. */
1879
- setTimeScale(scale: number): void;
1880
- /** This animation's own time scale. 1 once it has finished, since the
1881
- * animator no longer holds an entry to read. */
1882
- timeScale(): number;
1883
- /** True iff this handle is currently paused. */
1884
- isPaused(): boolean;
1885
- }
1886
- /** A duration-based animation from `from` to `to` over `ms`, shaped by an
1887
- * easing curve. Reach for a spring instead when the motion should respond to
1888
- * where the value already is rather than restart from a fixed duration. */
1889
- interface TweenOptions<T> {
1890
- from: T;
1891
- to: T;
1892
- ms: number;
1893
- easing?: EasingSpec;
1894
- /** Required when T is not `number`. For T = number, defaults to linear numeric lerp.
1895
- * Called per-tick with `(from, to, t)`. For interpolators with expensive setup,
1896
- * prefer `interpolator` which is built once at tween start. */
1897
- interpolate?: Interpolate<T>;
1898
- /** Factory interpolator built once at tween start. Takes precedence over
1899
- * `interpolate` when both are provided. Use this for d3-interpolate or any
1900
- * `(from, to) => (t) => v` shape. */
1901
- interpolator?: InterpolatorFactory<T>;
1902
- onTick: (value: T) => void;
1903
- onDone?: () => void;
1904
- /** Any new animation passed the same cancelKey cancels the prior one in flight. */
1905
- cancelKey?: string;
1906
- }
1907
- /** A spring animation: runs until the value settles on `to` rather than for a
1908
- * set duration, so it absorbs an initial velocity naturally. Non-numeric `T`
1909
- * needs the four vector helpers. */
1910
- interface SpringOptions<T> {
1911
- from: T;
1912
- to: T;
1913
- /** Initial velocity in T-units per second. Default: zero (T-shape-aware). */
1914
- velocity?: T;
1915
- preset?: SpringPresetName;
1916
- stiffness?: number;
1917
- damping?: number;
1918
- mass?: number;
1919
- interpolate?: Interpolate<T>;
1920
- /** Vector helpers — required for non-numeric T. */
1921
- add?: (a: T, b: T) => T;
1922
- subtract?: (a: T, b: T) => T;
1923
- scale?: (v: T, k: number) => T;
1924
- magnitude?: (v: T) => number;
1925
- /** Velocity magnitude below which the spring is considered settled. Default 0.01. */
1926
- restThreshold?: number;
1927
- onTick: (value: T) => void;
1928
- onDone?: () => void;
1929
- cancelKey?: string;
1930
- }
1931
- /** Spring and decay as one animation. With a `to`, a spring pulls toward it;
1932
- * with `to: null`, the value coasts on its velocity. Either can become the
1933
- * other mid-flight through the handle. */
1934
- interface PhysicsOptions<T> {
1935
- from: T;
1936
- /** Target. `null` ⇒ no spring force (decay-mode). */
1937
- to?: T | null;
1938
- /** Initial velocity in T-units per second. */
1939
- velocity?: T;
1940
- preset?: SpringPresetName;
1941
- stiffness?: number;
1942
- damping?: number;
1943
- mass?: number;
1944
- restThreshold?: number;
1945
- /** Vector helpers — required for non-numeric T. */
1946
- add?: (a: T, b: T) => T;
1947
- subtract?: (a: T, b: T) => T;
1948
- scale?: (v: T, k: number) => T;
1949
- magnitude?: (v: T) => number;
1950
- onTick: (value: T) => void;
1951
- onDone?: () => void;
1952
- cancelKey?: string;
1953
- }
1954
- /** An `AnimationHandle` that can also be steered while it runs — the point of
1955
- * the physics primitive. */
1956
- interface PhysicsHandle<T = unknown> extends AnimationHandle {
1957
- /** Retarget mid-flight. `null` ⇒ switch to decay-mode (no spring force). */
1958
- setTarget(to: T | null): void;
1959
- /** Replace the current velocity in T-units per second. */
1960
- setVelocity(v: T): void;
1961
- }
1962
- /** Momentum: coast from `from` at `velocity`, slowing by `friction` each
1963
- * second until below `threshold`. What a flick-to-pan leaves behind. */
1964
- interface DecayOptions<T> {
1965
- from: T;
1966
- velocity: T;
1967
- /** Per-second velocity multiplier in (0, 1). Default 0.95. */
1968
- friction?: number;
1969
- /** Velocity magnitude below which decay stops. Default 0.5. */
1970
- threshold?: number;
1971
- add: (a: T, b: T) => T;
1972
- scale: (v: T, k: number) => T;
1973
- magnitude: (v: T) => number;
1974
- onTick: (value: T) => void;
1975
- onDone?: () => void;
1976
- cancelKey?: string;
1977
- }
1978
- /** Options for `useAnimator`. Everything here is an injection seam for tests;
1979
- * the defaults are the real clock, rAF, and `setTimeout`. */
1980
- interface UseAnimatorOptions {
1981
- /** Optional clock injection for tests. Returns ms since some epoch. */
1982
- now?: () => number;
1983
- /** Optional rAF / cAF injection for tests. Defaults to window.requestAnimationFrame. */
1984
- requestFrame?: (cb: (t: number) => void) => number;
1985
- cancelFrame?: (handle: number) => void;
1986
- /** Optional `setTimeout` injection used by `stagger` for per-item delays.
1987
- * Defaults to the global `setTimeout`. Tests inject a virtual scheduler. */
1988
- setTimer?: (cb: () => void, ms: number) => unknown;
1989
- /** Companion to `setTimer`. Defaults to global `clearTimeout`. */
1990
- clearTimer?: (handle: unknown) => void;
1991
- }
1992
- /**
1993
- * Owns every running animation on a canvas and drives them from one rAF loop.
1994
- * Beyond the primitives (`tween`, `spring`, `decay`, `physics`) it offers
1995
- * composition — `loop`, `stagger` — and bulk control by handle, by cancel-key,
1996
- * or over everything at once.
1997
- *
1998
- * An animator does not know about the scene: animations report values through
1999
- * `onTick` and the caller decides what to do with them.
2000
- */
2001
- interface Animator {
2002
- tween<T>(opts: TweenOptions<T>): AnimationHandle;
2003
- spring<T>(opts: SpringOptions<T>): AnimationHandle;
2004
- decay<T>(opts: DecayOptions<T>): AnimationHandle;
2005
- /** Unified spring/decay primitive. With `to` set, behaves as a spring;
2006
- * with `to: null`, behaves as a velocity-driven decay. Supports
2007
- * mid-flight retargeting via the returned handle's `setTarget`. */
2008
- physics<T>(opts: PhysicsOptions<T>): PhysicsHandle<T>;
2009
- /** Cancel a specific animation by handle. Pose stays at current value (no jump). */
2010
- cancel(handle: AnimationHandle): void;
2011
- /** Cancel every animation currently active under `key`. */
2012
- cancelKey(key: string): void;
2013
- /** Cancel everything. Useful from a destructor or "reset scene" path. */
2014
- cancelAll(): void;
2015
- /** True iff at least one animation is active. With `key`, scoped to that cancelKey. */
2016
- isActive(key?: string): boolean;
2017
- /**
2018
- * True while the animator is currently executing an animation tick. Useful
2019
- * for adapter wrappers (e.g. `animateOnSetPose`) that need to detect
2020
- * "this `setPose` was called from inside another animation's onTick"
2021
- * (momentum decay, in-flight tween, spring) and avoid recursively
2022
- * scheduling a new wrap-animation that would fight the caller.
2023
- */
2024
- isTicking(): boolean;
2025
- /** Freeze every animation managed by this animator. */
2026
- pause(): void;
2027
- /** Resume every animation managed by this animator. */
2028
- resume(): void;
2029
- /** True iff the animator is currently globally paused. */
2030
- isPaused(): boolean;
2031
- /** Multiply every animation's virtual-clock rate by `scale`. 1 = normal. */
2032
- setTimeScale(scale: number): void;
2033
- /** The global time scale. Per-animation scales multiply on top of it. */
2034
- timeScale(): number;
2035
- /** Freeze every animation whose `cancelKey` matches. */
2036
- pauseKey(key: string): void;
2037
- /** Resume every animation whose `cancelKey` matches. */
2038
- resumeKey(key: string): void;
2039
- /** Set per-animation timeScale for every animation whose `cancelKey` matches. */
2040
- setTimeScaleByKey(key: string, scale: number): void;
2041
- /**
2042
- * Loop primitive: repeatedly invoke `factory` to produce a child animation.
2043
- * The factory must wire its returned handle's `onDone` to call `next` so
2044
- * the loop advances. Returns a handle whose pause/resume/setTimeScale/cancel
2045
- * delegate to the current in-flight child (and prevent future iterations
2046
- * on cancel).
2047
- *
2048
- * The loop is registered with the animator under a supervisor entry so
2049
- * `animator.cancel(handle)`, `animator.cancelKey(opts.cancelKey)`, and
2050
- * `animator.isActive(opts.cancelKey)` all work for it.
2051
- */
2052
- loop(factory: LoopFactory, opts?: LoopOptions): AnimationHandle;
2053
- /** Sugar over `loop` for the common case of looping a tween between two
2054
- * values with optional direction handling (`restart` | `reverse` |
2055
- * `alternate`). Registered with the animator like `loop`. */
2056
- tweenLoop<T>(opts: TweenLoopOptions<T>): AnimationHandle;
2057
- /**
2058
- * Stagger primitive: schedule a per-item animation, offset by `delay` ms
2059
- * per index (or a custom function of the index). Two forms:
2060
- * - Factory form: pass `factory` directly, returns a composite
2061
- * `AnimationHandle`.
2062
- * - Builder form: omit `factory`, get a `StaggerBuilder` for fluent
2063
- * `.each` / `.tween` / `.springPose` calls.
2064
- *
2065
- * The composite handle's `cancel` cancels pending timers AND in-flight
2066
- * children. `pause` / `resume` / `setTimeScale` propagate to in-flight
2067
- * children; `pause`/`resume` also freeze and thaw pending per-item timers
2068
- * (the remaining time before each pending fire is preserved across the
2069
- * pause).
2070
- *
2071
- * The stagger is registered with the animator under a supervisor entry so
2072
- * `animator.cancel(handle)`, `animator.cancelKey(opts.cancelKey)`, and
2073
- * `animator.isActive(opts.cancelKey)` all work for it.
2074
- */
2075
- stagger<TItem>(items: readonly TItem[], delay: StaggerDelay): StaggerBuilder<TItem>;
2076
- stagger<TItem>(items: readonly TItem[], delay: StaggerDelay, factory: StaggerFactory<TItem>, opts?: StaggerOptions): AnimationHandle;
2077
- /**
2078
- * Keyframe timeline. Registered like any other animation, so its playhead
2079
- * responds to `pause`, `setTimeScale` and `cancelKey`. Sampled tracks are a
2080
- * pure function of the playhead; event tracks fire only on forward playback.
2081
- */
2082
- timeline(opts: TimelineOptions): TimelineHandle;
2083
- /** Per-node, per-channel color override registry consulted by the renderer's
2084
- * path layer before reading consumer accessors. Used by `tweenVertexColors`,
2085
- * `springVertexColors`, `cycleVertexColors`, `staggerVertexColors`. Cleared
2086
- * automatically on animator unmount. */
2087
- colorOverrides: ColorOverrideRegistry;
2088
- /**
2089
- * Subscribe to a callback fired once per RAF frame while any animation is
2090
- * active. Returns an unsubscribe function. Used by consumers (typically
2091
- * `<SceneCanvas>`) that need to repaint when an animation's side-effect
2092
- * is read from a non-scene channel (e.g. `colorOverrides` consulted from
2093
- * a custom `drawOne`) — scene mutations naturally trigger a repaint, but
2094
- * `colorOverrides` writes do not.
2095
- *
2096
- * The callback fires AFTER the per-frame tick of each registered
2097
- * animation, so by the time it runs `colorOverrides.get(...)` returns
2098
- * the latest values. If no animations are active, no tick fires.
2099
- */
2100
- onTick(cb: () => void): () => void;
2101
- /**
2102
- * Keep the animator's RAF loop running until the returned cancel
2103
- * function is called. Use for animations whose effect is read on every
2104
- * frame but which don't have a natural progress state (e.g.
2105
- * `cycleVertexColors`, which expresses its current value as a function
2106
- * of `performance.now()` rather than as a tween from `from` to `to`).
2107
- * Without a keep-alive entry the loop would idle and `onTick` would
2108
- * stop firing even though the override is still installed.
2109
- */
2110
- keepAlive(): () => void;
2111
- }
2112
- /** Options for `Animator.loop`. */
2113
- interface LoopOptions {
2114
- /** Maximum number of iterations. Default Infinity. */
2115
- count?: number;
2116
- /** Invoked when the loop reaches `count` iterations naturally (not on cancel). */
2117
- onDone?: () => void;
2118
- /** Any new animation passed the same cancelKey cancels the prior one in flight.
2119
- * Also enables `animator.cancelKey` / `animator.isActive(key)` for this loop. */
2120
- cancelKey?: string;
2121
- }
2122
- /** Options for the top-level `Animator.stagger` factory form (third overload). */
2123
- interface StaggerOptions {
2124
- /** Cancel-key for the supervising registration. `animator.cancelKey(key)`
2125
- * cancels the whole stagger; `animator.isActive(key)` returns true while
2126
- * any timer or child is alive. */
2127
- cancelKey?: string;
2128
- }
2129
- /** Produces one iteration of a loop. Must arrange for `next` to be called when
2130
- * the animation it returns finishes, or the loop stalls after one pass. */
2131
- type LoopFactory = (iteration: number, next: () => void) => AnimationHandle;
2132
- /** Per-index delay schedule. Number ⇒ `index * delay` ms. Function ⇒ caller
2133
- * decides the absolute delay for each index (e.g. `i => i * i * 30`). */
2134
- type StaggerDelay = number | ((index: number) => number);
2135
- /** Produces the animation for one staggered item. */
2136
- type StaggerFactory<TItem> = (item: TItem, index: number) => AnimationHandle;
2137
- /** A `T` value or a function that derives one from the per-item context. Used
2138
- * by the fluent builder methods (`.tween`, `.springPose`) so each item can
2139
- * vary an option (e.g. `to: (_item, i) => (i + 1) * 10`). */
2140
- type StaggerPerItem<T, TItem> = T | ((item: TItem, index: number) => T);
2141
- /** Options for the stagger builder's `.tween`: a tween per item, where
2142
- * `from`, `to` and `ms` may each vary by item. */
2143
- interface StaggerTweenOptions<T, TItem> {
2144
- from: StaggerPerItem<T, TItem>;
2145
- to: StaggerPerItem<T, TItem>;
2146
- ms: StaggerPerItem<number, TItem>;
2147
- easing?: EasingSpec;
2148
- interpolate?: Interpolate<T>;
2149
- onTick: (value: T, item: TItem, index: number) => void;
2150
- onDone?: (item: TItem, index: number) => void;
2151
- }
2152
- /** Options for the stagger builder's `.springPose`: the spring tuning, and
2153
- * whether each item's settle is recorded as an undoable op. */
2154
- interface StaggerSpringPoseOptions<TPose> {
2155
- preset?: SpringPresetName;
2156
- stiffness?: number;
2157
- damping?: number;
2158
- mass?: number;
2159
- geometry?: PoseProjection<TPose>;
2160
- recordOp?: boolean;
2161
- opLabel?: string;
2162
- }
2163
- /** Fluent form of `Animator.stagger`: pick what to run per item after the
2164
- * items and the delay schedule are already fixed. */
2165
- interface StaggerBuilder<TItem> {
2166
- /** Run an arbitrary per-item factory. */
2167
- each(factory: StaggerFactory<TItem>): AnimationHandle;
2168
- /** Sugar: per-item `animator.tween` with per-item-varying options. */
2169
- tween<T>(opts: StaggerTweenOptions<T, TItem>): AnimationHandle;
2170
- /** Sugar: per-item `springPose` against an adapter. `poseFn` returns the
2171
- * target pose for each item. Each item must either be a primitive
2172
- * (string/number) or expose a string `id` field — otherwise pose ids
2173
- * would collide on `"[object Object]"` and successive tweens would
2174
- * cancel each other. Throws on items that satisfy neither. */
2175
- springPose<TPose>(adapter: SceneAdapter<{
2176
- id: string;
2177
- }, TPose>, poseFn: (item: TItem, index: number) => TPose, opts?: StaggerSpringPoseOptions<TPose>): AnimationHandle;
2178
- }
2179
- /** Options for `Animator.tweenLoop` — a tween's options plus how each
2180
- * iteration relates to the last. */
2181
- interface TweenLoopOptions<T> {
2182
- from: T;
2183
- to: T;
2184
- ms: number;
2185
- easing?: EasingSpec;
2186
- /** `restart` (default): from→to every iteration.
2187
- * `reverse`: to→from every iteration.
2188
- * `alternate`: even iterations from→to, odd iterations to→from. */
2189
- direction?: 'restart' | 'reverse' | 'alternate';
2190
- count?: number;
2191
- interpolate?: Interpolate<T>;
2192
- onTick: (value: T) => void;
2193
- onDone?: () => void;
2194
- cancelKey?: string;
2195
- }
2196
-
2197
- /** Cancel-key prefix. Each hook instance appends its own id, so two runners
2198
- * sharing an animator do not cancel each other. */
2199
- declare const VIEW_ANIMATION_KEY = "view";
2200
- /** How the camera should move. */
2201
- interface ViewAnimationOptions {
2202
- /** Duration in ms. Default 250. */
2203
- ms?: number;
2204
- /** Easing curve. Default `easeOutCubic`. */
2205
- easing?: EasingSpec;
2206
- /** Replace the kit's log-scale / fixed-anchor curve. */
2207
- interpolator?: InterpolatorFactory<View>;
2208
- /** Fires when the target is reached. Not called on cancel. */
2209
- onDone?: () => void;
2210
- }
2211
- /** Options accepted by {@link ViewAnimationApi.animateToBounds}. */
2212
- interface AnimateToBoundsOptions extends FitViewToBoundsOptions, ViewAnimationOptions {
2213
- }
2214
- /** What the runner reads and writes. On `<SceneCanvas>` this is the same
2215
- * channel `view.set` uses, so a camera animation on an uncontrolled canvas
2216
- * costs no React render. */
2217
- interface ViewChannel {
2218
- get(): View;
2219
- set(v: View): void;
2220
- }
2221
- /** The camera animation surface. One animation at a time. */
2222
- interface ViewAnimationApi {
2223
- /** Glide from the live view to `to`. A thunk receives the pending target when
2224
- * one is in flight, so successive discrete steps compound. */
2225
- animate(to: View | ((base: View) => View), opts?: ViewAnimationOptions): void;
2226
- /** `fitViewToBounds` composed with `animate`. */
2227
- animateToBounds(bounds: Bounds, dims: ViewportDims, opts?: AnimateToBoundsOptions): void;
2228
- /** Cancel. The view stays where it is — no jump to the target. */
2229
- stop(): void;
2230
- isAnimating(): boolean;
2231
- /** Where the in-flight animation is heading, or null when none is. */
2232
- target(): View | null;
2233
- /** Cancel unless the write that prompted this came from the runner's own
2234
- * per-frame write. Feed it from every channel that can move the camera. */
2235
- stopIfExternal(): void;
2236
- }
2237
- /**
2238
- * Animate the viewport `View`. Runs on the kit's {@link Animator} — pass one to
2239
- * share a canvas's animator, or omit it and the hook makes its own.
2240
- *
2241
- * Every animation from one instance registers under that instance's cancel key,
2242
- * so starting one cancels whatever *it* had in flight, and each starts from the
2243
- * *live* view rather than a captured value — an interrupted camera never jumps.
2244
- * Two instances on one animator are independent.
2245
- */
2246
- declare function useViewAnimation(view: ViewChannel, animator?: Animator): ViewAnimationApi;
2247
-
2248
- /**
2249
- * @experimental
2250
- * PointerContext — a tiny ambient context that publishes the world-space
2251
- * position of the canvas pointer, refreshed on every `pointermove` over
2252
- * the canvas. Cleared (set to `null`) on `pointerleave`.
2253
- *
2254
- * Why ref-based and not state-based: cursor moves fire dozens of times per
2255
- * second; routing those through React state would re-render every consumer
2256
- * in the tree. The context exposes a stable `pointerRef` whose `.current`
2257
- * is mutated directly by the publisher, plus a thunk `getDropPoint()` that
2258
- * reads it on demand. Consumers (e.g. `useClipboard`) pull via the thunk
2259
- * inside their callbacks — no subscription, no re-render.
2260
- *
2261
- * `<SceneCanvas>` publishes automatically. `useClipboardOps` consumes when
2262
- * the caller didn't pass an explicit `getDropPoint` option. Other future
2263
- * hit-on-cursor consumers (drop-zone hover, context-menu anchor) can reuse
2264
- * the same context.
2265
- */
2266
-
2267
- /** @experimental World-space pointer position, or `null` when the pointer
2268
- * isn't over the publishing canvas. */
2269
- type PointerWorldPos = {
2270
- worldX: number;
2271
- worldY: number;
2272
- } | null;
2273
- /** @experimental */
2274
- interface PointerContextValue {
2275
- /** Live ref — mutate to publish, read for the latest snapshot. The
2276
- * identity is stable for the lifetime of the provider. */
2277
- readonly pointerRef: MutableRefObject<PointerWorldPos>;
2278
- /** Convenience thunk equivalent to `() => pointerRef.current`. Stable
2279
- * identity for the lifetime of the provider; safe to pass to hooks. */
2280
- readonly getDropPoint: () => PointerWorldPos;
2281
- }
2282
- /**
2283
- * @experimental
2284
- * Wrap the part of the React tree that should share a pointer-position
2285
- * context. Usually placed at the demo / app root, alongside
2286
- * `<ActionsProvider>` and `<SelectionContextProvider>`.
2287
- *
2288
- * Most consumers don't need to mount this directly — `<SceneCanvas>` mounts
2289
- * an internal provider when no parent provider is in scope, so child hooks
2290
- * (`useClipboard` without an explicit `getDropPoint`) read the canvas's
2291
- * tracked pointer for free.
2292
- */
2293
- declare function PointerContextProvider({ children }: {
2294
- children: ReactNode;
2295
- }): ReactNode;
2296
- /** @experimental Read the surrounding pointer-context value, or `null` when
2297
- * no provider is in scope. */
2298
- declare function usePointerContext(): PointerContextValue | null;
2299
-
2300
- /** Which tool is active, plus the stack of tools temporarily held active by a
2301
- * hotkey (space-for-hand and the like). The dispatcher reads this to decide
2302
- * whose bindings are in scope. */
2303
- interface ActiveToolContextValue {
2304
- active: string;
2305
- hotkeyStack: string[];
2306
- setActive(id: string): void;
2307
- pushHotkey(id: string): void;
2308
- popHotkey(): void;
2309
- }
2310
- /** Props for `<ActiveToolContextProvider>`. */
2311
- interface ActiveToolContextProviderProps {
2312
- children: ReactNode;
2313
- initialActive?: string;
2314
- }
2315
- /** Provides active-tool state for a canvas. `<SceneCanvas>` mounts one. */
2316
- declare function ActiveToolContextProvider({ children, initialActive, }: ActiveToolContextProviderProps): react_jsx_runtime.JSX.Element;
2317
- /** The active-tool state in scope. Throws outside a provider; use
2318
- * `useActiveToolContextOptional` where one is not guaranteed. */
2319
- declare function useActiveToolContext(): ActiveToolContextValue;
2320
- /**
2321
- * Like `useActiveToolContext`, but returns `null` when no
2322
- * `<ActiveToolContextProvider>` is in scope instead of throwing. Used by
2323
- * `useStandardActions` to preserve its silent-no-op contract when no provider
2324
- * is present.
2325
- */
2326
- declare function useOptionalActiveToolContext(): ActiveToolContextValue | null;
2327
- /**
2328
- * Conditional `<ActiveToolContextProvider>` wrapper. Mounts a provider only
2329
- * when no parent provider is in scope — otherwise renders children unwrapped
2330
- * so callers (e.g. `<WeaselProvider>`, `<SceneCanvas>`) defer to the host's
2331
- * existing scope. Mirrors `ActionsProviderIfRoot` / `DepRegistryProviderIfRoot`.
2332
- */
2333
- declare function ActiveToolContextProviderIfRoot({ children, }: {
2334
- children: ReactNode;
2335
- }): react_jsx_runtime.JSX.Element;
2336
-
2337
- /**
2338
- * `enterTextEditAction` — immediate Action descriptor for entering in-place
2339
- * text editing on a selected text node.
2340
- *
2341
- * ## Status: REAL
2342
- *
2343
- * Fires via `useTextTool.bindings` when the user clicks on a
2344
- * selected text node. Calls `deps.textEdit.startEdit(id)` to activate the
2345
- * contenteditable overlay managed by `useTextEdit` / `useSceneTextEdit`.
2346
- *
2347
- * ## No defaultBinding / defaultBinding
2348
- *
2349
- * This action has no ambient key or gesture binding — it fires ONLY via
2350
- * `useTextTool`'s `Tool.bindings` entry:
2351
- *
2352
- * ```ts
2353
- * bindings: [
2354
- * { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' },
2355
- * ]
2356
- * ```
2357
- *
2358
- * Keeping it binding-free avoids ambient double-fire and scopes the action to
2359
- * the text tool context where `classifyTarget` is already wired.
2360
- *
2361
- * ## Self-guard: only act on text nodes
2362
- *
2363
- * The `'selected-body'` target yields a match for any selected node kind. To
2364
- * avoid entering text-edit mode when the text tool happens to have a non-text
2365
- * node selected, the action self-guards via an optional `isTextNode` predicate
2366
- * on `TextEditDep`:
2367
- *
2368
- * - When `isTextNode` is absent: action fires unconditionally (the binding
2369
- * spec is the real gate — consumers should only bind this action from the
2370
- * text tool).
2371
- * - When `isTextNode(id)` returns `false`: action is a no-op for that node.
2372
- *
2373
- * ### Pre-filtering at dispatch time
2374
- *
2375
- * `classifyTarget` now surfaces node kind, so a binding can pre-filter instead
2376
- * of relying on the self-guard:
2377
- *
2378
- * ```ts
2379
- * { spec: { kind: 'click', target: 'kind:text:selected' }, actionId: 'enterTextEdit' }
2380
- * ```
2381
- *
2382
- * That reads the *routing trait's* kind, so it matches whatever names the
2383
- * consumer registered in `<SceneCanvas routing>` — `'text'` under the kit's
2384
- * inferred default. `isTextNode` stays on `TextEditDep` because it also covers
2385
- * consumers who bind the broader `'selected-body'` target, and because it is
2386
- * the only guard for a consumer who opted out of routing entirely.
2387
- *
2388
- * ## Migration plan for useTextTool
2389
- *
2390
- * When wiring `useTextTool` to `Tool.bindings`:
2391
- *
2392
- * 1. Add to `useTextTool`'s `bindings`:
2393
- * ```ts
2394
- * { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' }
2395
- * ```
2396
- * 2. Register a `textEdit` dep sourced from the `useTextEdit` / `useSceneTextEdit`
2397
- * return value, plus an `isTextNode` predicate that checks `data.kind === 'text'`
2398
- * (or however the consumer identifies text nodes).
2399
- * 3. The existing `hitExisting` gate in `useTextTool`'s click route becomes
2400
- * redundant — remove it in the same pass.
2401
- */
2402
-
2403
- /**
2404
- * Dep for `enterTextEditAction`.
2405
- *
2406
- * Wrap the return value of `useTextEdit` / `useSceneTextEdit` to source this
2407
- * dep. The `isTextNode` predicate is optional — when absent the action fires
2408
- * unconditionally (the binding spec acts as the gate).
2409
- *
2410
- * @example
2411
- * ```ts
2412
- * const textEdit = useSceneTextEdit({ scene, container });
2413
- * useDepSource('textEdit', () => ({
2414
- * startEdit: textEdit.startEdit,
2415
- * isTextNode: (id) => scene.get(id as NodeId)?.data?.kind === 'text',
2416
- * }));
2417
- * ```
2418
- */
2419
- interface TextEditDep {
2420
- /**
2421
- * Begin editing the node with `id`. Activates the contenteditable overlay
2422
- * managed by `useTextEdit` / `useSceneTextEdit`.
2423
- */
2424
- startEdit(id: string, opts?: {
2425
- caret?: number | 'all';
2426
- }): void;
2427
- /**
2428
- * Optional predicate: returns `true` when the node with `id` is a text node.
2429
- * When absent the action fires on any selected node (binding spec is the gate).
2430
- * When present and returning `false`, the invocation is a no-op.
2431
- */
2432
- isTextNode?(id: string): boolean;
2433
- }
2434
- /**
2435
- * @experimental
2436
- * Static descriptor for the `enterTextEdit` Action.
2437
- *
2438
- * Requires dep-schema entries: `textEdit`, `selection`.
2439
- *
2440
- * No `defaultBinding` / `defaultBinding` — fires only via `Tool.bindings`.
2441
- * Self-guards via `TextEditDep.isTextNode` when provided.
2442
- */
2443
- declare const enterTextEditAction: Action & {
2444
- requires: string[];
2445
- };
2446
-
2447
- /**
2448
- * Consumer-supplied commit for the Slice action. `commit` receives the finite
2449
- * slice segment (world coords); the consumer scans the scene, splits crossed
2450
- * paths via `splitPathByLine`, and applies the result as one undoable batch.
2451
- */
2452
- interface SliceDep {
2453
- commit(a: Point2, b: Point2): void;
2454
- }
2455
- /**
2456
- * @experimental
2457
- * Static descriptor for the `slice` Action.
2458
- *
2459
- * Ongoing drag invoker: tracks a slice line from drag start to current
2460
- * pointer, renders a live line overlay while the gesture is in flight,
2461
- * and on commit calls `SliceDep.commit(a, b)`. No-ops gracefully when
2462
- * the `slice` dep is absent.
2463
- */
2464
- declare const sliceAction: Action & {
2465
- requires: string[];
2466
- };
2467
-
2468
- /**
2469
- * Clipboard dep — the imperative surface `useClipboardOps` returns.
2470
- *
2471
- * Consumers publish their live clipboard through `useDepSource('clipboard',
2472
- * …)` from inside the `<DepRegistryProvider>` (i.e. under `<SceneCanvas>`).
2473
- * The kit deliberately does not build one for them: `useClipboardOps` needs
2474
- * an adapter and a selection reader that only the consumer can supply.
2475
- */
2476
- interface ClipboardDep {
2477
- copy(): void;
2478
- paste(): void;
2479
- isEmpty(): boolean;
2480
- }
2481
- /**
2482
- * @experimental
2483
- * Static descriptor for the `clipboard.copy` Action (Cmd/Ctrl+C).
2484
- */
2485
- declare const clipboardCopyAction: Action & {
2486
- requires: string[];
2487
- };
2488
- /**
2489
- * @experimental
2490
- * Static descriptor for the `clipboard.cut` Action (Cmd/Ctrl+X) — copy, then
2491
- * the same batched delete `deleteAction` performs, as one undo entry.
2492
- */
2493
- declare const clipboardCutAction: Action & {
2494
- requires: string[];
2495
- };
2496
-
2497
- /** Optional consumer seam: given a node and the affine `m` that a pose-transform
2498
- * action applied to the node's POSE, return updated `data` with the node's
2499
- * data-held geometry transformed by `m`, or `null` if this node has no
2500
- * data-held geometry (the kit leaves `data` alone). */
2501
- interface GeometryProjection {
2502
- transform(node: {
2503
- id?: string;
2504
- data: unknown;
2505
- pose: unknown;
2506
- }, m: Mat3): unknown | null;
2507
- }
2508
-
2509
- /** Minimal view API the action layer consumes. */
2510
- interface ViewApi {
2511
- get(): View;
2512
- set(v: View): void;
2513
- /** Optional recenter callback. When wired, `viewportZoomAction`'s `reset`
2514
- * branch (Cmd-0) calls this instead of resetting to identity — letting
2515
- * consumers re-fit the page (or other reference bounds) into the workspace.
2516
- * Return the target `View` to let the action animate there; return nothing
2517
- * to keep dispatching the view yourself. */
2518
- recenter?(): View | void;
2519
- /** Optional canvas-local host dimensions (CSS px). When wired,
2520
- * `viewportZoomAction`'s keyboard branches (Cmd+= / Cmd+-) anchor at the
2521
- * host center instead of the top-left origin. Null when the host isn't
2522
- * measurable (unmounted). */
2523
- hostSize?(): {
2524
- width: number;
2525
- height: number;
2526
- } | null;
2527
- /** Optional camera animation. `<SceneCanvas>` wires these three; a consumer
2528
- * publishing their own `view` dep need not, and actions fall back to `set`. */
2529
- animate?(to: View, opts?: ViewAnimationOptions): void;
2530
- stopAnimation?(): void;
2531
- /** Where an in-flight camera animation is heading, or null. Compute the next
2532
- * discrete step from this so repeated presses compound. */
2533
- animationTarget?(): View | null;
2534
- /** Optional momentum decay. `<SceneCanvas>` wires this from `useDecayLoop`;
2535
- * a consumer publishing their own `view` dep need not, and `viewport.dragPan`
2536
- * simply lands the pan without coasting. */
2537
- decay?(config: DecayLoopConfig): void;
2538
- stopDecay?(): void;
2539
- }
2540
- /**
2541
- * Adapter dep for `areaSelectAction`.
2542
- *
2543
- * Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>` via AABB
2544
- * overlap over scene nodes. Consumers with custom hit-testing override this
2545
- * dep entry in their own registrar.
2546
- */
2547
- /**
2548
- * Topmost-node-at-world-point dep, consumed by `moveAction` for
2549
- * reparent-on-drop and available to any action that needs a single-best
2550
- * pick. Mirrors the same hit-test plumbing `<SceneCanvas>` feeds to the
2551
- * tool dispatcher; consumers with custom hit-testing override here.
2552
- *
2553
- * `exclude` is iterated once per call and treated as a set membership
2554
- * test — the dep walks hits front-to-back and returns the first id not
2555
- * in the exclude set. Pass moving-node roots + their descendants when
2556
- * the caller wants to ignore the nodes it's manipulating.
2557
- */
2558
- type NodeAtPointDep = (point: {
2559
- x: number;
2560
- y: number;
2561
- }, exclude?: Iterable<NodeId>) => NodeId | null;
2562
- /** What an area-selecting action needs: a way to ask what a region covers,
2563
- * and a way to read and replace the selection. */
2564
- interface AreaSelectDep {
2565
- /** Return ids of all scene nodes whose AABB overlaps `bounds`. */
2566
- hitTestArea(bounds: {
2567
- x: number;
2568
- y: number;
2569
- width: number;
2570
- height: number;
2571
- }): NodeId[];
2572
- /** Return the current selection id list. */
2573
- getSelection(): NodeId[];
2574
- /** Replace the current selection. */
2575
- setSelection(ids: NodeId[]): void;
2576
- }
2577
- /**
2578
- * Adapter dep for `editAnchorsAction`.
2579
- *
2580
- * Provides narrow read/write access to the editable polygon for a single
2581
- * node. Consumers register this dep so anchor-edit actions can read/write
2582
- * the polygon WITHOUT knowing whether it lives directly on the node's
2583
- * pose (`pose.kind === 'polygon'`) or on `node.data.path` (the kit's
2584
- * built-in pen-tool default, also WeaselDraw's shape).
2585
- *
2586
- * Note on live previews: in-flight edit state is surfaced through the
2587
- * dispatcher's standard `OngoingHandle.previewIds/previewPose/previewData`
2588
- * triple (not this dep), so chrome and preview-ghost stay in lock-step
2589
- * via one source of truth.
2590
- */
2591
- interface EditAnchorsDep {
2592
- /** Id of the node currently being edited. Empty string means no node is
2593
- * currently in edit mode — the chrome and gesture both opt out. */
2594
- editingId: string;
2595
- /** Enter/exit edit mode for a specific node. Pass `null` (or an empty
2596
- * string) to exit. `enterPathEditAction` and `exitPathEditAction` call
2597
- * this; consumers can call it directly to drive edit mode programmatically. */
2598
- setEditingId(id: string | null): void;
2599
- /** Returns the COMMITTED editable polygon in world coordinates, or
2600
- * null if this node has no editable polygon. Does NOT consult in-
2601
- * flight previews — callers that need live state read the dispatcher's
2602
- * in-flight handles. */
2603
- getEditablePath(id: string): unknown;
2604
- /** Returns where the polygon is stored — `'pose'` when `node.pose`
2605
- * IS the polygon, `'data'` when it lives on `node.data.path` with a
2606
- * rect pose, or `null` when the node has no editable polygon. The
2607
- * action uses this to know which preview-ghost axis to populate
2608
- * (`previewPose` only / `previewData` + `previewPose` for data.path). */
2609
- getStorageKind(id: string): 'pose' | 'data' | null;
2610
- /** Returns the node's raw `pose` and `data` so storage-aware actions
2611
- * can capture origin state at gesture-start and synthesize a matching
2612
- * `previewPose` / `previewData` during `onMove`. Used by
2613
- * `editAnchorsAction` for the data.path branch (rect pose + data
2614
- * carrying extra fields like fill / stroke that must be preserved
2615
- * through the preview). Returns null when the node is gone. */
2616
- getNodeShape(id: string): {
2617
- pose: unknown;
2618
- data: unknown;
2619
- } | null;
2620
- /** Commit `worldPath` as the new value for `id`. Implementation routes
2621
- * to setPose (when pose IS the polygon) or batched setPose+update
2622
- * (when the polygon lives on data.path). Records one history entry
2623
- * labelled `label`. */
2624
- applyEdit(id: string, worldPath: unknown, label: string): void;
2625
- /**
2626
- * Anchors currently selected within the edited path, as **flat anchor
2627
- * indices** — the same numbering `enumerateAnchors` produces and the
2628
- * `anchor:N` affordance kinds carry.
2629
- *
2630
- * Selection is transient UI state, deliberately not part of the scene:
2631
- * it is cleared whenever `editingId` changes, and any edit that
2632
- * renumbers anchors (insert, delete) is responsible for leaving it
2633
- * coherent. Empty means "no anchor selected" — the keyboard actions
2634
- * (nudge, delete) no-op rather than acting on all anchors, matching
2635
- * Illustrator.
2636
- */
2637
- selectedAnchors: ReadonlySet<number>;
2638
- /** Replace the anchor selection. Pass an empty iterable to clear. */
2639
- setSelectedAnchors(next: Iterable<number>): void;
2640
- /**
2641
- * In-flight anchor-marquee rect in world coords, or null when no
2642
- * marquee drag is active. Written by `marqueeAnchorsAction` and read by
2643
- * the path-editing overlay — the same "ongoing action owns the preview,
2644
- * chrome just draws it" split the move/resize ghosts use.
2645
- */
2646
- marquee: {
2647
- x: number;
2648
- y: number;
2649
- width: number;
2650
- height: number;
2651
- } | null;
2652
- /** Set or clear the in-flight marquee rect. */
2653
- setMarquee(rect: {
2654
- x: number;
2655
- y: number;
2656
- width: number;
2657
- height: number;
2658
- } | null): void;
2659
- }
2660
- /**
2661
- * Adapter dep for `lassoSelectAction`.
2662
- *
2663
- * Provides polygon-lasso hit-testing + selection read/write.
2664
- * Consumers that don't implement `hitTestLasso` can omit it; the action
2665
- * falls back to a bounding-box AABB test via `hitTestArea`.
2666
- */
2667
- interface LassoSelectDep {
2668
- /**
2669
- * Hit-test against a closed polygon (vertex order CW or CCW; last→first
2670
- * closing edge is implicit). Returns matching node ids.
2671
- * Optional — when absent, `lassoSelectAction` falls back to AABB via
2672
- * `hitTestArea`.
2673
- */
2674
- hitTestLasso?(polygon: ReadonlyArray<{
2675
- x: number;
2676
- y: number;
2677
- }>, mode: 'centers' | 'intersect' | 'enclosed'): string[];
2678
- /** Return ids of nodes whose AABB overlaps the given rect (fallback). */
2679
- hitTestArea(bounds: {
2680
- x: number;
2681
- y: number;
2682
- width: number;
2683
- height: number;
2684
- }): string[];
2685
- /** Return the current selection id list. */
2686
- getSelection(): string[];
2687
- /** Replace the current selection. */
2688
- setSelection(ids: string[]): void;
2689
- }
2690
- /**
2691
- * Options for the kit `image/svg+xml` content handler, threaded from
2692
- * SceneCanvas's `ingestion={{ svg }}` prop.
2693
- */
2694
- interface SvgIngestOptions {
2695
- /** Parse dropped/pasted/picked SVG files into native scene nodes (path /
2696
- * text leaves under containers mirroring the source `<g>` structure)
2697
- * instead of the default single embedded-image node.
2698
- *
2699
- * Pass `unpackSvgFiles` from `@weasel-js/svg`:
2700
- *
2701
- * ```ts
2702
- * import { unpackSvgFiles } from '@weasel-js/svg';
2703
- * <SceneCanvas ingestion={{ svg: { unpack: unpackSvgFiles } }} />
2704
- * ```
2705
- *
2706
- * It is injected rather than flagged on with `true` because the SVG parser
2707
- * lives in `@weasel-js/svg`, which depends on this package — core importing
2708
- * it back would make the two mutually dependent and unpublishable
2709
- * separately. Passing the function keeps the parser out of core's bundle
2710
- * for consumers who never unpack. */
2711
- unpack?: SvgUnpacker;
2712
- }
2713
- /** Parses SVG files and inserts the resulting nodes into `ctx.scene`, as one
2714
- * `applyOps` batch per file. Implemented by `unpackSvgFiles` in
2715
- * `@weasel-js/svg`; see {@link SvgIngestOptions.unpack}. */
2716
- type SvgUnpacker = (files: File[], ctx: IngestCtx) => Promise<void>;
2717
- /**
2718
- * Clipboard-paste seam consumed by the kit weasel-JSON content handler
2719
- * (`IngestCtx.clipboard`). Built by `<SceneCanvas>` from its own synthesized
2720
- * adapter + the `ingestion.clipboard` prop; absent when the consumer set
2721
- * `ingestion.clipboard.enabled === false` or the adapter lacks `commitPaste`.
2722
- * Absence makes the handler decline inert (dwarn, nothing ingested) — its
2723
- * matched items were already consumed at match time and do not fall through
2724
- * to other handlers.
2725
- */
2726
- interface ClipboardIngestCtx {
2727
- /** The hosting canvas's adapter — `commitPaste` materializes the pasted
2728
- * nodes (fresh ids, offset applied); insertion still goes through ops. */
2729
- adapter: InsertAdapter<{
2730
- id: string;
2731
- }>;
2732
- /** JSON reviver for the weasel wire payload (typed arrays etc.) — from
2733
- * `SceneCanvasProps.ingestion.clipboard.reviver`. */
2734
- reviver?: (key: string, value: unknown) => unknown;
2735
- }
2736
- /**
2737
- * Dep for the `ingest` action (external-content ingestion).
2738
- * Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
2739
- * `useIngestionDepSource` — canvas rect + current view.
2740
- */
2741
- interface IngestionDep {
2742
- /** Visible canvas area in world coordinates. */
2743
- viewportWorldRect(): {
2744
- x: number;
2745
- y: number;
2746
- width: number;
2747
- height: number;
2748
- };
2749
- /** Consumer file→src resolver (from SceneCanvas's `ingestion` prop).
2750
- * Live accessor — read it at use time. Destructuring (or copying the
2751
- * property early) snapshots the current value and won't track later
2752
- * prop changes across an `await`. */
2753
- resolveSrc?: (file: File) => Promise<string>;
2754
- /** Kit SVG-handler options (from SceneCanvas's `ingestion` prop).
2755
- * Live accessor, same caveat as `resolveSrc`. */
2756
- svg?: SvgIngestOptions;
2757
- /** Clipboard-paste seam for the kit weasel-JSON handler.
2758
- * Live accessor, same caveat as `resolveSrc`. */
2759
- clipboard?: ClipboardIngestCtx;
2760
- }
2761
- /**
2762
- * Per-kind extra geometry passed to `InsertDep.commit`.
2763
- *
2764
- * Built-in tools populate a typed variant so the kit's default factory can
2765
- * render the true tool params (line endpoints, polygon side count, star
2766
- * geometry, pencil sample list). Consumer-defined tools may pass any
2767
- * `{ kind: string; ... }` payload; the kit's factory falls back to AABB
2768
- * inscription for unknown kinds.
2769
- *
2770
- * `bounds` is still passed alongside as a useful AABB pose hint — factories
2771
- * may use it as the node's pose even when richer geometry is available.
2772
- */
2773
- type InsertExtras = {
2774
- kind: 'rect';
2775
- } | {
2776
- kind: 'ellipse';
2777
- } | {
2778
- kind: 'line';
2779
- a: {
2780
- x: number;
2781
- y: number;
2782
- };
2783
- b: {
2784
- x: number;
2785
- y: number;
2786
- };
2787
- } | {
2788
- kind: 'polygon';
2789
- sides: number;
2790
- rotation: number;
2791
- center?: {
2792
- x: number;
2793
- y: number;
2794
- };
2795
- radius?: number;
2796
- } | {
2797
- kind: 'star';
2798
- points: number;
2799
- innerRadiusRatio: number;
2800
- rotation: number;
2801
- center?: {
2802
- x: number;
2803
- y: number;
2804
- };
2805
- outerRadius?: number;
2806
- } | {
2807
- kind: 'pencil';
2808
- samples: ReadonlyArray<DragSample>;
2809
- } | {
2810
- kind: 'text';
2811
- text?: string;
2812
- } | {
2813
- kind: 'image';
2814
- src?: string;
2815
- opacity?: number;
2816
- /** Chrome-only: what the in-flight drag paints. Read by the overlay
2817
- * layer, ignored by the insert dep. */
2818
- preview?: 'bitmap' | 'outline';
2819
- } | {
2820
- kind: string;
2821
- [extra: string]: unknown;
2822
- };
2823
- /**
2824
- * World-space point snapping — grid, guides, or any consumer rule.
2825
- *
2826
- * Sourced by `<SceneCanvas>` from its `toolOptions.snapPoint`. Actions apply
2827
- * it to the coords they ingest so the live preview and the committed
2828
- * geometry agree; `insertAction` snaps the drag's start and current point.
2829
- *
2830
- * Optional: when the dep is absent, actions treat it as identity.
2831
- */
2832
- interface SnapDep {
2833
- /** Snap a world-space point. Return `p` unchanged to opt out. */
2834
- point(p: {
2835
- x: number;
2836
- y: number;
2837
- }): {
2838
- x: number;
2839
- y: number;
2840
- };
2841
- }
2842
- /**
2843
- * Adapter dep for `insertAction`.
2844
- *
2845
- * Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>`. The `extras`
2846
- * carry the active tool's kind + per-kind geometry. Callers
2847
- * that need typed data must supply a richer `insert` dep.
2848
- */
2849
- interface InsertDep {
2850
- /**
2851
- * Materialise a new node from the given drag-rect bounds and typed
2852
- * per-kind extras. Returns the new node's id, or `null` if the consumer
2853
- * rejected the insert (e.g. sub-threshold bounds, unknown kind).
2854
- */
2855
- commit(bounds: {
2856
- x: number;
2857
- y: number;
2858
- width: number;
2859
- height: number;
2860
- }, extras: InsertExtras): NodeId | null;
2861
- }
2862
- /**
2863
- * Adapter dep for `resizeAction`.
2864
- *
2865
- * Carries the four behavior-shaping options the legacy `useResize` hook
2866
- * exposed through `UseResizeOptions`: bounds-frame behaviors (e.g.
2867
- * `lockAspectWithModifier`), world-space anchor-point snap behaviors (e.g.
2868
- * `pointSnapToGrid`), group-expansion (`expandIds`), and pose↔bounds
2869
- * projection (`geometry`).
2870
- *
2871
- * Optional in `DepSchema`: when absent, `resizeAction` falls back to
2872
- * identity defaults (no behaviors, identity expandIds, `RECT_POSE_DESCRIPTOR`
2873
- * geometry). Consumers wire the dep via `useDepSource('resizePolicy', ...)`
2874
- * from any descendant of `<DepRegistryProvider>` / `<SceneCanvas>`.
2875
- *
2876
- * The generic is erased to `unknown` at the schema entry; consumers cast at
2877
- * the call site (mirrors the `scene` entry's convention).
2878
- */
2879
- interface ResizePolicy<TPose> {
2880
- /** Bounds-frame constraints. Constrained to `TPose extends ResizePose` since
2881
- * constraints read/write `{x,y,width,height}`. For non-rect TPose pass `[]`. */
2882
- constraints: TPose extends ResizePose ? BoundsConstraint<TPose>[] : never[];
2883
- /** World-space anchor-point snap behaviors. Same TPose constraint as
2884
- * `constraints`. */
2885
- pointSnap: TPose extends ResizePose ? PointSnapBehavior<TPose>[] : never[];
2886
- /** Group-expansion at gesture start. Identity (`ids => ids`) when group
2887
- * resize isn't wanted. */
2888
- expandIds: (ids: string[]) => string[];
2889
- /** Projection from `TPose` to bounds and back. Use `RECT_POSE_DESCRIPTOR`
2890
- * for plain rect poses. */
2891
- projection: PoseProjection<TPose>;
2892
- }
2893
- /**
2894
- * Layout-strategy lookup by container id, consumed by `moveAction` to run
2895
- * the drag-time reflow pass. Sourced by `<SceneCanvas>` from its `layouts`
2896
- * prop. Optional: `getLayout` returns null for any container when no layout
2897
- * is configured, so the reflow pass is a no-op then.
2898
- */
2899
- interface LayoutDep {
2900
- getLayout(containerId: string): LayoutStrategy<unknown> | null;
2901
- }
2902
- /**
2903
- * The names an action may declare in `requires`, and what each resolves to.
2904
- *
2905
- * This is the whole vocabulary of things an action can reach — selection,
2906
- * scene, view, history, and the rest. Consumers add their own entries by
2907
- * augmenting the interface (`declare module '@weasel-js/core'`), which is what
2908
- * makes a custom dep name type-check in `requires` and in the deps bag.
2909
- */
2910
- interface DepSchema {
2911
- /** Kit selection state — ids of currently selected nodes. */
2912
- selection: SelectionApi;
2913
- /** Current viewport — camera position + scale. */
2914
- view: ViewApi;
2915
- /**
2916
- * Scene tree — structural reads + undoable mutations.
2917
- *
2918
- * The entry uses the fully-erased form `Scene<unknown, string, unknown>`
2919
- * because `DepSchema` must be concrete. Actions that need a typed scene
2920
- * should cast: `deps.scene as Scene<MyData, MyLayer, MyPose>`.
2921
- */
2922
- scene: Scene<unknown, string, unknown>;
2923
- /** Undo/redo history bound to the current scene. */
2924
- history: History;
2925
- /**
2926
- * Canvas pointer position in world space.
2927
- *
2928
- * Exposes `pointerRef` (mutable live ref) and `getDropPoint()` thunk.
2929
- * Marked `@experimental` in the source.
2930
- */
2931
- pointer: PointerContextValue;
2932
- /** Currently active tool id + hotkey-hold stack. */
2933
- activeTool: ActiveToolContextValue;
2934
- /**
2935
- * Area-select dep — AABB hit-test + selection read/write.
2936
- *
2937
- * Sourced from `<SceneCanvas>` via AABB overlap over all scene
2938
- * nodes. Override per-consumer for custom hit-testing (e.g. contain-mode,
2939
- * lock-aware filtering).
2940
- */
2941
- areaSelect: AreaSelectDep;
2942
- /**
2943
- * Topmost node at a world-space point. Sourced by `<SceneCanvas>` from
2944
- * the same picker that feeds the tool dispatcher's `getNodeAtPoint`.
2945
- * Optional: actions that read this (e.g. `moveAction` reparent-on-drop)
2946
- * fall back to a no-op when the dep isn't registered.
2947
- */
2948
- nodeAtPoint?: NodeAtPointDep;
2949
- /**
2950
- * Insert dep — node factory for drag-to-insert.
2951
- *
2952
- * Sourced from `<SceneCanvas>`. The `kind` param comes from
2953
- * the active binding's `opts.params.kind`. Override per-consumer to
2954
- * provide a typed node factory (e.g. with custom data payloads).
2955
- */
2956
- insert: InsertDep;
2957
- /**
2958
- * Snap dep — world-space point snapping (grid / guides).
2959
- *
2960
- * Sourced by `<SceneCanvas>` from `toolOptions.snapPoint`. Optional:
2961
- * absent means no snapping (identity).
2962
- */
2963
- snap?: SnapDep;
2964
- /**
2965
- * Lasso-select dep — polygon hit-test + selection read/write.
2966
- *
2967
- * Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>`.
2968
- * Falls back to AABB hit-test when `hitTestLasso` is absent.
2969
- */
2970
- lassoSelect: LassoSelectDep;
2971
- /**
2972
- * Edit-anchors dep — narrow read/write of one polygon's path pose.
2973
- *
2974
- * Sourced from consumer. Wraps `getPose`/`setPose`/`applyOps`
2975
- * for the currently-being-edited polygon node.
2976
- *
2977
- * The `editAnchorsAction` requires this dep to be registered when anchor
2978
- * editing is active. If absent, `start` returns an empty handle (no-op).
2979
- */
2980
- editAnchors: EditAnchorsDep;
2981
- /**
2982
- * Text-edit dep — activates the in-place text editing overlay.
2983
- *
2984
- * Sourced from consumer via `useTextEdit` / `useSceneTextEdit`.
2985
- * The `enterTextEditAction` requires this dep to be registered by the text
2986
- * tool when text editing is available.
2987
- *
2988
- * The optional `isTextNode` predicate guards against entering edit mode on
2989
- * non-text nodes. A binding can pre-filter instead with a
2990
- * `target: 'kind:text:selected'` spec; the guard remains for consumers who
2991
- * bind the broader `'selected-body'` target or opted out of routing.
2992
- */
2993
- textEdit: TextEditDep;
2994
- /**
2995
- * Resize-policy dep — bounds constraints, point-snap behaviors,
2996
- * group expansion, and pose↔bounds projection for `resizeAction`.
2997
- *
2998
- * Optional: when omitted, `resizeAction` falls back to identity defaults
2999
- * (no constraints, no snap, identity expandIds, `RECT_POSE_DESCRIPTOR`).
3000
- * Consumers wire via `useDepSource('resizePolicy', ...)` or the
3001
- * `useResizePolicy` helper.
3002
- */
3003
- resizePolicy?: ResizePolicy<unknown>;
3004
- /**
3005
- * Booleans adapter — read selection ids, fetch world-space `Path`s,
3006
- * compare z-order, and mint result nodes for Pathfinder ops.
3007
- *
3008
- * Consumers wire via `useBooleansAdapter(adapter)` (a thin wrapper
3009
- * around `useDepSource('booleansAdapter', ...)`). The descriptor's
3010
- * `enabled` predicate reads `deps.selection` for the count check; the
3011
- * invoker reads `deps.booleansAdapter` to execute the op.
3012
- */
3013
- booleansAdapter?: BooleansAdapter;
3014
- /**
3015
- * Gesture dispatcher control surface — exposes `cancelAll(reason)` so
3016
- * actions that need to abort an in-flight handle (Escape cancels a
3017
- * drag, etc.) can do so. Sourced by `<SceneCanvas>` from the
3018
- * dispatcher instance it already owns.
3019
- */
3020
- dispatcher?: {
3021
- cancelAll(reason: 'commit' | 'cancel'): void;
3022
- };
3023
- /**
3024
- * Layout-strategy lookup. Sourced by `<SceneCanvas>` from `layouts`.
3025
- * Optional: absent (or all-null) → `moveAction` skips reflow.
3026
- */
3027
- layout?: LayoutDep;
3028
- /**
3029
- * Slice dep — consumer-supplied commit for the Slice action.
3030
- *
3031
- * Receives the finite slice segment in world coordinates; the consumer
3032
- * scans the scene, splits crossed paths via `splitPathByLine`, and
3033
- * applies the result as one undoable batch.
3034
- *
3035
- * Optional: when absent, `sliceAction` is a no-op.
3036
- */
3037
- slice?: SliceDep;
3038
- /**
3039
- * Clipboard dep — the imperative surface `useClipboardOps` returns.
3040
- *
3041
- * Published by the consumer (`useDepSource('clipboard', …)` from under
3042
- * `<SceneCanvas>`), because `useClipboardOps` needs an adapter and a
3043
- * selection reader only the consumer has. Feeds `clipboard.copy` /
3044
- * `clipboard.cut`; both no-op when the dep is absent.
3045
- */
3046
- clipboard?: ClipboardDep;
3047
- /**
3048
- * Optional consumer commit hook. When present, `moveAction` (and other
3049
- * default actions) submit their committed ops through it instead of
3050
- * `scene.applyBatch`, so apps with their own history integration
3051
- * (checkpoint + push entry) capture the gesture as one undo entry.
3052
- * When absent, commits fall back to `scene.applyBatch`.
3053
- */
3054
- applyOps?: (ops: Op[], label: string) => void;
3055
- /** Optional pose-composition strategy for hierarchical (local-pose) scenes.
3056
- * When absent, defaults to IDENTITY (absolute-pose: nodes store world
3057
- * coords). Local-pose consumers supply { compose: composeRectPose,
3058
- * decompose: decomposeRectPose } (or their pose shape's equivalent). */
3059
- poseComposition?: PoseComposition<unknown>;
3060
- /**
3061
- * Ingestion dep — canvas viewport rect + consumer file→src resolver.
3062
- *
3063
- * Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
3064
- * `useIngestionDepSource`. Feeds `ingestAction` with the world-space
3065
- * viewport rect for paste-placement and image fit-clamping, and forwards
3066
- * the consumer's optional `resolveSrc` seam.
3067
- *
3068
- * Optional: when absent, the `ingest` action no-ops (there is no
3069
- * placement geometry to work with).
3070
- */
3071
- ingestion?: IngestionDep;
3072
- /**
3073
- * Optional consumer seam for the eager-sync layer: lets pose-transform
3074
- * actions (resize/move/nudge/flip — NOT rotate) ALSO rewrite a node's
3075
- * data-held geometry. Given a node and the affine `m` applied to its pose,
3076
- * `transform(node, m)` returns updated `data` (geometry mapped by `m`) or
3077
- * `null` for nodes with no data-held geometry.
3078
- *
3079
- * Strictly opt-in: when absent (or when `transform` returns null), the kit
3080
- * emits only the pose op and leaves `data` untouched. apps/draw wires this
3081
- * to mirror `data.path` through `transformPath`. Rotate intentionally never
3082
- * consults this seam (rotation lives on the pose, baked at render).
3083
- */
3084
- geometryProjection?: GeometryProjection;
3085
- }
3086
- /**
3087
- * Every dep name the registry knows about — derived from {@link DepSchema} so
3088
- * the two can't drift.
3089
- *
3090
- * Declared here rather than beside the registry so that this `keyof` reference
3091
- * resolves to the exported `DepSchema` declaration; from another module it
3092
- * resolves to that module's import alias, which the API docs can't link.
3093
- */
3094
- type DepName = keyof DepSchema;
3095
-
3096
- /** Holds the live sources an action's declared dependencies resolve to.
3097
- * Sources are thunks, read at invocation time, so an action never captures
3098
- * stale state. */
3099
- interface DepRegistry {
3100
- register<K extends DepName>(name: K, source: () => DepSchema[K]): () => void;
3101
- get<K extends DepName>(name: K): DepSchema[K] | undefined;
3102
- }
3103
- /** Provides the dep registry for a canvas. `<SceneCanvas>` mounts one; a
3104
- * consumer registering its own dep sources must be inside it. */
3105
- declare function DepRegistryProvider({ children }: {
3106
- children: ReactNode;
3107
- }): react_jsx_runtime.JSX.Element;
3108
- /** The dep registry in scope. Throws outside a `<DepRegistryProvider>`. */
3109
- declare function useDepRegistry(): DepRegistry;
3110
- /**
3111
- * Like `useDepRegistry`, but returns `null` when no `<DepRegistryProvider>` is
3112
- * in scope instead of throwing. Used by `useStandardActions` to preserve its
3113
- * silent-no-op contract when neither provider is present.
3114
- */
3115
- declare function useOptionalDepRegistry(): DepRegistry | null;
3116
- /** Register a live source for `name` for the lifetime of the calling
3117
- * component. The `source` thunk is called at dispatch time and should
3118
- * return the latest value. */
3119
- declare function useDepSource<K extends DepName>(name: K, source: () => DepSchema[K]): void;
3120
-
3121
- /**
3122
- * When an entry's bindings are live. A set, not one value: the hand tool is
3123
- * palette-selectable AND engaged by holding space, and both hold at once.
3124
- */
3125
- interface Eligibility {
3126
- /** Selectable as the focused entry — exclusive, one at a time. */
3127
- focus?: boolean;
3128
- /** Also live while this key is held. */
3129
- offhand?: HotkeyTrigger;
3130
- /** Live regardless of what is focused. */
3131
- always?: boolean;
3132
- /** Live only for input this entry's own affordances produced. */
3133
- claimed?: boolean;
3134
- /** Modality filter, applied wherever it would otherwise be live. */
3135
- capabilities?: CapabilityTag[];
3136
- }
3137
- /**
3138
- * Where an entry's overlay sits in the layer stack, relative to the
3139
- * selection chrome. `'top'` is the default and renders above everything;
3140
- * the other two exist for chrome that belongs under the selection handles
3141
- * (a snap-target highlight, say). With no selection overlay in the stack,
3142
- * all three collapse to `'top'`.
3143
- */
3144
- type OverlayPosition = 'top' | 'before-selection' | 'after-selection';
3145
- /**
3146
- * A registry entry: what it contributes, and when it is eligible. Every role
3147
- * is optional and independent — an entry that only routes input declares only
3148
- * `bindings` and `actions`.
3149
- */
3150
- interface Contribution {
3151
- id: string;
3152
- eligibility: Eligibility;
3153
- bindings?: GestureBinding[];
3154
- actions?: Action[];
3155
- /** One layer, or several composed in the given order. */
3156
- overlay?: RenderLayer<unknown> | RenderLayer<unknown>[];
3157
- /** Defaults to `'top'`. Applies to every layer in `overlay`. */
3158
- overlayPosition?: OverlayPosition;
3159
- presentation?: ToolPresentation;
3160
- /** Reflection escape hatch — the authored form, when there was one. */
3161
- def?: unknown;
3162
- }
3163
-
3164
- /**
3165
- * Configurable activation-key descriptor for tools that expose their
3166
- * keybinding to the host (currently Lasso and Eyedropper). Captures
3167
- * only the fields meaningful to a caller-supplied tool-select key —
3168
- * dispatcher-internal fields (`skipInEditable`, `enabled`,
3169
- * `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
3170
- * not part of the configurable surface.
3171
- */
3172
- interface ToolKeybinding {
3173
- /** Key or list of keys to match (case-insensitive against `event.key`). */
3174
- key: string | readonly string[];
3175
- /** Require Cmd (mac) / Ctrl (others). Default `false`. */
3176
- mod?: boolean;
3177
- /** Require Alt. Default `false`. */
3178
- alt?: boolean;
3179
- /**
3180
- * Shift policy. `undefined`/`false` forbids shift, `true` requires
3181
- * shift, `'optional'` allows either.
3182
- */
3183
- shift?: boolean | 'optional';
3184
- }
3185
- /**
3186
- * What a tool declares.
3187
- *
3188
- * A tool is a declarative shell, not an event handler: it names the gestures
3189
- * it responds to and, for each, the action to invoke. The dispatcher owns the
3190
- * gesture, and the action owns the preview and the commit — so a tool
3191
- * definition is mostly `bindings`, plus presentation and any actions the tool
3192
- * itself introduces.
3193
- */
3194
- interface ToolDef<TScratch = void> {
3195
- id: string;
3196
- /** Capability tags for modality eligibility. Forwarded onto `Tool.capabilities`. */
3197
- capabilities?: CapabilityTag[];
3198
- /**
3199
- * Actions this tool owns and needs registered while it is in the tools
3200
- * registry — e.g. polygon's `polygon.adjustSides`, which its own bindings
3201
- * reference by id.
3202
- *
3203
- * Declared here rather than registered by the hook with `useAction`,
3204
- * because tool hooks run wherever the consumer calls them — for
3205
- * `<SceneCanvas>` that is ABOVE `<ActionsProviderIfRoot>`, where
3206
- * `useActionsRegistry()` returns null and `useAction` silently no-ops. The
3207
- * result was a binding pointing at an action id nothing had registered, so
3208
- * the gesture fell through to whatever matched next (polygon's
3209
- * wheel/arrow-key side adjustment did nothing and `nudge.*` moved the
3210
- * selection instead). `<ToolActionsMounter>` registers these from inside
3211
- * the provider.
3212
- */
3213
- actions?: Action[];
3214
- /** Hook name as exported from the kit barrel (e.g. `'useHandTool'`).
3215
- * Set by built-in hooks for inspector / debugging. Consumer-authored
3216
- * tools may set this to surface their hook name; omitted is fine.
3217
- * Introspection-only — do not make this load-bearing in production.
3218
- * Read off the def via `Tool.def` (the reflection escape hatch). */
3219
- hookName?: string;
3220
- presentation?: ToolPresentation<TScratch>;
3221
- /** Optional caller-supplied activation key. Most built-in tools have their
3222
- * activation key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`;
3223
- * this field is for tools that want their activation key to be
3224
- * configurable by the host (currently Lasso and Eyedropper). The dynamic
3225
- * loop in `useKeybindings.ts` picks this up and appends a binding entry
3226
- * to the consolidated `tool.activate` action (with `opts.params.toolId`
3227
- * set so the invoker knows which tool to switch to). */
3228
- keybinding?: ToolKeybinding;
3229
- /** Held-key trigger: this tool engages while the key is down and
3230
- * disengages on release. Carried onto `Tool.eligibility.offhand`, which
3231
- * assembly reads to register the consolidated `tool.offhand` action — the
3232
- * declaration is the wiring, with nothing for the host to do. */
3233
- hotkey?: HotkeyTrigger;
3234
- onActivate?: (ctx: ToolCtx<TScratch>) => void;
3235
- onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
3236
- cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
3237
- /** Override the default scratch initializer. Default is `() => null`
3238
- * cast to `TScratch`, which works for tools whose scratch is fresh
3239
- * every gesture. Tools that need scratch identity to survive across
3240
- * gesture boundaries (e.g. the pen tool's multi-click subpath state)
3241
- * pass a stable-ref-returning thunk here. The factory forwards this
3242
- * onto the returned `Tool.initScratch`. */
3243
- initScratch?: () => TScratch;
3244
- /** Declarative gesture bindings, forwarded onto `Tool.bindings`. The
3245
- * gesture dispatcher consults these at active scope while this tool is the
3246
- * active one, and at hotkey scope while it is held. This is the tool's
3247
- * entire input surface — the `initial` / `engaged` phase tables that used
3248
- * to sit beside it are gone, along with the second dispatcher that read
3249
- * them. */
3250
- bindings?: GestureBinding[];
3251
- /** Optional overlay layer rendered while the tool occupies any slot
3252
- * (active, hotkey, or ambient), surfaced on `Tool.overlay`.
3253
- *
3254
- * The layer's `draw` closure should read dynamic state through refs or
3255
- * closures captured in the enclosing render scope, and gate on it there —
3256
- * `if (!scratch.something) return []` — rather than expecting the kit to
3257
- * swap layers as the gesture progresses.
3258
- *
3259
- * Pass an array to contribute several layers; they render in order. */
3260
- overlay?: RenderLayer<unknown> | RenderLayer<unknown>[];
3261
- /** Where `overlay` sits relative to the selection chrome. Defaults to
3262
- * `'top'`, above everything. */
3263
- overlayPosition?: OverlayPosition;
3264
- }
3265
- /** Viewport-tool spec. Once phase tables went away this stopped differing
3266
- * from `ToolDef` in any structural way; `defineViewportTool` survives as the
3267
- * authoring signal that a tool pans/zooms the view rather than the scene. */
3268
- type ViewportToolDef<TScratch = void> = ToolDef<TScratch>;
3269
-
3270
- /** Modifier-key snapshot at event dispatch time. */
3271
- interface ToolModifiers {
3272
- alt: boolean;
3273
- shift: boolean;
3274
- meta: boolean;
3275
- ctrl: boolean;
3276
- }
3277
- /** Per-event context passed to every channel handler. `scratch` is typed
3278
- * via the tool's `TScratch` parameter; it survives across a single
3279
- * gesture (pointer-down through end/cancel) and is replaced on next
3280
- * gesture start by `initScratch()`. */
3281
- interface ToolCtx<TScratch = unknown> {
3282
- worldX: number;
3283
- worldY: number;
3284
- modifiers: ToolModifiers;
3285
- selection: SelectionApi;
3286
- /** Adapter/scene access — opaque at this layer; tools that need it
3287
- * cast to a known shape. This layer doesn't constrain it. */
3288
- adapter: unknown;
3289
- applyOps: (ops: Op[], label: string) => void;
3290
- /** Current viewport. Reflects camera-position semantics — see
3291
- * `View` JSDoc. */
3292
- view: View;
3293
- /** Mutate the viewport. In controlled mode this calls the consumer's
3294
- * `onViewChange`; in uncontrolled mode it updates Canvas's internal
3295
- * state. View changes are not undoable. */
3296
- setView: (next: View) => void;
3297
- /** Bounding rect of the canvas element in viewport coords. Used by
3298
- * zoom/pan tools to convert event clientX/clientY to canvas-relative
3299
- * anchors. */
3300
- canvasRect: DOMRect;
3301
- /** Screen-space pointer coords relative to `canvasRect`. Useful for
3302
- * viewport tools that pan/zoom in screen space (e.g. hand-pan
3303
- * computes deltas in pixels, not world units). Optional — populated
3304
- * by the dispatcher on pointer events; absent on keyboard events. */
3305
- screenPoint?: {
3306
- x: number;
3307
- y: number;
3308
- };
3309
- /** Optional debug sink. When `<Canvas debug={...}>` is enabled, Canvas
3310
- * threads its sink here so tool-internal hit math (handle hitboxes,
3311
- * rotation handle, etc.) lands in the same overlay as Canvas's own
3312
- * bounds/origin records. Tools should call this conditionally with `?.`. */
3313
- debug?: DebugSink;
3314
- scratch: TScratch;
3315
- }
3316
- /** Hotkey-slot trigger key. The slot is engaged while this key is held —
3317
- * hence "hotkey": active as long as the key is hot. `null` (or omitted)
3318
- * means the tool is not eligible for the hotkey slot. */
3319
- type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift';
3320
- /** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
3321
- * `Bounds` type — the optional `rotation` field carries through so a tool
3322
- * can report an oriented preview rect (e.g. mid-rotate). */
3323
- type ToolBounds = Bounds;
3324
- /** Presentation metadata for tool palettes / menus. Optional on every
3325
- * tool — consumers that render a palette (`<ToolPalette>`) read these
3326
- * fields to display the tool; consumers that don't can ignore them.
3327
- *
3328
- * Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
3329
- * is already plumbed through `<Canvas>` to `style.cursor` on the host. */
3330
- interface ToolPresentation<TScratch = unknown> {
3331
- /** Human-readable label, distinct from the `id`. Falls back to `id`. */
3332
- label?: string;
3333
- /** Inline-SVG icon component output. May be a static `ReactNode` or a
3334
- * function of scratch state (rare; useful for shape-aware affordances). */
3335
- icon?: React.ReactNode | ((scratch?: TScratch) => React.ReactNode);
3336
- /** Palette grouping key. Tools sharing a group render contiguously
3337
- * with separators between groups. Free-form string; the kit
3338
- * recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
3339
- group?: string;
3340
- /** Display override for the keyboard shortcut. When omitted the palette
3341
- * derives one from `Tool.keybinding` via its own formatter. */
3342
- shortcut?: string;
3343
- }
3344
- /**
3345
- * The focus-declaring case of a `Contribution`: a mode the user switches
3346
- * into, plus the hooks that only make sense for one (`initScratch`,
3347
- * activate/deactivate, live preview, `cursor`). Everything else — bindings,
3348
- * actions, overlay, presentation — is inherited.
3349
- */
3350
- interface Tool<TScratch = unknown> extends Contribution {
3351
- /** Optional caller-supplied key. Most built-in tools have their activation
3352
- * key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
3353
- * is for tools that want their activation key to be configurable by the
3354
- * host (currently Lasso and Eyedropper). The dynamic loop in
3355
- * `useKeybindings.ts` picks this up and appends a binding entry to the
3356
- * consolidated `tool.activate` action (with `opts.params.toolId` set so
3357
- * the invoker knows which tool to switch to). */
3358
- keybinding?: ToolKeybinding;
3359
- initScratch?: () => TScratch;
3360
- cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
3361
- onActivate?: (ctx: ToolCtx<TScratch>) => void;
3362
- onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
3363
- /** Returns the in-flight preview pose for `id` if this tool is mid-gesture
3364
- * on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
3365
- * reflect live gesture state without reaching into hook internals. The
3366
- * return type is `unknown` here because the Tool interface is pose-agnostic;
3367
- * callers that know the pose shape (e.g. Canvas typed by `TPose`) cast at
3368
- * the use site. */
3369
- previewPose?: (id: string) => unknown;
3370
- /** Returns the in-flight preview bounds for `id` if this tool is mid-gesture
3371
- * on it; otherwise `null`. Optional companion to `previewPose` for tools that
3372
- * can compute bounds without round-tripping through a geometry adapter. */
3373
- previewBounds?: (id: string) => ToolBounds | null;
3374
- /** Returns ids whose committed scene-render should be suppressed while this
3375
- * tool is mid-gesture (e.g. cascade move's dragged + descendant ids whose
3376
- * preview ghosts replace the committed pose). The standard scene slot
3377
- * consults this alongside `previewPose` to avoid double-rendering. Returns
3378
- * `null` when no gesture is in flight. */
3379
- previewIds?: () => Iterable<string> | null;
3380
- }
3381
- /** Internal — which slot a tool occupies in the dispatch order. */
3382
- type ToolSlot = 'hotkey' | 'active' | 'ambient';
3383
- /** Internal alias for "a Tool of any scratch type" — used in registries and
3384
- * dispatchers that hold tools of heterogeneous scratch shapes. `any` is
3385
- * intentional: `Tool<TScratch>` is invariant in TScratch, so `Tool<unknown>`
3386
- * is too strict for containers that accept any concrete `Tool<T>`. */
3387
- type AnyTool = Tool<any>;
3388
-
3389
- /**
3390
- * Pure matcher primitives live in `@weasel-js/gestures`. This file
3391
- * re-exports them for kit-internal consumers and layers the actions-layer
3392
- * binding-scope / matchBest logic on top.
3393
- */
3394
-
3395
- /** Where a binding came from, which is also its priority: a held hotkey beats
3396
- * the active tool, which beats bindings that are always in scope. */
3397
- type BindingScope = 'ambient' | 'active' | 'hotkey';
3398
- /** A binding paired with where it came from, ready to be matched against an
3399
- * event. */
3400
- interface ScopedBinding {
3401
- binding: GestureBinding;
3402
- scope: BindingScope;
3403
- /** Tool id that owns this binding — `'&'`-channel phase atoms resolve
3404
- * to this. `null` for ambient bindings that came from a registered
3405
- * Action with no owning tool. */
3406
- ownerToolId: string | null;
3407
- }
3408
- /** The binding that won a match. */
3409
- interface MatchResult {
3410
- binding: GestureBinding;
3411
- scope: BindingScope;
3412
- /** Tool id that owns the binding — propagated from `ScopedBinding`
3413
- * so the dispatcher can record it as the handle owner. */
3414
- ownerToolId: string | null;
3415
- }
3416
- /** CSS-style specificity tuple for a GestureSpec. Higher tuple wins under
3417
- * lexicographic compare. Dimensions, in order of precedence:
3418
- *
3419
- * [0] target — how much the spec's target narrows; see `targetRank`.
3420
- * [1] mods — count of required modifier keys (shift/alt/ctrl/meta/mod).
3421
- * `'optional'` does NOT count.
3422
- * [2] phase — how much the spec's `phase` narrows; see `phaseRank`.
3423
- * [3] exact — per-kind tiebreak: 2 for a drop/paste spec with a
3424
- * non-empty `types` MIME filter, else 1.
3425
- *
3426
- * Identical tuples fall back to registration order in the matcher's
3427
- * stable sort, preserving the pre-specificity tiebreaker. */
3428
- declare function specificity(spec: GestureSpec): readonly [number, number, number, number];
3429
-
3430
- /**
3431
- * Dispatcher orchestrator — pure module, no React, no DOM.
3432
- *
3433
- * Assembles `ScopedBinding[]` from the actions registry, active tool, and
3434
- * hotkey stack; matches input events via `matchSorted`; gates each candidate
3435
- * on `enabled()`; then invokes `immediate` or `ongoing` invokers and tracks
3436
- * in-flight handles.
3437
- *
3438
- * ## Specificity-ordered fall-through
3439
- * `matchSorted` returns every matching binding in precedence order
3440
- * (hotkey > active > ambient, first-declared within scope). The dispatcher
3441
- * walks that list and fires the first action whose `enabled()` returns
3442
- * `true`. If every candidate's `enabled()` returns a disabled reason, the
3443
- * event is unhandled. This mirrors CSS-style specificity matching with a
3444
- * `:not(:disabled)` filter, and lets a tool declare a high-specificity
3445
- * binding (e.g. drag-on-empty → areaSelect) that gracefully falls through
3446
- * to a lower-specificity ambient binding (e.g. drag → viewport.dragPan)
3447
- * when its required deps aren't wired.
3448
- *
3449
- * ## gestureId scheme
3450
- * - `key-held` ongoing actions: `key-held-<key>` (e.g. `key-held- ` for Space).
3451
- * Chosen because key-held gestures are identified by the held key alone.
3452
- * - `pointerdown` / drag ongoing actions: `pointer-<pointerId>`, taken from
3453
- * the originating DOM `PointerEvent`. Each physical pointer — mouse, each
3454
- * touch, the stylus — gets its own handle slot. Events with no
3455
- * `pointerId` (synthesized probes, programmatic drags, most tests) key to
3456
- * `pointer-mouse`, so a single synthetic pointer behaves as it always has.
3457
- * - `multitouch` ongoing actions: `multitouch-<fingers>`.
3458
- * - Fallback for any other kind that triggers an ongoing invoker: `ongoing-<kind>`.
3459
- *
3460
- * ## Action-lookup miss behavior
3461
- * When `matchBest` resolves a binding whose `actionId` has no entry in
3462
- * `ctx.actions.list()`, the dispatcher emits `console.warn` and returns
3463
- * `'unhandled'`. The user's input gesture falls through as if unmatched.
3464
- * This preserves input flow (nothing is swallowed silently) while flagging
3465
- * the misconfiguration at dev time.
3466
- */
3467
-
3468
- /** Everything the dispatcher must consult to route one input event: the
3469
- * registered actions and their deps, which tool is active, which hotkeys are
3470
- * held, and — optionally — the chrome state that action eligibility rules are
3471
- * evaluated against. Rebuilt per event rather than held, so the dispatcher
3472
- * itself stays stateless apart from in-flight gestures. */
3473
- interface DispatcherContext {
3474
- /** All registered actions; the dispatcher walks `.defaultBinding` for ambient bindings. */
3475
- actions: ActionsRegistry;
3476
- /** Dep sources keyed by name. */
3477
- depRegistry: DepRegistry;
3478
- /** Active tool's id (from ActiveToolContext). */
3479
- activeToolId: string;
3480
- /** Held-hotkey stack, top of stack last. */
3481
- hotkeyStack: readonly string[];
3482
- /** Lookup for tool definitions. */
3483
- toolsById: ReadonlyMap<string, Tool>;
3484
- /** Platform flag for `mod` shorthand resolution. */
3485
- isMac: boolean;
3486
- /**
3487
- * Thunk returning a fresh `RuleCtx` for the current frame — the routed
3488
- * view's, when the surface hosts several. When it answers, the dispatcher
3489
- * filters matched candidates by their declared `Action.eligible` rule
3490
- * (omitted => always eligible). Absent, or answering `undefined`, applies
3491
- * no eligibility filtering.
3492
- */
3493
- getRuleCtx?: () => RuleCtx | undefined;
3494
- }
3495
- /**
3496
- * Handle returned by `Dispatcher.beginUiOngoing()` for driving an
3497
- * ongoing invoker from a UI control (color picker, slider).
3498
- *
3499
- * - `update(params)` rebuilds an `InvocationCtx` with the new params and
3500
- * calls the handle's `onMove`. Safe to call many times.
3501
- * - `end(reason)` calls `onEnd(ctx, reason)` once and removes the handle
3502
- * from the in-flight map. Idempotent — further calls are no-ops.
3503
- */
3504
- interface UiOngoingControl {
3505
- readonly gestureId: string;
3506
- update(params?: Record<string, unknown>): void;
3507
- end(reason: 'commit' | 'cancel'): void;
3508
- }
3509
- /**
3510
- * Successful `Dispatcher.resolveOnly` prediction: the binding + action that
3511
- * would fire if `event` were dispatched for real. `action` is the resolved
3512
- * descriptor so callers (the hover-cursor pump) can read metadata like
3513
- * `Action.cursor` without a second registry lookup.
3514
- */
3515
- interface ResolveOnlyResult {
3516
- actionId: string;
3517
- action: Action;
3518
- scope: BindingScope;
3519
- /** Tool id owning the winning binding; `null` for ambient action bindings. */
3520
- ownerToolId: string | null;
3521
- }
3522
- /**
3523
- * One candidate from `Dispatcher.resolveAll` — a binding that matched the
3524
- * event, with why it did or didn't get to fire.
3525
- *
3526
- * Verdicts:
3527
- * - `would-fire` — eligible, `enabled()` passed, and nothing above it fired.
3528
- * At most one candidate per call carries this.
3529
- * - `ineligible` — the action's `eligible` rule evaluated false against the
3530
- * live `RuleCtx`. `reason` is the rule, serialized.
3531
- * - `disabled` — `enabled()` returned a disabled reason, carried verbatim.
3532
- * - `shadowed` — never asked, for one of two reasons: something above it
3533
- * already fired, or it is a repeat binding of the action that itself won
3534
- * higher in the list (several bindings may point at one action, and the
3535
- * dispatcher runs each action at most once). A repeat of an action that was
3536
- * already judged `ineligible` or `disabled` is NOT shadowed — it inherits
3537
- * that action's verdict, since that is the reason it doesn't fire.
3538
- */
3539
- interface ResolvedCandidate {
3540
- actionId: string;
3541
- action: Action;
3542
- binding: GestureBinding;
3543
- scope: BindingScope;
3544
- ownerToolId: string | null;
3545
- /** The tuple from `specificity(binding.spec)`, surfaced so a reader can see
3546
- * why one candidate outranks another rather than inferring it. */
3547
- specificity: readonly [number, number, number, number];
3548
- verdict: {
3549
- kind: 'would-fire';
3550
- } | {
3551
- kind: 'ineligible';
3552
- reason: string;
3553
- } | {
3554
- kind: 'disabled';
3555
- reason: string;
3556
- } | {
3557
- kind: 'shadowed';
3558
- };
3559
- }
3560
- /** Options for {@link Dispatcher.resolveAll}. */
3561
- interface ResolveAllOptions {
3562
- /**
3563
- * Evaluate eligibility and `enabled()` for candidates below the winner
3564
- * instead of short-circuiting them to `shadowed`.
3565
- *
3566
- * Off by default, and deliberately so: the default walk's early exit is what
3567
- * keeps `resolveOnly`'s `enabled()` call count identical to a real dispatch,
3568
- * and `enabled()` predicates are only contractually pure — not free.
3569
- *
3570
- * Turn it on for diagnostics, where "this one was outranked" is a less
3571
- * useful answer than "this one was outranked AND would have been disabled
3572
- * anyway". With it on, `shadowed` narrows to its precise meaning: this
3573
- * candidate would have fired, but something above it did.
3574
- */
3575
- evaluateShadowed?: boolean;
3576
- }
3577
- /**
3578
- * Routes input events to actions.
3579
- *
3580
- * For each event it assembles the bindings in scope (hotkey, then active tool,
3581
- * then ambient), matches them in specificity order, filters by eligibility and
3582
- * each action's `enabled` gate, and invokes the first survivor. Ongoing
3583
- * actions — anything that runs across a drag — are kept in flight here and
3584
- * pumped with subsequent moves until the gesture ends.
3585
- */
3586
- interface Dispatcher {
3587
- /**
3588
- * Route an input event through the binding pipeline. Returns `'handled'`
3589
- * when a binding matched and the action invoked successfully (whether it
3590
- * returned ops or not). Returns `'unhandled'` when no binding matched or
3591
- * the matched action's `enabled()` returned a disabled reason.
3592
- */
3593
- handleInput(event: InputEvent, ctx: DispatcherContext): 'handled' | 'unhandled';
3594
- /**
3595
- * Predict which action `event` would route to WITHOUT invoking it. Replays
3596
- * the same walk as `handleInput` — scope assembly, specificity-sorted
3597
- * match, eligibility filter, per-candidate `enabled()` gate — and returns
3598
- * the first candidate that would fire, or `null` when the event would go
3599
- * unhandled. Pure query: no invoker runs, no in-flight state changes, no
3600
- * trace-log entry.
3601
- *
3602
- * Known divergence from a real dispatch: an ongoing invoker that matches
3603
- * but returns an empty handle at `start()` (runtime bail) makes the real
3604
- * dispatch fall through to the next candidate; prediction cannot see that
3605
- * and reports the bailing action. Keep `enabled()` accurate on actions
3606
- * that rely on prediction (hover cursors).
3607
- */
3608
- resolveOnly(event: InputEvent, ctx: DispatcherContext): ResolveOnlyResult | null;
3609
- /**
3610
- * Every binding that matches `event`, in dispatch precedence order, each
3611
- * with a verdict explaining whether it would fire. Same walk as
3612
- * `resolveOnly` — scope assembly, specificity-sorted match, eligibility
3613
- * check, per-candidate `enabled()` gate — without stopping at the winner
3614
- * and without invoking anything. Nothing is dropped: candidates that
3615
- * `resolveOnly`'s walk would filter out are kept here and labelled
3616
- * `ineligible` instead. Pure query: no invoker runs, no in-flight state
3617
- * changes, no trace-log entry.
3618
- *
3619
- * `resolveOnly` is the first `would-fire` entry of this list.
3620
- *
3621
- * Shares `resolveOnly`'s known divergence from a real dispatch: an ongoing
3622
- * invoker that matches but returns an empty handle at `start()` makes the
3623
- * real dispatch fall through, and this cannot see that.
3624
- *
3625
- * By default everything below the winner is `shadowed` without being asked,
3626
- * which is what keeps this walk as cheap as the dispatch it replays. Pass
3627
- * `{ evaluateShadowed: true }` to keep evaluating past the winner, so a
3628
- * lower candidate that is ALSO ineligible or disabled says so — see
3629
- * {@link ResolveAllOptions.evaluateShadowed}.
3630
- */
3631
- resolveAll(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): ResolvedCandidate[];
3632
- /**
3633
- * Synthesize an end-of-gesture for every in-flight ongoing handle.
3634
- * Used by tool-switch cancellation (Q2 decision).
3635
- */
3636
- cancelAll(reason: 'commit' | 'cancel'): void;
3637
- /**
3638
- * Read-only view of currently in-flight ongoing handles, keyed by gestureId.
3639
- * For debug/testing.
3640
- */
3641
- inFlight(): ReadonlyMap<string, OngoingHandle>;
3642
- /**
3643
- * CSS cursor for the gesture currently in flight, or `null` when nothing
3644
- * is. Reads `Action.activeCursor` (falling back to `Action.cursor`) off the
3645
- * action whose handle is open — the hover pump applies this instead of its
3646
- * prediction once a gesture starts, which is how `grab` becomes `grabbing`.
3647
- */
3648
- inFlightCursor(): string | null;
3649
- /**
3650
- * Read-only iterator over currently in-flight `OngoingHandle` instances.
3651
- *
3652
- * Surface for the canvas's preview-ghost layer (`usePreviewGhostLayer`)
3653
- * to walk each handle's `previewIds()` / `previewPose(id)` and render
3654
- * dispatcher-driven gesture previews. Read-only by design:
3655
- * external consumers must not mutate the in-flight map.
3656
- */
3657
- getInFlightHandles(): Iterable<OngoingHandle>;
3658
- /**
3659
- * Subscribe to in-flight state changes. The callback fires after every
3660
- * mutation that affects what the preview-ghost / dispatcher-overlay
3661
- * layers read — handle start, every `onMove` pump, end, cancel,
3662
- * cancel-all. Consumers re-read `getInFlightHandles()` and re-render.
3663
- *
3664
- * Returns an unsubscribe function.
3665
- */
3666
- subscribe(fn: () => void): () => void;
3667
- /**
3668
- * Monotonic counter bumped on exactly the events {@link subscribe} fires
3669
- * on. The snapshot half of the `useSyncExternalStore` contract: pair it
3670
- * with `subscribe` to drive a render off in-flight gesture state without
3671
- * a `useReducer` force-rerender.
3672
- *
3673
- * Starts at 0 and only ever increases. Two reads returning the same number
3674
- * mean nothing pumped in between; it does **not** guarantee that a bump
3675
- * changed anything observable (a pump that matched no binding still
3676
- * counts — see `subscribe`).
3677
- */
3678
- getVersion(): number;
3679
- /**
3680
- * Snapshot of the currently active action, for surfaces (chrome-caps
3681
- * visibility rules, debug HUDs) that need to react to "what action
3682
- * is in flight right now."
3683
- *
3684
- * - `kind` — the `OngoingHandle.kind` reported by the in-flight
3685
- * handle (e.g. `'marquee'`, `'move'`). `null` when no action is
3686
- * in flight OR the handle didn't declare a kind.
3687
- * - `id` — the dispatcher's internal `gestureId` (`pointer-1`,
3688
- * `key-held-Space`, …) — the pointer/key channel the action rode
3689
- * in on. `null` when no action is in flight.
3690
- *
3691
- * When multiple handles are in flight simultaneously (e.g. a key-held
3692
- * action overlapping a pointer action), the most-recently-started
3693
- * handle wins. This matches user intent: the latest interaction is
3694
- * the one consumers care about.
3695
- *
3696
- * That rule used to be near-vacuous on the pointer side, because every
3697
- * pointer shared one handle slot and two pointer drags could not coexist.
3698
- * With per-pointer keying they can — but only via paths that bypass the
3699
- * multi-pointer policy in `useGestureDispatcher` (which stops a second
3700
- * finger from opening a drag while a pinch is live), such as a mouse and
3701
- * a pen used together. Latest-start remains the right answer there.
3702
- */
3703
- getActiveAction(): {
3704
- kind: string | null;
3705
- id: string | null;
3706
- };
3707
- /**
3708
- * Start an ongoing action driven by UI (not a gesture). Builds an
3709
- * `InvocationCtx` with the given `deps` and `params`, calls
3710
- * `action.invoker.start(ctx, { params })`, and registers the returned
3711
- * handle in the in-flight map so `getInFlightHandles()` reports it —
3712
- * enabling preview rendering via `SceneCanvas`.
3713
- *
3714
- * Returns `null` if `actionId` is unknown, the action's invoker is not
3715
- * ongoing, or `start` returned an empty handle.
3716
- *
3717
- * If a UI-driven handle for the same `actionId` is already in flight,
3718
- * it is committed (`end('commit')`) before the new one starts.
3719
- */
3720
- beginUiOngoing(actionId: string, deps: ActionDeps, params?: Record<string, unknown>): UiOngoingControl | null;
3721
- }
3722
- /** Build a dispatcher. `getAction` overrides how action ids are resolved;
3723
- * by default the `DispatcherContext`'s registry is used. */
3724
- declare function createDispatcher(opts?: {
3725
- getAction?: (id: string) => Action | undefined;
3726
- }): Dispatcher;
3727
-
3728
- /**
3729
- * @experimental
3730
- * A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
3731
- * (no per-binding opts) or an object form that pairs a spec with
3732
- * `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
3733
- * Use the object form when two bindings for the same action differ only in
3734
- * a runtime parameter — the dispatcher extracts `opts.params` and passes
3735
- * them to `ImmediateInvoker.run` as its second argument.
3736
- */
3737
- type BoundGesture = GestureSpec | {
3738
- spec: GestureSpec;
3739
- opts: BindingOpts;
3740
- };
3741
- /**
3742
- * @experimental
3743
- * Flatten an action's `defaultBinding` into `GestureBinding`s. A bare
3744
- * `GestureSpec` has `kind` at top level; the object form has `spec`.
3745
- */
3746
- declare function actionBindings(action: Action): GestureBinding[];
3747
- /**
3748
- * @experimental
3749
- * Single registered action. v1: one binding per action.
3750
- */
3751
- interface Action {
3752
- id: string;
3753
- label: string;
3754
- /** The gesture-spec form of the binding, read by the gesture dispatcher.
3755
- * May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
3756
- * or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
3757
- * `{ spec, opts }` — use the object form for parametric actions where two
3758
- * bindings for the same action differ only by `opts.params` (e.g. `flip`
3759
- * with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
3760
- * passes them to `ImmediateInvoker.run` as its second argument. */
3761
- defaultBinding?: GestureSpec | BoundGesture[];
3762
- /** Names of the deps this action's invoker reads (keys of `DepSchema`).
3763
- * The dispatcher (and `trigger`, when `requires` is present) resolves
3764
- * each name against the `DepRegistry` at invocation time and passes the
3765
- * resulting bag to the invoker. Dev builds warn when the invoker reads a
3766
- * dep it didn't declare here — see `buildDepsFromRequires`. */
3767
- requires?: readonly DepName[];
3768
- /** Inline-SVG icon for palette / toolbar surfaces. Mirrors
3769
- * `ToolPresentation.icon` so a generic `<ActionBar>` can render from
3770
- * action metadata the same way `<ToolPalette>` renders from tool
3771
- * metadata. May be a static `ReactNode` or a function (rare; useful
3772
- * for state-aware icons like a "lock" toggle). */
3773
- icon?: ReactNode | (() => ReactNode);
3774
- /** Grouping key for palette/menu surfaces. Free-form string; the kit
3775
- * ships defaults for `'align'` (six edges/centers), `'distribute'`
3776
- * (two axes), and recommends `'pathfinder'` for boolean ops. */
3777
- group?: string;
3778
- /** Display override for the keyboard shortcut. When omitted, palette
3779
- * surfaces derive a label from `defaultBinding` via their own
3780
- * formatter. */
3781
- shortcut?: string;
3782
- /** Pluggable invocation strategy. The gesture dispatcher routes matched
3783
- * bindings through `invoker.start` / `invoker.run` depending on timing.
3784
- * All kit-standard descriptors ship one; consumer-supplied actions
3785
- * without an invoker can still register but won't be triggered. */
3786
- invoker?: Invoker;
3787
- /** When set to `'hotkey'`, this action's `defaultBinding` rides the hotkey
3788
- * `BindingScope` instead of the ambient scope — meaning it beats any
3789
- * active-tool binding on the same input shape. Use for tool-switch
3790
- * shortcuts and global held-key triggers. Default: ambient. */
3791
- scope?: 'hotkey';
3792
- /**
3793
- * @experimental
3794
- * Optional predicate the command palette consults when rendering. Return
3795
- * `true` when the action is currently triggerable. Return a reason string
3796
- * (e.g. `'Selection required'`) when disabled — the palette greys out
3797
- * the row, skips it in keyboard nav, ignores clicks, and shows the
3798
- * reason next to the label. Keystroke dispatch (the registered binding)
3799
- * is unaffected; the action's own `run` should self-guard.
3800
- *
3801
- * **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
3802
- * must not throw. If a call throws or exceeds the budget in dev mode,
3803
- * `evaluateEnabled` logs a one-time warning per action id; throws are
3804
- * caught and treated as disabled with reason `'(predicate threw)'`.
3805
- *
3806
- * Snapshot-on-open semantics: the palette evaluates `enabled` once when
3807
- * opened and does NOT re-evaluate on selection changes while open. Live
3808
- * reactive updates are deferred — palette is short-lived.
3809
- *
3810
- * The reason set is a closed enum — to add a new reason, edit
3811
- * `ActionDisabledReason` and the consumer's display map.
3812
- *
3813
- * The optional `deps` argument is the same bag passed to
3814
- * `ImmediateInvoker.run`; callers (`evaluateEnabled` / the ActionBar) may
3815
- * synthesize it from the surrounding `DepRegistry` so predicates can
3816
- * inspect selection / scene / etc. Predicates that don't need deps just
3817
- * ignore the arg.
3818
- */
3819
- enabled?: (deps?: ActionDeps) => true | ActionDisabledReason;
3820
- /**
3821
- * Declarative eligibility rule, evaluated against the current
3822
- * `RuleCtx` by the dispatcher before invoking `start()`. Omitted =
3823
- * always eligible.
3824
- *
3825
- * Accepts either a fluent `Condition` (callable with `.rule`) or a
3826
- * raw `Rule` tree; the dispatcher normalizes via `.rule` unwrap.
3827
- *
3828
- * Prefer `capability:`-based rules (e.g. `{ capability: 'transforms-selection' }`)
3829
- * over `mode:` rules — capability rules survive new modes being added
3830
- * that allow the same capability.
3831
- */
3832
- eligible?: Rule | Condition;
3833
- /**
3834
- * CSS cursor shown while the pointer hovers a spot where this action
3835
- * would win the drag. The hover-cursor pump (in `useGestureDispatcher`)
3836
- * runs `Dispatcher.resolveOnly` on each idle pointermove — the same
3837
- * match walk a real pointerdown takes — and applies the winning
3838
- * action's `cursor`, so the hint and the actual click target stay in
3839
- * sync by construction. Omitted = no override (the active tool's
3840
- * `Tool.cursor` shows). Affordance hits are resolved earlier in the
3841
- * pump via `AffordanceRegion.cursor` and never reach this field.
3842
- *
3843
- * Static value only. Prediction runs `enabled()` but cannot run the
3844
- * invoker, so an action that matches yet bails at `start()` (empty
3845
- * handle) may still show its cursor — keep `enabled` accurate for
3846
- * actions that declare one.
3847
- */
3848
- cursor?: CursorSpec;
3849
- /**
3850
- * CSS cursor shown while THIS action's ongoing handle is in flight —
3851
- * grabbing while panning, `move` while dragging a selection, `crosshair`
3852
- * while pulling a marquee.
3853
- *
3854
- * Separate from `cursor` because the two answer different questions:
3855
- * `cursor` is a prediction ("a drag from here would pan"), this is a state
3856
- * ("you are panning"). An action can declare either, both, or neither;
3857
- * with only `cursor` set, the hover hint holds for the duration of the
3858
- * gesture.
3859
- *
3860
- * This is where mid-gesture cursors live now. They used to come from the
3861
- * tool side — `ViewportToolDef.engaged.cursor` for a phase-gated string,
3862
- * or a function-form `Tool.cursor` reading the gesture scratch out of the
3863
- * tool-routing dispatcher. Both belonged to a pipeline whose whole job was
3864
- * being taken over by bindings, and neither could describe a cursor for an
3865
- * action a tool doesn't own.
3866
- */
3867
- activeCursor?: CursorSpec;
3868
- }
3869
- /**
3870
- * @experimental
3871
- * Closed enum of reasons an action might report itself as disabled. The
3872
- * consumer (palette, menu, etc.) maps these symbolic values to display
3873
- * strings via its own label map — see `demo/CommandPalette.tsx` for the
3874
- * canonical mapping.
3875
- */
3876
- declare const ActionDisabledReason: {
3877
- readonly SelectionRequired: "selection-required";
3878
- readonly SceneEmpty: "scene-empty";
3879
- readonly NotApplicable: "not-applicable";
3880
- /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
3881
- readonly PredicateThrew: "predicate-threw";
3882
- };
3883
- /** Why an action is unavailable right now. */
3884
- type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDisabledReason];
3885
- /**
3886
- * @experimental
3887
- * Result of evaluating an Action's `enabled` predicate.
3888
- */
3889
- interface ActionEnabledResult {
3890
- enabled: boolean;
3891
- reason?: ActionDisabledReason;
3892
- }
3893
- /**
3894
- * @experimental
3895
- * Safely evaluate an action's `enabled` predicate. Returns `{enabled: true}`
3896
- * when no predicate is supplied. Catches throws and treats them as disabled
3897
- * with reason `'(predicate threw)'`. In dev mode, warns once per action id
3898
- * when a single call exceeds 4ms (single frame at 240fps — generous; real
3899
- * predicates should be sub-millisecond).
3900
- */
3901
- declare function evaluateEnabled(action: Action, deps?: ActionDeps): ActionEnabledResult;
3902
- /**
3903
- * @experimental
3904
- * Partial override or full descriptor passed via `<SceneCanvas actions={...}>`.
3905
- * `null` disables a default at this id.
3906
- */
3907
- type ActionEntry = null | Partial<Action> | Action;
3908
- /**
3909
- * @experimental
3910
- * Shape of the `actions` prop on `<SceneCanvas>`. `null` disables all defaults.
3911
- */
3912
- type ActionsProp = null | Record<string, ActionEntry>;
3913
- /**
3914
- * @experimental
3915
- * Imperative API exposed by `useActionsRegistry()`.
3916
- */
3917
- interface ActionsRegistry {
3918
- /** Add an action and return its release. Registrants for one id stack,
3919
- * newest live, so releasing yours uncovers whoever you displaced. Call it
3920
- * from an effect and release it in that effect's cleanup — registering from
3921
- * a render body pushes an entry per render and releases none. */
3922
- register(action: Action): () => void;
3923
- /** Drop every registrant of `id`. This is the "this action should not exist"
3924
- * door, not a release — for that, call what `register` returned. */
3925
- unregister(id: string): void;
3926
- /** Declare `id` not for this scope: it stays registered, and every other
3927
- * scope over the same store still resolves it, but here it lists as absent
3928
- * and neither `trigger` nor `begin` will fire it. Returns a release; an
3929
- * `<ActionsScope>` drops what it muted when it unmounts. This is the
3930
- * "not for me" door — `unregister` is the "should not exist" one. */
3931
- mute(id: string): () => void;
3932
- list(): readonly Action[];
3933
- /** Fire an immediate-invoker action by id. The optional `params` arg is
3934
- * forwarded to `ImmediateInvoker.run` as its second argument — use it for
3935
- * parametric actions (e.g. `trigger('tool.activate', { toolId: 'rect' })`).
3936
- * Ongoing-invoker actions are not reachable from `trigger`. */
3937
- trigger(id: string, params?: Record<string, unknown>): boolean;
3938
- /**
3939
- * Subscribe to registry mutations. The callback fires after any
3940
- * `register`/`unregister` that changes the version. Returns an
3941
- * unsubscribe function. Designed for `useSyncExternalStore`-driven
3942
- * surfaces (e.g. `<ActionBar>` in `@weasel-js/ui`) that need to
3943
- * re-render when the action set changes.
3944
- */
3945
- subscribe(listener: () => void): () => void;
3946
- /**
3947
- * Start an ongoing action driven by UI (color picker, opacity slider).
3948
- * Returns a control object with `update(params)` and `end(reason)`.
3949
- *
3950
- * Returns `null` if no dispatcher is wired into this registry, the
3951
- * action is unknown, or its invoker is not ongoing.
3952
- *
3953
- * See `Dispatcher.beginUiOngoing` for full semantics including
3954
- * auto-commit when a prior UI handle for the same action is in flight.
3955
- */
3956
- begin(id: string, params?: Record<string, unknown>): UiOngoingControl | null;
3957
- /** Wire a dispatcher into the registry so `begin()` can delegate to it.
3958
- * Returns a release that clears the slot only while this dispatcher still
3959
- * holds it: a canvas displaced by a later one must not take input away from
3960
- * the canvas now on screen. Call with `null` to detach unconditionally. */
3961
- setDispatcher(d: Dispatcher | null): () => void;
3962
- /** Wire a `DepRegistry` into the registry so `trigger()` / `begin()` can
3963
- * resolve action deps even when this provider is mounted ABOVE the dep
3964
- * registry (e.g. a consumer's root `<ActionsProvider>` reused by
3965
- * SceneCanvas's `ActionsProviderIfRoot`). Takes precedence over the dep
3966
- * registry read from context at the provider's own level. Call with
3967
- * `null` to detach unconditionally; the returned release clears the slot
3968
- * only while this registry still holds it. */
3969
- setDepRegistry(r: DepRegistry | null): () => void;
3970
- }
3971
- /**
3972
- * @experimental
3973
- * A view of the registry in scope that can declare ids not for itself. Wrap
3974
- * anything that should be able to opt out of an action — a second
3975
- * `<SceneCanvas>` sharing the host's `<ActionsProvider>` mounts one — and its
3976
- * `mute` calls stay inside it. Renders nothing of its own.
3977
- *
3978
- * Not a `BindingScope`: that names the tier a binding matches at (hotkey /
3979
- * active / ambient), which this has nothing to do with.
3980
- */
3981
- declare function ActionsScope({ children }: {
3982
- children: ReactNode;
3983
- }): ReactElement;
3984
- /**
3985
- * @experimental
3986
- * Mounts an `ActionsRegistry` for its lifetime. Children call
3987
- * `useActionsRegistry()` or `useAction()` to participate. Mounts no input
3988
- * listener of its own — the gesture dispatcher owns input.
3989
- */
3990
- declare function ActionsProvider({ children }: {
3991
- children: ReactNode;
3992
- }): ReactElement;
3993
- declare function useActionsRegistry(): ActionsRegistry | null;
3994
- /**
3995
- * @experimental
3996
- * Register an `Action` for the lifetime of the calling component. No-op (with
3997
- * a dev-only warning) when no `ActionsProvider` is in scope. Re-registers on
3998
- * `action` reference change (consumers should memoize stable identities to
3999
- * avoid churn).
4000
- */
4001
- declare function useAction(action: Action): void;
4002
-
4003
- export { type Contribution as $, type Action as A, type BuiltinShapeToolId as B, type Condition as C, type Dims as D, type AffordanceBinding as E, type CommonAffordanceScratch as F, type GeometryProjection as G, type HotkeyTrigger as H, type InertiaConfig as I, ColorOverrideRegistry as J, type BooleansAdapter as K, type LayerHit as L, type UseAnimatorOptions as M, type SpringPresetName as N, type OverlayPosition as O, type PanBounds as P, type EasingSpec as Q, type RenderLayer as R, type SliceDep as S, type Tool as T, type UseSelectionOptions as U, type VisibilityRules as V, type AnimationHandle as W, type VertexColorChannel as X, type SampledTrack as Y, type Eligibility as Z, type BindingScope as _, type DeviceProfile as a, type PhysicsOptions as a$, type ScopedBinding as a0, ALWAYS as a1, type ActionDeps as a2, ActionDisabledReason as a3, type ActionEnabledResult as a4, type ActionEntry as a5, ActionsProvider as a6, ActionsScope as a7, ActiveToolContextProvider as a8, ActiveToolContextProviderIfRoot as a9, type EditAnchorsDep as aA, type EventTrack as aB, type GestureBinding as aC, IDENTITY_POSE_COMPOSITION as aD, type ImmediateInvoker as aE, type IngestCtx as aF, type IngestionDep as aG, type InsertDep as aH, type Interpolate as aI, type InterpolatorFactory as aJ, type InvocationCtx as aK, type Invoker as aL, KIT_SHAPE_KINDS as aM, type Keyframe as aN, type LassoSelectDep as aO, type LayerCommandCache as aP, type LayerDrawFailure as aQ, type LayoutDep as aR, type LoopFactory as aS, type LoopOptions as aT, type MatchResult as aU, NEVER as aV, type NestedTimeline as aW, type NodeAtPointDep as aX, type OngoingHandle as aY, type OngoingInvoker as aZ, type PhysicsHandle as a_, type ActiveToolContextProviderProps as aa, type ActiveToolContextValue as ab, type AnimateToBoundsOptions as ac, type AreaSelectDep as ad, type BezierEasing as ae, type BindingOpts as af, type BooleanOp as ag, type BooleanOpResult as ah, type BoundGesture as ai, type BuildRuleCtxArgs as aj, type ClaimableGesture as ak, type ClipboardDep as al, type ClipboardIngestCtx as am, type ColorOverride as an, type ColorOverrideFn as ao, type CustomPaintContext as ap, type DecayLoopConfig as aq, type DecayOptions as ar, type DepName as as, type DepRegistry as at, DepRegistryProvider as au, type DispatcherContext as av, type DragSample as aw, EASINGS as ax, type EasingFn as ay, type EasingName as az, type DetectedDeviceFacts as b, easeInOutBounce as b$, type Point2 as b0, PointerContextProvider as b1, type PointerContextValue as b2, type PointerWorldPos as b3, type PoseAdapter as b4, type PoseComposition as b5, type ResizePolicy as b6, type ResolveAllOptions as b7, type ResolveOnlyResult as b8, type ResolvedCandidate as b9, type UiOngoingControl as bA, VIEW_ANIMATION_KEY as bB, type ViewAnimationApi as bC, type ViewApi as bD, type ViewChannel as bE, actionBindings as bF, applyBooleanOp as bG, buildRuleCtx as bH, clipboardCopyAction as bI, clipboardCutAction as bJ, composeRectPose as bK, composeWorldPose as bL, createDispatcher as bM, cubicBezierEasing as bN, decomposeRectPose as bO, describeRule as bP, drawLayers as bQ, drawOneLayer as bR, easeIn as bS, easeInBack as bT, easeInBounce as bU, easeInCirc as bV, easeInCubic as bW, easeInElastic as bX, easeInExpo as bY, easeInOut as bZ, easeInOutBack as b_, SPRING_PRESETS as ba, type SelectionExtendKey as bb, type SelectionMode as bc, type Selector as bd, type SnapDep as be, type SpringOptions as bf, type SpringPreset as bg, type StaggerBuilder as bh, type StaggerDelay as bi, type StaggerFactory as bj, type StaggerOptions as bk, type StaggerPerItem as bl, type StaggerSpringPoseOptions as bm, type StaggerTweenOptions as bn, type SvgUnpacker as bo, type TextEditDep as bp, type TimelineHandle as bq, type TimelineOptions as br, type TimelineTrack as bs, type ToolCtx as bt, type ToolModifiers as bu, type ToolPresentation as bv, type ToolSlot as bw, type Track as bx, type TweenLoopOptions as by, type TweenOptions as bz, type Rule as c, easeInOutCirc as c0, easeInOutCubic as c1, easeInOutElastic as c2, easeInOutExpo as c3, easeInOutQuad as c4, easeInOutQuart as c5, easeInOutQuint as c6, easeInOutSine as c7, easeInQuad as c8, easeInQuart as c9, useAction as cA, useActionsRegistry as cB, useActiveToolContext as cC, useDecayLoop as cD, useDepRegistry as cE, useDepSource as cF, useOptionalActiveToolContext as cG, useOptionalDepRegistry as cH, usePointerContext as cI, useSelection as cJ, useViewAnimation as cK, worldPoseLookup as cL, easeInQuint as ca, easeInSine as cb, easeOut as cc, easeOutBack as cd, easeOutBounce as ce, easeOutCirc as cf, easeOutCubic as cg, easeOutElastic as ch, easeOutExpo as ci, easeOutQuad as cj, easeOutQuart as ck, easeOutQuint as cl, easeOutSine as cm, enterTextEditAction as cn, evaluate as co, evaluateEnabled as cp, isLayerPainted as cq, isLayerVisible as cr, linear as cs, rebaseLocalPose as ct, registerContentHandler as cu, resolveEasing as cv, resolveParams as cw, sliceAction as cx, specificity as cy, translateRectPose as cz, type RuleCtx as d, type ChromeCtx as e, type ChromeId as f, type ViewAnimationOptions as g, type DepSchema as h, type ActionsRegistry as i, type AffordanceHit as j, type Dispatcher as k, type ToolDef as l, type ViewportToolDef as m, type AnyTool as n, type ToolKeybinding as o, type OngoingOverlay as p, type ChromeState as q, type SelectionApi as r, type LayerGroup as s, type InsertExtras as t, type ContentHandlerEntry as u, type SvgIngestOptions as v, type ActionsProp as w, type Animator as x, type Affordance as y, type AffordanceRegion as z };