@weasel-js/routing 1.14.2 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,12 @@
1
- import { ModifierKey, ClaimableGesture, GestureSpec, InputEvent, TargetSpec, IngestItem, BodyTarget, BindingOverrideInput, BodyKind, KeyHeldSpec, TargetPredicate, KeySpec, ClickSpec, DoubleClickSpec, DragSpec, ContextMenuSpec, ParsedModifiers, GestureName } from '@weasel-js/gestures';
1
+ import { History } from '@weasel-js/history';
2
+ import { PrefGroup, PrefLeaf, PrefNumber } from '@weasel-js/prefs';
2
3
  import { FramedBox, View, Point } from '@weasel-js/geom';
3
- import { CursorSpec } from '@weasel-js/cursor';
4
- import { MenuEntry, Rect, MenuSide, MenuShortcut } from '@weasel-js/menu';
5
- import { PrefGroup, PrefLeaf } from '@weasel-js/prefs';
6
4
  import { CapabilityTag, ActiveMode, ModeRegistry } from '@weasel-js/modes';
5
+ import { ModifierKey, ClaimableGesture, GestureSpec, IngestItem, InputEvent, TargetSpec, BodyTarget, BindingOverrideInput, BodyKind, KeyHeldSpec, TargetPredicate, KeySpec, ClickSpec, DoubleClickSpec, DragSpec, ContextMenuSpec, ParsedModifiers, GestureName } from '@weasel-js/gestures';
7
6
  import * as _weasel_js_icons from '@weasel-js/icons';
8
7
  import { IconRef } from '@weasel-js/icons';
8
+ import { CursorSpec } from '@weasel-js/cursor';
9
+ import { MenuEntry, Rect, MenuSide, MenuShortcut } from '@weasel-js/menu';
9
10
  import * as react from 'react';
10
11
  import { ReactNode } from 'react';
11
12
  import { Emitter, QueryMatch } from '@weasel-js/registry';
@@ -169,363 +170,429 @@ type ResizeAnchor = {
169
170
  y: 'min' | 'max' | 'free';
170
171
  };
171
172
 
172
- /** The chrome an in-flight gesture paints beside the scene. */
173
-
174
- /**
175
- * What a `polyline` overlay is, in a word — the narrowest thing an action can
176
- * say about how its run should look without naming a paint.
177
- *
178
- * The named roles are the kit's; a painter maps each to a style and falls back
179
- * to plain chrome for one it does not know, so a package outside core can coin
180
- * its own and still be drawn.
181
- */
182
- type OverlayRole =
183
- /** Generic in-flight chrome — the marquee/lasso paint. */
184
- 'chrome'
185
- /** A cut the gesture would make. */
186
- | 'cut'
187
- /** An edge the gesture would author between two points. */
188
- | 'connector'
189
- /** The stretch of a path a soft-selection drag reaches. */
190
- | 'falloff'
191
- /** What a lock holds a dragged point to — a line, a circle. */
192
- | 'lock'
193
- /** What a dragged item would be dropped on: the box it would go into. */
194
- | 'target'
195
- /** Where a dragged item would land: a reorder's insertion bar. */
196
- | 'insert'
197
- /** Where a dragged item is over and cannot land. */
198
- | 'refused' | (string & {});
199
173
  /**
200
- * What an in-flight gesture proposes to paint *beside* the scene, returned by
201
- * `OngoingHandle.overlay()` — a marquee rect, a lasso trail, the outline of a
202
- * shape being dragged out, the line a slice would cut along.
174
+ * Live state read by rule evaluation. Built once per frame on the consuming
175
+ * surface — chrome-caps, the affordance pipeline, the dispatcher's
176
+ * eligibility filter — and discarded.
203
177
  *
204
- * Every variant is world geometry and nothing else — no `DrawCommand`, no
205
- * color, no coordinate space to choose — so a surface rendering the same
206
- * gesture some other way can still read it. The painter owns every style
207
- * decision.
178
+ * Adding a new field is additive: existing rules don't change, new
179
+ * selector atoms can read it.
208
180
  */
209
- type OngoingOverlay = {
210
- kind: 'marquee';
211
- start: {
212
- x: number;
213
- y: number;
214
- };
215
- current: {
216
- x: number;
217
- y: number;
218
- };
219
- shiftHeld: boolean;
220
- } | {
221
- kind: 'lasso';
222
- vertices: ReadonlyArray<{
223
- x: number;
224
- y: number;
225
- }>;
226
- current: {
227
- x: number;
228
- y: number;
181
+ interface RuleCtx {
182
+ readonly focused: boolean;
183
+ readonly selection: readonly NodeId[];
184
+ readonly multiActive: boolean;
185
+ readonly modifiers: ModifierState;
186
+ readonly action: {
187
+ readonly kind: string | null;
188
+ readonly id: string | null;
229
189
  };
230
- shiftHeld: boolean;
231
- } | {
232
- /**
233
- * An open run of world points the gesture is proposing — a cut line, a
234
- * routed connector. `role` is the whole of what an action says about
235
- * appearance.
236
- */
237
- kind: 'polyline';
238
- /** At least two points. Anything shorter is dropped in resolution. */
239
- points: ReadonlyArray<{
240
- x: number;
241
- y: number;
242
- }>;
243
- /** What the run means. Omitted reads as {@link OverlayRole} `'chrome'`. */
244
- role?: OverlayRole;
245
- } | {
246
- /**
247
- * A round brush dragged through world points: the area it has swept so
248
- * far, and its footprint at the last point. The radius is in screen
249
- * pixels, so the footprint keeps its size at every zoom.
250
- */
251
- kind: 'brush';
252
- /** The brush's path so far, at least one point; the last is where it is now. */
253
- points: ReadonlyArray<{
254
- x: number;
255
- y: number;
256
- }>;
257
- radiusPx: number;
258
- } | {
190
+ readonly hover: NodeId | null;
259
191
  /**
260
- * Live insert-drag preview. Pre-commit there is no scene node to ghost
261
- * via `previewIds()`/`previewPose()`, so insert paints its preview
262
- * through the overlay instead.
192
+ * How far the viewport is zoomed in, as one number — `1` at native scale.
263
193
  *
264
- * `bounds` is the AABB of the current drag (start/current normalized).
265
- * `extras` is the per-kind extras the action already collected; the
266
- * overlay renderer narrows on `shape` and casts the field shape it
267
- * expects.
268
- */
269
- kind: 'insertPreview';
270
- /** Which preview the dispatcher's overlay draws. The kit's own shape
271
- * kinds are the ones it knows how to render; a consumer-defined kind
272
- * is legal and simply gets no live preview. */
273
- shape: string;
274
- bounds: {
275
- x: number;
276
- y: number;
277
- width: number;
278
- height: number;
279
- };
280
- extras: unknown;
281
- /** World-space point to paint a small "anchor" dot at, marking the
282
- * click point — for radial shapes (polygon/star), where no vertex
283
- * sits on it, and for any shape in center mode. */
284
- anchorPoint?: {
285
- x: number;
286
- y: number;
287
- };
288
- /** World affines the shape will also be drawn under once it lands, by
289
- * the container it is drawn into: the preview is painted under each. */
290
- copies?: readonly (readonly [number, number, number, number, number, number])[];
291
- } | {
292
- /**
293
- * Why the gesture cannot land where it is, in words for the user: a
294
- * refusal's reason. A surface shows `text` beside the pointer, which
295
- * `at` is, in world coordinates.
194
+ * A scalar rather than the `View` this used to carry, because `zoomAtLeast`
195
+ * is the only selector that ever read one and a number is all it needs. A
196
+ * host whose viewport is a camera has no `View` to hand over and would
197
+ * otherwise have to invent one to use eligibility at all; it omits this
198
+ * instead, and `zoomAtLeast` declines rather than matching on a fiction.
296
199
  */
297
- kind: 'reason';
298
- at: {
299
- x: number;
300
- y: number;
301
- };
302
- text: string;
303
- };
304
- /** A handle's overlays as a list: empty when it has none. */
305
- declare function overlaysOf(handle: OngoingHandle): readonly OngoingOverlay[];
306
-
307
- /**
308
- * Modifier roles: the modifiers an ongoing action reads while it runs, named
309
- * by what they do rather than which key they are, so a binding or a tuning can
310
- * move one to another key without forking the action.
311
- */
312
-
313
- /** The keys that hold a role: any one of them held holds it. `'mod'` is Cmd
314
- * on a Mac and Ctrl elsewhere. An empty list never holds it. */
315
- type RoleKeys = readonly ModifierKey[];
316
- /** One modifier an action reads mid-gesture. */
317
- interface ModifierRole {
318
- label: string;
319
- /** The keys that hold it unless a tuning or a binding says otherwise. */
320
- keys: RoleKeys;
200
+ readonly zoom?: number;
201
+ /** Active mode id. `'normal'` when no non-default mode is engaged. */
202
+ readonly mode: string;
203
+ /** Capability tags allowed by the active mode (the union of
204
+ * `ModeDefinition.allows` plus implicit tags). The `capability:`
205
+ * selector reads this to determine whether a tag is permitted. */
206
+ readonly allowedCapabilities: ReadonlySet<CapabilityTag>;
207
+ /** Whether the current selection may be resized. `<SceneCanvas>` folds
208
+ * `selectTool.resize.resizable` over the selection (true only when every
209
+ * selected node is resizable). Read by the `resizable:` selector to gate
210
+ * `selection.resize-handles`. Absent (legacy ctx builders) is treated as
211
+ * resizable — back-compat: handles show unless a consumer opts a node out. */
212
+ readonly selectionResizable?: boolean;
213
+ /** Whether a path is currently in anchor-edit mode. Read by the
214
+ * `editingAnchors:` selector, which gates the path-edit chrome.
215
+ *
216
+ * This is deliberately a fact about state, not about permission: the
217
+ * anchor overlay and the anchor hit-test must agree, and the thing they
218
+ * must agree on is "is there an edited path right now", which no
219
+ * capability or mode id answers. A mode that allows `edits-anchors`
220
+ * with nothing being edited should draw no anchors. Absent is treated
221
+ * as false. */
222
+ readonly editingAnchors?: boolean;
223
+ /** Device facts — pointer coarseness, hover capability, density.
224
+ *
225
+ * Absent (legacy ctx builders) is treated as
226
+ * {@link DEFAULT_DEVICE_PROFILE}: a fine pointer that can hover, at
227
+ * density 1. That is what the kit assumed before this field existed, so
228
+ * an absent profile is behavior-preserving by construction. */
229
+ readonly device?: DeviceProfile;
230
+ /** The view the input being dispatched landed in — a view id, or `null`
231
+ * for the surface's own camera. The dispatcher fills it; absent anywhere
232
+ * else a rule is evaluated. */
233
+ readonly viewId?: string | null;
234
+ /** Chrome ids the active tool hides (`ToolDef.hides`): never visible,
235
+ * whatever their rules say. Absent hides nothing. */
236
+ readonly hiddenChrome?: ReadonlySet<string>;
321
237
  }
322
- /** An action's roles by id, e.g. `'handle.holdAngle'`. */
323
- type ModifierRoles = Readonly<Record<string, ModifierRole>>;
324
- /** Keys a tuning or a binding gives some of an action's roles. */
325
- type RoleRebinds = Readonly<Record<string, RoleKeys>>;
326
- /** Whether any of `keys` is held. */
327
- declare function keysHeld(keys: RoleKeys, modifiers: ModifierState, isMac: boolean): boolean;
328
- /**
329
- * A reader for an action's own roles. It answers from `ctx.roles`, which the
330
- * dispatcher resolves on every event, and falls back to the role's default
331
- * keys where nothing resolved them: an invoker called directly, or a context
332
- * a cancel-all built with no event.
333
- */
334
- declare function roleReader<R extends ModifierRoles>(roles: R): (ctx: {
335
- roles?: Readonly<Record<string, boolean>>;
238
+ /** The live state a `RuleCtx` is assembled from — the chrome context plus
239
+ * what the active mode allows. */
240
+ interface BuildRuleCtxArgs {
241
+ focused: boolean;
242
+ selection: readonly NodeId[];
243
+ multiActive: boolean;
336
244
  modifiers: ModifierState;
337
- }, id: keyof R & string) => boolean;
338
-
245
+ action: {
246
+ kind: string | null;
247
+ id: string | null;
248
+ };
249
+ hover: NodeId | null;
250
+ /** See {@link RuleCtx.zoom}. */
251
+ zoom?: number;
252
+ mode: string;
253
+ allowedCapabilities: ReadonlySet<CapabilityTag>;
254
+ /** Optional — omitted means "resizable" (handles show). See {@link RuleCtx}. */
255
+ selectionResizable?: boolean;
256
+ /** Optional — omitted means "no path is being anchor-edited". */
257
+ editingAnchors?: boolean;
258
+ /** Optional — omitted means {@link DEFAULT_DEVICE_PROFILE}. */
259
+ device?: DeviceProfile;
260
+ /** Optional — omitted hides nothing. See {@link RuleCtx.hiddenChrome}. */
261
+ hiddenChrome?: ReadonlySet<string>;
262
+ }
339
263
  /**
340
- * Invoker — pluggable invocation strategy for an Action.
264
+ * Capability set to assume when no mode registry is wired.
341
265
  *
342
- * An Action's `invoker` field describes how it's run. `immediate` invokers
343
- * fire once (delete, align, undo). `ongoing` invokers open a phase machine
344
- * driven by the dispatcher (move, resize, pinch-zoom).
266
+ * "No modes configured" means *everything the default mode permits*, not
267
+ * *nothing permitted*. The empty set is the wrong default: it makes every
268
+ * `capability:` rule evaluate false, so a kit default written the
269
+ * recommended way (capability rules outlive new modes; mode rules have to
270
+ * be edited) would silently hide its chrome for every consumer that hasn't
271
+ * opted into modality. `NORMAL.allows` plus the implicit tags is what a
272
+ * mode-less canvas actually behaves like.
345
273
  */
274
+ declare const DEFAULT_ALLOWED_CAPABILITIES: ReadonlySet<CapabilityTag>;
275
+ /** Gather the current canvas and mode state into the context that action
276
+ * eligibility and chrome-visibility rules are evaluated against. */
277
+ declare function buildRuleCtx(args: BuildRuleCtxArgs): RuleCtx;
346
278
 
347
- /** A 2D point in either world or screen coordinates; geom's `Point`. */
348
- type Point2 = Point;
349
279
  /**
350
- * A point in the space the dispatcher's `clientToWorld` maps the pointer into:
351
- * the world, on a 2D canvas. A host with a camera of its own, such as a
352
- * `@weasel-js/kernel3d` scene, passes identity, so there it is the pane's
353
- * screen point and the deps reading it cast a ray through the camera they
354
- * close over. Every world point the dispatcher hands out, and every dep
355
- * payload it reaches, is one of these.
280
+ * A selector is a conjunction of key/value tests. Multiple keys at the same
281
+ * level AND together. Each key maps to a selector primitive in the evaluator.
356
282
  */
357
- type WorldPoint = Point2;
283
+ interface Selector {
284
+ selection?: {
285
+ is?: number;
286
+ atLeast?: number;
287
+ empty?: boolean;
288
+ };
289
+ mode?: string | {
290
+ not: string;
291
+ } | {
292
+ in: readonly string[];
293
+ };
294
+ capability?: CapabilityTag | readonly CapabilityTag[] | {
295
+ in: readonly CapabilityTag[];
296
+ } | {
297
+ not: CapabilityTag;
298
+ };
299
+ gesturing?: boolean;
300
+ actionIs?: string;
301
+ modifierHeld?: keyof ModifierState;
302
+ focused?: boolean;
303
+ hovering?: boolean;
304
+ hoveringSelected?: boolean;
305
+ zoomAtLeast?: number;
306
+ /** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
307
+ * mode. Absent flag is treated as `false`. */
308
+ editingAnchors?: boolean;
309
+ /** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
310
+ * (resizable), so `{ resizable: true }` passes for legacy ctx builders
311
+ * that don't compute it. */
312
+ resizable?: boolean;
313
+ /** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
314
+ * (touch, most styluses). Absent device is treated as `false`. */
315
+ coarsePointer?: boolean;
316
+ /** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
317
+ * device is treated as `true`. */
318
+ canHover?: boolean;
319
+ }
358
320
  /**
359
- * Which UI affordance was hit at pointerdown. Populated by the dispatcher when
360
- * the `affordanceAt` thunk is provided to `useGestureDispatcher`.
321
+ * Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
322
+ * over `Selector` leaves. `when` is the escape hatch — its closure is
323
+ * opaque to introspection and should be avoided when a declarative form
324
+ * exists. Empty `all` is true; empty `any` is false.
325
+ */
326
+ type Rule = Selector | {
327
+ all: readonly Rule[];
328
+ } | {
329
+ any: readonly Rule[];
330
+ } | {
331
+ not: Rule;
332
+ } | {
333
+ when: (ctx: RuleCtx) => boolean;
334
+ };
335
+ /** Constant rules. Kept here so they have a single source. */
336
+ declare const ALWAYS: Rule;
337
+ /** A rule that never passes. `any` of nothing is false. */
338
+ declare const NEVER: Rule;
339
+ /**
340
+ * Render a {@link Rule} as a short human-readable string, for inspectors and
341
+ * diagnostics that need to say WHICH rule decided something.
361
342
  *
362
- * `kind` is a discriminator string:
363
- * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
364
- * `'handle:bottom-right'` — corner resize handles.
365
- * - `'handle:top'` / `'handle:right'` / `'handle:bottom'` / `'handle:left'` —
366
- * edge resize handles.
367
- * - `'rotate-handle'` — the rotation affordance.
368
- * - `'anchor:N'` — a path anchor at index N.
343
+ * ```
344
+ * { capability: 'edits-page' } → capability:edits-page
345
+ * { all: [{ mode: 'normal' }, { focused: true }] } → all(mode:normal, focused:true)
346
+ * { not: { selection: { empty: true } } } → not(selection:empty=true)
347
+ * { when: function hasPath() { … } } → when(hasPath)
348
+ * ```
349
+ *
350
+ * Never throws and always terminates.
369
351
  */
370
- interface AffordanceHit {
371
- /** Discriminator string, e.g. `'handle:bottom-right'`. */
372
- kind: string;
373
- /** Id of whatever produced this hit — a kit affordance's `id`, or the
374
- * registered layer's id. */
375
- owner?: string;
376
- /** `'exclusive'` means no binding may act on this point unless its target
377
- * consults the affordance. `'shared'` (the default) competes on scope and
378
- * specificity as bindings always have. */
379
- strength?: 'exclusive' | 'shared';
380
- /** Which gestures an exclusive claim bars. Omitted bars all of them. */
381
- claimedKinds?: readonly ClaimableGesture[];
382
- /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
383
- fixedPoint?: {
384
- x: number;
385
- y: number;
386
- };
387
- /** Which nodes this affordance belongs to. */
388
- targetIds?: string[];
389
- /** Set only when `kind` matches `'handle:*'`: which corner or edge stays
390
- * fixed during a resize; `'free'` on the axis an edge handle leaves alone. */
391
- anchor?: ResizeAnchor;
392
- /** Cursor to show while the pointer hovers this affordance (no gesture in
393
- * flight). Unset falls through to action-cursor prediction, then to the
394
- * active tool's cursor. */
395
- cursor?: CursorSpec;
396
- /**
397
- * Free-form payload from whatever produced the hit, carried through to the
398
- * matching action untouched. Kit affordances leave it unset; it exists for
399
- * a registered layer's own chrome, where the hit-test already resolved
400
- * *which* of its pieces was hit.
401
- */
402
- payload?: unknown;
352
+ declare function describeRule(rule: Rule): string;
353
+ /** Evaluate a rule against a context. Pure, and cheap enough to run per
354
+ * frame for every piece of chrome. */
355
+ declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
356
+ /**
357
+ * Composable eligibility predicate with a fluent surface. `cond(ctx)`
358
+ * evaluates its `Rule` tree, carried at `.rule`; the fluent methods return new
359
+ * Conditions wrapping new trees.
360
+ *
361
+ * **Chain semantics: strict left-to-right, no precedence.** `a.and(b).or(c)` is
362
+ * `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`. For grouped
363
+ * disjunction, name the subexpression or use the top-level `or(...)`.
364
+ */
365
+ interface Condition {
366
+ (ctx: RuleCtx): boolean;
367
+ readonly rule: Rule;
368
+ /** `this && other` */
369
+ and(other: Condition | Rule): Condition;
370
+ /** `this || other` */
371
+ or(other: Condition | Rule): Condition;
372
+ /** `this && !other` */
373
+ andNot(other: Condition | Rule): Condition;
374
+ /** `this || !other` */
375
+ orNot(other: Condition | Rule): Condition;
403
376
  }
377
+
404
378
  /**
405
- * One accumulated point on a drag trail: world-space position plus whatever
406
- * stylus state the originating `PointerEvent` carried.
379
+ * Whether two rules can never hold at once — no `RuleCtx` passes both.
407
380
  *
408
- * The stylus fields are absent for mouse/touch on browsers that don't report
409
- * them, and for synthetic events.
381
+ * Conservative: `true` is a proof, `false` means "may overlap". It reasons
382
+ * about the selector values themselves (two different `mode`s, an empty
383
+ * selection against a non-empty one, `focused: true` against `false`) and
384
+ * through `all` / `any` / `not`; a `when` closure is opaque, so it overlaps
385
+ * everything. The route-conflict check uses it to stay quiet about bindings
386
+ * that share a tuple but are never eligible together.
410
387
  */
411
- interface DragSample extends WorldPoint {
412
- /** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
413
- pressure?: number;
414
- /** Degrees, ±90. Zero for mouse/touch. */
415
- tiltX?: number;
416
- /** Degrees, ±90. Zero for mouse/touch. */
417
- tiltY?: number;
388
+ declare function rulesExclusive(a: Rule, b: Rule): boolean;
389
+ /**
390
+ * Whether `rule` can hold while `mode` is active. Only its `mode` and
391
+ * `capability` tests are settled by the mode; every other test could go either
392
+ * way, so `false` is a proof and `true` means "may hold".
393
+ */
394
+ declare function ruleCanHoldIn(rule: Rule, mode: ActiveMode): boolean;
395
+
396
+ /** The detected half of a profile — everything except the derived scale. */
397
+ type DetectedDeviceFacts = Omit<DeviceProfile, 'targetScale'>;
398
+ /**
399
+ * Handle/hit multiplier applied on a coarse pointer.
400
+ *
401
+ * 8px handle → 14px; 24px rotation distance → 42px. Counting the
402
+ * surrounding grab zone, that lands in the Apple HIG 44pt / Material 48dp
403
+ * minimum-touch-target band. One constant so it is tunable in one place.
404
+ */
405
+ declare const COARSE_TARGET_SCALE = 1.75;
406
+ /**
407
+ * Fold consumer overrides over detected facts and derive `targetScale`.
408
+ *
409
+ * `targetScale` is re-derived AFTER the merge, so an override of
410
+ * `coarsePointer` alone scales the chrome as expected. An explicit
411
+ * `targetScale` override wins over the derivation.
412
+ */
413
+ declare function resolveDeviceProfile(detected: DetectedDeviceFacts, overrides?: Partial<DeviceProfile>): DeviceProfile;
414
+ /**
415
+ * Assumed when `matchMedia` is unavailable (SSR, jsdom) and used as the
416
+ * absent-means value for `RuleCtx.device`.
417
+ *
418
+ * Derived from `resolveDeviceProfile` rather than hand-written, so it can
419
+ * never drift from what the function would compute for the same facts. A
420
+ * mouse-like device is the safe default: it is what the kit has always
421
+ * assumed, so an absent profile changes nothing for existing consumers.
422
+ */
423
+ declare const DEFAULT_DEVICE_PROFILE: DeviceProfile;
424
+
425
+ /**
426
+ * Modifier roles: the modifiers an ongoing action reads while it runs, named
427
+ * by what they do rather than which key they are, so a binding or a tuning can
428
+ * move one to another key without forking the action.
429
+ */
430
+
431
+ /** The keys that hold a role: any one of them held holds it. `'mod'` is Cmd
432
+ * on a Mac and Ctrl elsewhere. An empty list never holds it. */
433
+ type RoleKeys = readonly ModifierKey[];
434
+ /** One modifier an action reads mid-gesture. */
435
+ interface ModifierRole {
436
+ label: string;
437
+ /** The keys that hold it unless a tuning or a binding says otherwise. */
438
+ keys: RoleKeys;
418
439
  }
419
- /** Per-invocation runtime context the dispatcher hands to an Invoker.
420
- * Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
421
- * populated only for matching gesture kinds. */
422
- interface InvocationCtx {
423
- world: WorldPoint;
424
- /**
425
- * The pointer in client/screen pixels, for an action that has to place DOM
426
- * at the pointer. Absent on every invocation whose event carries no client
427
- * coordinates — a keystroke, a UI-driven trigger, a synthetic probe — so
428
- * read it with a fallback rather than assuming a point.
429
- *
430
- * A view-mutating drag wants `drag.screenDelta`, not the difference of two
431
- * of these: world deltas are self-referential mid-pan, and so is any delta
432
- * taken across a view change.
433
- */
434
- screen?: Point2;
440
+ /** An action's roles by id, e.g. `'handle.holdAngle'`. */
441
+ type ModifierRoles = Readonly<Record<string, ModifierRole>>;
442
+ /** Keys a tuning or a binding gives some of an action's roles. */
443
+ type RoleRebinds = Readonly<Record<string, RoleKeys>>;
444
+ /** Whether any of `keys` is held. */
445
+ declare function keysHeld(keys: RoleKeys, modifiers: ModifierState, isMac: boolean): boolean;
446
+ /**
447
+ * A reader for an action's own roles. It answers from `ctx.roles`, which the
448
+ * dispatcher resolves on every event, and falls back to the role's default
449
+ * keys where nothing resolved them: an invoker called directly, or a context
450
+ * a cancel-all built with no event.
451
+ */
452
+ declare function roleReader<R extends ModifierRoles>(roles: R): (ctx: {
453
+ roles?: Readonly<Record<string, boolean>>;
435
454
  modifiers: ModifierState;
436
- deps: ActionDeps;
437
- drag?: {
438
- start: WorldPoint;
439
- current: WorldPoint;
440
- delta: Point2;
441
- /**
442
- * Drag delta in the surface's own CSS pixels from the drag origin: client
443
- * pixels, or through the host's `clientToLocal` where it supplies one, so
444
- * a canvas under a scaled ancestor pans with the pointer. Use this — never
445
- * `delta` — for any action whose effect
446
- * mutates the viewport itself (pan, view-zoom), because world-space
447
- * deltas become self-referential as the view shifts mid-drag.
448
- *
449
- * Absent when the underlying pointer events carried no
450
- * `clientX`/`clientY`.
451
- */
452
- screenDelta?: Point2;
453
- affordance?: AffordanceHit;
454
- /**
455
- * Full pointermove history for the current drag, in world space, with
456
- * per-sample stylus state when the browser reported it.
457
- * Available only during `onMove` and `onEnd` calls (not on `start`).
458
- *
459
- * During `onMove` this is the dispatcher's live accumulator, and it grows
460
- * under you — read it, don't retain it. On `onEnd` it is a snapshot, so
461
- * that is where an action that keeps the trail should take it.
462
- */
463
- points?: DragSample[];
464
- };
465
- wheel?: {
466
- deltaX: number;
467
- deltaY: number;
468
- deltaZ: number;
469
- };
470
- /** One `pinch` sample: `scale` is a multiplier since the previous sample,
471
- * `rotation` degrees since it. See `PinchEvent`. */
472
- pinch?: {
473
- scale: number;
474
- rotation: number;
475
- };
476
- multiTouch?: {
477
- /** Canvas-local CSS pixels — see `MultiTouchEvent.centroid`. Neither
478
- * `world` nor `screen`: it is the space `zoomAt` anchors in, so passing
479
- * it through `clientToWorld` removes the canvas origin twice. */
480
- centroid: Point2;
481
- /** Distance between the two primary pointers, in CSS pixels. */
482
- spread: number;
483
- /** Degrees the line between the two primary pointers has turned since
484
- * the gesture began, clockwise positive. It keeps counting past a half
485
- * turn. Zero where the events carry no `MultiTouchEvent.angle`. */
486
- rotation: number;
487
- /**
488
- * Pinch-zoom geometry, populated while a multitouch handle is in flight.
489
- * `startSpread` is the spread at the moment the gesture began.
490
- */
491
- pinch?: {
492
- startSpread: number;
493
- currentSpread: number;
494
- /** Canvas-local, same as the enclosing `centroid`. */
495
- centroid: Point2;
496
- };
455
+ }, id: keyof R & string) => boolean;
456
+
457
+ /** The chrome an in-flight gesture paints beside the scene. */
458
+
459
+ /**
460
+ * What a `polyline` overlay is, in a word — the narrowest thing an action can
461
+ * say about how its run should look without naming a paint.
462
+ *
463
+ * The named roles are the kit's; a painter maps each to a style and falls back
464
+ * to plain chrome for one it does not know, so a package outside core can coin
465
+ * its own and still be drawn.
466
+ */
467
+ type OverlayRole =
468
+ /** Generic in-flight chrome — the marquee/lasso paint. */
469
+ 'chrome'
470
+ /** A cut the gesture would make. */
471
+ | 'cut'
472
+ /** An edge the gesture would author between two points. */
473
+ | 'connector'
474
+ /** The stretch of a path a soft-selection drag reaches. */
475
+ | 'falloff'
476
+ /** What a lock holds a dragged point to — a line, a circle. */
477
+ | 'lock'
478
+ /** What a dragged item would be dropped on: the box it would go into. */
479
+ | 'target'
480
+ /** Where a dragged item would land: a reorder's insertion bar. */
481
+ | 'insert'
482
+ /** Where a dragged item is over and cannot land. */
483
+ | 'refused' | (string & {});
484
+ /**
485
+ * What an in-flight gesture proposes to paint *beside* the scene, returned by
486
+ * `OngoingHandle.overlay()` — a marquee rect, a lasso trail, the outline of a
487
+ * shape being dragged out, the line a slice would cut along.
488
+ *
489
+ * Every variant is world geometry and nothing else — no `DrawCommand`, no
490
+ * color, no coordinate space to choose — so a surface rendering the same
491
+ * gesture some other way can still read it. The painter owns every style
492
+ * decision.
493
+ */
494
+ type OngoingOverlay = {
495
+ kind: 'marquee';
496
+ start: {
497
+ x: number;
498
+ y: number;
497
499
  };
498
- key?: {
499
- key: string;
500
- repeat: boolean;
500
+ current: {
501
+ x: number;
502
+ y: number;
501
503
  };
502
- /** What a carry over the surface brings: its payload, as `startCarry` was
503
- * handed it. Present for every event of a `carry` binding's gesture. */
504
- carry?: {
505
- type: string;
506
- value: unknown;
504
+ shiftHeld: boolean;
505
+ } | {
506
+ kind: 'lasso';
507
+ vertices: ReadonlyArray<{
508
+ x: number;
509
+ y: number;
510
+ }>;
511
+ current: {
512
+ x: number;
513
+ y: number;
507
514
  };
515
+ shiftHeld: boolean;
516
+ } | {
508
517
  /**
509
- * The view the input landed in, as the surface's view resolver assigned it:
510
- * a view id, or `null` for the surface's own camera. Absent where no
511
- * surface routed the input — a UI-driven trigger, a bare dispatcher.
518
+ * An open run of world points the gesture is proposing — a cut line, a
519
+ * routed connector. `role` is the whole of what an action says about
520
+ * appearance.
512
521
  */
513
- viewId?: string | null;
522
+ kind: 'polyline';
523
+ /** At least two points. Anything shorter is dropped in resolution. */
524
+ points: ReadonlyArray<{
525
+ x: number;
526
+ y: number;
527
+ }>;
528
+ /** What the run means. Omitted reads as {@link OverlayRole} `'chrome'`. */
529
+ role?: OverlayRole;
530
+ } | {
514
531
  /**
515
- * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
516
- * UI-driven ongoing actions (color picker, opacity slider). The gesture
517
- * dispatcher does not populate this field; gesture-driven actions receive
518
- * params via `BindingOpts.params` on `start` (the `opts` arg).
532
+ * A round brush dragged through world points: the area it has swept so
533
+ * far, and its footprint at the last point. The radius is in screen
534
+ * pixels, so the footprint keeps its size at every zoom.
519
535
  */
520
- params?: Record<string, unknown>;
536
+ kind: 'brush';
537
+ /** The brush's path so far, at least one point; the last is where it is now. */
538
+ points: ReadonlyArray<{
539
+ x: number;
540
+ y: number;
541
+ }>;
542
+ radiusPx: number;
543
+ } | {
521
544
  /**
522
- * Which of the action's `modifierRoles` are held for this event, by role
523
- * id. The dispatcher fills it on every event of a gesture whose action
524
- * declares roles; absent otherwise. Read it through `roleReader`, which
525
- * falls back to the roles' default keys where it is absent.
545
+ * Live insert-drag preview. Pre-commit there is no scene node to ghost
546
+ * via `previewIds()`/`previewPose()`, so insert paints its preview
547
+ * through the overlay instead.
548
+ *
549
+ * `bounds` is the AABB of the current drag (start/current normalized).
550
+ * `extras` is the per-kind extras the action already collected; the
551
+ * overlay renderer narrows on `shape` and casts the field shape it
552
+ * expects.
526
553
  */
527
- roles?: Readonly<Record<string, boolean>>;
528
- }
554
+ kind: 'insertPreview';
555
+ /** Which preview the dispatcher's overlay draws. The kit's own shape
556
+ * kinds are the ones it knows how to render; a consumer-defined kind
557
+ * is legal and simply gets no live preview. */
558
+ shape: string;
559
+ bounds: {
560
+ x: number;
561
+ y: number;
562
+ width: number;
563
+ height: number;
564
+ };
565
+ extras: unknown;
566
+ /** World-space point to paint a small "anchor" dot at, marking the
567
+ * click point — for radial shapes (polygon/star), where no vertex
568
+ * sits on it, and for any shape in center mode. */
569
+ anchorPoint?: {
570
+ x: number;
571
+ y: number;
572
+ };
573
+ /** World affines the shape will also be drawn under once it lands, by
574
+ * the container it is drawn into: the preview is painted under each. */
575
+ copies?: readonly (readonly [number, number, number, number, number, number])[];
576
+ } | {
577
+ /**
578
+ * Why the gesture cannot land where it is, in words for the user: a
579
+ * refusal's reason. A surface shows `text` beside the pointer, which
580
+ * `at` is, in world coordinates.
581
+ */
582
+ kind: 'reason';
583
+ at: {
584
+ x: number;
585
+ y: number;
586
+ };
587
+ text: string;
588
+ };
589
+ /** A handle's overlays as a list: empty when it has none. */
590
+ declare function overlaysOf(handle: OngoingHandle): readonly OngoingOverlay[];
591
+
592
+ /**
593
+ * The options a gesture binding hands the action it fires, and reading their params.
594
+ */
595
+
529
596
  /** Per-invocation options the dispatcher reads from a `GestureBinding`'s
530
597
  * `opts` field and passes to `OngoingInvoker.start`. */
531
598
  interface BindingOpts {
@@ -568,6 +635,8 @@ interface BindingOpts {
568
635
  * For a key binding: live while the pointer is over the surface, whichever
569
636
  * canvas holds the keys, and only then. For a key that means "here" — a
570
637
  * magnifier's peek — on a page with more than one surface that hears it.
638
+ * A held key follows the pointer: one already down when it arrives presses
639
+ * the binding, and leaving releases it.
571
640
  */
572
641
  over?: boolean;
573
642
  /**
@@ -576,69 +645,279 @@ interface BindingOpts {
576
645
  */
577
646
  roles?: RoleRebinds;
578
647
  /** Screen travel, in CSS pixels, a drag makes before this binding's ongoing action starts, over the
579
- * action's own `dragThresholdPx`. See {@link OngoingInvoker.dragThresholdPx}. */
648
+ * action's own `dragThresholdPx`. See `OngoingInvoker.dragThresholdPx`. */
580
649
  dragThresholdPx?: number;
581
650
  }
582
651
  /** Resolve `BindingOpts.params` to a concrete record (calling the thunk if
583
652
  * needed). Returns `undefined` for absent params. */
584
653
  declare function resolveParams(params: BindingOpts['params']): Record<string, unknown> | undefined;
585
- /** The deps an action receives, composed per call from what it `requires`.
586
- * Each name resolves to its {@link DepSchema} entry, and is absent when no
587
- * source registered it; a name nothing has declared passes through as
588
- * `unknown`. */
589
- interface ActionDeps extends Partial<DepSchema> {
590
- [k: string]: unknown;
591
- }
592
654
 
593
- /** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
594
- * `onMove` on subsequent input events of the same gesture and calls
595
- * `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
596
- * on pointercancel / blur / escape). */
597
- interface OngoingHandle {
598
- /**
599
- * Optional logical action kind — a stable, human-readable tag the
600
- * dispatcher exposes via `getActiveAction()`, e.g. `'marquee'`, `'move'`,
601
- * `'resize'`, `'pan'`.
602
- *
603
- * When omitted, `getActiveAction().kind` reports `null` even though a
604
- * handle is in flight.
605
- */
606
- kind?: string;
607
- /**
608
- * The cursor this gesture shows now, read after every move. Wins over the
609
- * action's `activeCursor`, for a cursor that follows the gesture — the
610
- * rotate glyph turning about its pivot. `undefined` defers to the action.
611
- */
612
- cursor?(): CursorSpec | undefined;
613
- /**
614
- * A move of the gesture. Return `'pass'` from a pointer drag's move to hand
615
- * the drag off: the dispatcher ends this handle with `'cancel'`, offers the
616
- * drag as it was pressed to every other candidate in rank order, and the one
617
- * that takes it starts as if it had won the press, then sees this move. With
618
- * no taker the drag is over. A drag never returns to an action that passed it.
619
- */
620
- onMove?(ctx: InvocationCtx): void | 'pass';
621
- onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
622
- /**
623
- * Optional preview surface: the in-flight state the canvas's preview-ghost
624
- * layer renders on top of the committed scene during the gesture.
625
- *
626
- * `previewIds()` enumerates the displaced node ids; `previewPose(id)`
627
- * returns the interim pose for one of them (shape opaque — the canvas
628
- * casts to its `TPose` parameter). `previewIds` returning `null`, or
629
- * omitted, means "no preview this gesture". An id with no `previewPose`
630
- * is shown at its effective pose: an action that publishes its poses to
631
- * the scene's pose overrides, as move, resize and rotate do, leaves
632
- * `previewPose` out rather than answering the same pose twice. `<SceneCanvas>`
633
- * also shows both answers to the scene as overrides (core's
634
- * `publishPreviews`), so what derives from a previewed node follows it.
635
- */
636
- previewIds?(): Iterable<string> | null;
637
- previewPose?(id: string): unknown | null;
638
- /**
639
- * Subset of `previewIds()` the ghost layer paints at full opacity. The
640
- * ghost alpha says "this is in flight under the pointer"; a node the
641
- * gesture merely displaces — a layout sibling reflowing into its
655
+ /**
656
+ * Invoker — pluggable invocation strategy for an Action.
657
+ *
658
+ * An Action's `invoker` field describes how it's run. `immediate` invokers
659
+ * fire once (delete, align, undo). `ongoing` invokers open a phase machine
660
+ * driven by the dispatcher (move, resize, pinch-zoom).
661
+ */
662
+
663
+ /** A 2D point in either world or screen coordinates; geom's `Point`. */
664
+ type Point2 = Point;
665
+ /**
666
+ * A point in the space the dispatcher's `clientToWorld` maps the pointer into:
667
+ * the world, on a 2D canvas. A host with a camera of its own, such as a
668
+ * `@weasel-js/kernel3d` scene, passes identity, so there it is the pane's
669
+ * screen point and the deps reading it cast a ray through the camera they
670
+ * close over. Every world point the dispatcher hands out, and every dep
671
+ * payload it reaches, is one of these.
672
+ */
673
+ type WorldPoint = Point2;
674
+ /**
675
+ * Which UI affordance was hit at pointerdown. Populated by the dispatcher when
676
+ * the `affordanceAt` thunk is provided to `useGestureDispatcher`.
677
+ *
678
+ * `kind` is a discriminator string:
679
+ * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
680
+ * `'handle:bottom-right'` — corner resize handles.
681
+ * - `'handle:top'` / `'handle:right'` / `'handle:bottom'` / `'handle:left'` —
682
+ * edge resize handles.
683
+ * - `'rotate-handle'` — the rotation affordance.
684
+ * - `'anchor:N'` — a path anchor at index N.
685
+ */
686
+ interface AffordanceHit {
687
+ /** Discriminator string, e.g. `'handle:bottom-right'`. */
688
+ kind: string;
689
+ /** Id of whatever produced this hit — a kit affordance's `id`, or the
690
+ * registered layer's id. */
691
+ owner?: string;
692
+ /** `'exclusive'` means no binding may act on this point unless its target
693
+ * consults the affordance. `'shared'` (the default) competes on scope and
694
+ * specificity as bindings always have. */
695
+ strength?: 'exclusive' | 'shared';
696
+ /** Which gestures an exclusive claim bars. Omitted bars all of them. */
697
+ claimedKinds?: readonly ClaimableGesture[];
698
+ /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
699
+ fixedPoint?: {
700
+ x: number;
701
+ y: number;
702
+ };
703
+ /** Which nodes this affordance belongs to. */
704
+ targetIds?: string[];
705
+ /** Set only when `kind` matches `'handle:*'`: which corner or edge stays
706
+ * fixed during a resize; `'free'` on the axis an edge handle leaves alone. */
707
+ anchor?: ResizeAnchor;
708
+ /** Cursor to show while the pointer hovers this affordance (no gesture in
709
+ * flight). Unset falls through to action-cursor prediction, then to the
710
+ * active tool's cursor. */
711
+ cursor?: CursorSpec;
712
+ /**
713
+ * Free-form payload from whatever produced the hit, carried through to the
714
+ * matching action untouched. Kit affordances leave it unset; it exists for
715
+ * a registered layer's own chrome, where the hit-test already resolved
716
+ * *which* of its pieces was hit.
717
+ */
718
+ payload?: unknown;
719
+ }
720
+ /**
721
+ * One accumulated point on a drag trail: world-space position plus whatever
722
+ * stylus state the originating `PointerEvent` carried.
723
+ *
724
+ * The stylus fields are absent for mouse/touch on browsers that don't report
725
+ * them, and for synthetic events.
726
+ */
727
+ interface DragSample extends WorldPoint {
728
+ /** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
729
+ pressure?: number;
730
+ /** Degrees, ±90. Zero for mouse/touch. */
731
+ tiltX?: number;
732
+ /** Degrees, ±90. Zero for mouse/touch. */
733
+ tiltY?: number;
734
+ }
735
+ /** Per-invocation runtime context the dispatcher hands to an Invoker.
736
+ * Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
737
+ * populated only for matching gesture kinds. */
738
+ interface InvocationCtx {
739
+ world: WorldPoint;
740
+ /**
741
+ * The pointer in client/screen pixels, for an action that has to place DOM
742
+ * at the pointer. Absent on every invocation whose event carries no client
743
+ * coordinates — a keystroke, a UI-driven trigger, a synthetic probe — so
744
+ * read it with a fallback rather than assuming a point.
745
+ *
746
+ * A view-mutating drag wants `drag.screenDelta`, not the difference of two
747
+ * of these: world deltas are self-referential mid-pan, and so is any delta
748
+ * taken across a view change.
749
+ */
750
+ screen?: Point2;
751
+ modifiers: ModifierState;
752
+ deps: ActionDeps;
753
+ drag?: {
754
+ start: WorldPoint;
755
+ current: WorldPoint;
756
+ delta: Point2;
757
+ /**
758
+ * Drag delta in the surface's own CSS pixels from the drag origin: client
759
+ * pixels, or through the host's `clientToLocal` where it supplies one, so
760
+ * a canvas under a scaled ancestor pans with the pointer. Use this — never
761
+ * `delta` — for any action whose effect
762
+ * mutates the viewport itself (pan, view-zoom), because world-space
763
+ * deltas become self-referential as the view shifts mid-drag.
764
+ *
765
+ * Absent when the underlying pointer events carried no
766
+ * `clientX`/`clientY`.
767
+ */
768
+ screenDelta?: Point2;
769
+ affordance?: AffordanceHit;
770
+ /**
771
+ * Full pointermove history for the current drag, in world space, with
772
+ * per-sample stylus state when the browser reported it.
773
+ * Available only during `onMove` and `onEnd` calls (not on `start`).
774
+ *
775
+ * During `onMove` this is the dispatcher's live accumulator, and it grows
776
+ * under you — read it, don't retain it. On `onEnd` it is a snapshot, so
777
+ * that is where an action that keeps the trail should take it.
778
+ */
779
+ points?: DragSample[];
780
+ };
781
+ wheel?: {
782
+ deltaX: number;
783
+ deltaY: number;
784
+ deltaZ: number;
785
+ };
786
+ /** One `pinch` sample: `scale` is a multiplier since the previous sample,
787
+ * `rotation` degrees since it. See `PinchEvent`. */
788
+ pinch?: {
789
+ scale: number;
790
+ rotation: number;
791
+ };
792
+ multiTouch?: {
793
+ /** Canvas-local CSS pixels — see `MultiTouchEvent.centroid`. Neither
794
+ * `world` nor `screen`: it is the space `zoomAt` anchors in, so passing
795
+ * it through `clientToWorld` removes the canvas origin twice. */
796
+ centroid: Point2;
797
+ /** Distance between the two primary pointers, in CSS pixels. */
798
+ spread: number;
799
+ /** Degrees the line between the two primary pointers has turned since
800
+ * the gesture began, clockwise positive. It keeps counting past a half
801
+ * turn. Zero where the events carry no `MultiTouchEvent.angle`. */
802
+ rotation: number;
803
+ /**
804
+ * Pinch-zoom geometry, populated while a multitouch handle is in flight.
805
+ * `startSpread` is the spread at the moment the gesture began.
806
+ */
807
+ pinch?: {
808
+ startSpread: number;
809
+ currentSpread: number;
810
+ /** Canvas-local, same as the enclosing `centroid`. */
811
+ centroid: Point2;
812
+ };
813
+ };
814
+ key?: {
815
+ key: string;
816
+ repeat: boolean;
817
+ };
818
+ /** What a carry over the surface brings: its payload, as `startCarry` was
819
+ * handed it. Present for every event of a `carry` binding's gesture. */
820
+ carry?: {
821
+ type: string;
822
+ value: unknown;
823
+ };
824
+ /**
825
+ * The view the input landed in, as the surface's view resolver assigned it:
826
+ * a view id, or `null` for the surface's own camera. Absent where no
827
+ * surface routed the input — a UI-driven trigger, a bare dispatcher.
828
+ */
829
+ viewId?: string | null;
830
+ /**
831
+ * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
832
+ * UI-driven ongoing actions (color picker, opacity slider). The gesture
833
+ * dispatcher does not populate this field; gesture-driven actions receive
834
+ * params via `BindingOpts.params` on `start` (the `opts` arg).
835
+ */
836
+ params?: Record<string, unknown>;
837
+ /**
838
+ * Which of the action's `modifierRoles` are held for this event, by role
839
+ * id. The dispatcher fills it on every event of a gesture whose action
840
+ * declares roles; absent otherwise. Read it through `roleReader`, which
841
+ * falls back to the roles' default keys where it is absent.
842
+ */
843
+ roles?: Readonly<Record<string, boolean>>;
844
+ }
845
+
846
+ /** The deps an action receives, composed per call from what it `requires`.
847
+ * Each name resolves to its {@link DepSchema} entry, and is absent when no
848
+ * source registered it; a name nothing has declared passes through as
849
+ * `unknown`. */
850
+ interface ActionDeps extends Partial<DepSchema> {
851
+ [k: string]: unknown;
852
+ }
853
+
854
+ /** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
855
+ * `onMove` on subsequent input events of the same gesture and calls
856
+ * `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
857
+ * on pointercancel / blur / escape). */
858
+ interface OngoingHandle {
859
+ /**
860
+ * Optional logical action kind — a stable, human-readable tag the
861
+ * dispatcher exposes via `getActiveAction()`, e.g. `'marquee'`, `'move'`,
862
+ * `'resize'`, `'pan'`.
863
+ *
864
+ * When omitted, `getActiveAction().kind` reports `null` even though a
865
+ * handle is in flight.
866
+ */
867
+ kind?: string;
868
+ /**
869
+ * The cursor this gesture shows now, read after every move. Wins over the
870
+ * action's `activeCursor`, for a cursor that follows the gesture — the
871
+ * rotate glyph turning about its pivot. `undefined` defers to the action.
872
+ */
873
+ cursor?(): CursorSpec | undefined;
874
+ /**
875
+ * A move of the gesture. Return `'pass'` from a pointer drag's move to hand
876
+ * the drag off: the dispatcher ends this handle with `'cancel'`, offers the
877
+ * drag as it was pressed to every other candidate in rank order, and the one
878
+ * that takes it starts as if it had won the press, then sees this move. With
879
+ * no taker the drag is over. A drag never returns to an action that passed it.
880
+ */
881
+ onMove?(ctx: InvocationCtx): void | 'pass';
882
+ onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
883
+ /**
884
+ * Commit this gesture again, as it ended, with `params` in place of the ones it read: what lets a drag of an
885
+ * action declaring `params` be the tree's last operation, kept when its `onEnd('commit')` leaves one history
886
+ * entry. The keeper calls it after undoing that entry and putting back the selection the gesture started on.
887
+ * Recompute the commit from what the gesture recorded (its start state, the pointer as it ended or every move
888
+ * when the path taken matters, the modifiers held), never the live pointer, and commit one entry as `onEnd`
889
+ * did. `params` is every param the gesture resolved at its end with the panel's values laid over them; read
890
+ * them instead of `opts.params`. It may run any number of times. A gesture that cannot be recomputed
891
+ * faithfully leaves it out, and its action gets no panel.
892
+ */
893
+ replay?(params: Record<string, unknown>): void;
894
+ /**
895
+ * Params this gesture's commit took from what it edited rather than from its binding, read once it has ended:
896
+ * a held warp's own grid and tolerance, say, which the binding's settings do not reach. The last-operation
897
+ * panel shows them over the resolved params, and `replay` is handed them, so a change to one field leaves the
898
+ * rest as the gesture found them.
899
+ */
900
+ params?(): Readonly<Record<string, unknown>>;
901
+ /**
902
+ * Optional preview surface: the in-flight state the canvas's preview-ghost
903
+ * layer renders on top of the committed scene during the gesture.
904
+ *
905
+ * `previewIds()` enumerates the displaced node ids; `previewPose(id)`
906
+ * returns the interim pose for one of them (shape opaque — the canvas
907
+ * casts to its `TPose` parameter). `previewIds` returning `null`, or
908
+ * omitted, means "no preview this gesture". An id with no `previewPose`
909
+ * is shown at its effective pose: an action that publishes its poses to
910
+ * the scene's pose overrides, as move, resize and rotate do, leaves
911
+ * `previewPose` out rather than answering the same pose twice. `<SceneCanvas>`
912
+ * also shows both answers to the scene as overrides (core's
913
+ * `publishPreviews`), so what derives from a previewed node follows it.
914
+ */
915
+ previewIds?(): Iterable<string> | null;
916
+ previewPose?(id: string): unknown | null;
917
+ /**
918
+ * Subset of `previewIds()` the ghost layer paints at full opacity. The
919
+ * ghost alpha says "this is in flight under the pointer"; a node the
920
+ * gesture merely displaces — a layout sibling reflowing into its
642
921
  * destination slot — is not, and reads better settled. Honored at
643
922
  * subtree-root granularity.
644
923
  */
@@ -674,648 +953,33 @@ interface OngoingHandle {
674
953
  }
675
954
  /** Fire-once invocation. Runs to completion synchronously (or fires off an
676
955
  * async side-effect; the registry doesn't wait). */
677
- interface ImmediateInvoker {
678
- timing: 'immediate';
679
- /** `params` carries the matched binding's opts.params. Invoked from the
680
- * command palette, or anywhere else with no per-binding context, `params`
681
- * is undefined; descriptors should default to a sensible variant.
682
- *
683
- * Return `'pass'` when, having looked at the event, there is nothing to do
684
- * with it — a scroll already at the end of its range — and the dispatcher
685
- * walks on to the next matching binding, as for a declined `enabled()`.
686
- * Prediction cannot run the invoker, so it still names this action. */
687
- run(deps: ActionDeps, params?: Record<string, unknown>): void | 'pass';
688
- }
689
- /** Phase-machine invocation. `start` opens the phase and returns the handle
690
- * the dispatcher pumps. */
691
- interface OngoingInvoker {
692
- timing: 'ongoing';
693
- start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
694
- /**
695
- * Screen travel, in CSS pixels, a pointer drag makes before the dispatcher calls `start`; a
696
- * binding's own `dragThresholdPx` wins over it. Until then the gesture is held for this action and
697
- * nothing else takes it, and a release short of it ends the gesture with `start` never called.
698
- * Unset, the action starts once the press is a drag at all (`gestures.dragPx`).
699
- */
700
- dragThresholdPx?: number;
701
- }
702
- /** Pluggable invocation strategy for an Action. */
703
- type Invoker = ImmediateInvoker | OngoingInvoker;
704
-
705
- /**
706
- * GestureBinding — connects a GestureSpec to an Action id (with per-binding
707
- * options). Tools own arrays of these on their `bindings` field; ambient
708
- * gesture-bindings are registered globally.
709
- */
710
-
711
- /** An interaction: a gesture spec composed with the id of the action it
712
- * invokes. Tools declare arrays of these; the dispatcher matches an incoming
713
- * input event against them and runs the winner's action. */
714
- interface GestureBinding {
715
- spec: GestureSpec;
716
- actionId: string;
717
- opts?: BindingOpts;
718
- }
719
- /**
720
- * A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
721
- * (no per-binding opts) or an object form that pairs a spec with
722
- * `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
723
- * Use the object form when two bindings for the same action differ only in
724
- * a runtime parameter — the dispatcher extracts `opts.params` and passes
725
- * them to `ImmediateInvoker.run` as its second argument.
726
- */
727
- type BoundGesture = GestureSpec | {
728
- spec: GestureSpec;
729
- opts: BindingOpts;
730
- };
731
- /**
732
- * The whole of an action `actionBindings` reads. Routing needs this much and
733
- * none of `Action`'s palette fields, so it is the narrowest type the
734
- * dispatcher's binding walk can be written against.
735
- */
736
- interface BindingSource {
737
- id: string;
738
- /** The gesture-spec form of the binding, read by the gesture dispatcher.
739
- * May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
740
- * or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
741
- * `{ spec, opts }` — use the object form for parametric actions where two
742
- * bindings for the same action differ only by `opts.params` (e.g. `flip`
743
- * with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
744
- * passes them to `ImmediateInvoker.run` as its second argument. */
745
- defaultBinding?: GestureSpec | BoundGesture[];
746
- }
747
- /**
748
- * Flatten an action's `defaultBinding` into `GestureBinding`s. A bare
749
- * `GestureSpec` has `kind` at top level; the object form has `spec`.
750
- */
751
- declare function actionBindings(action: BindingSource): GestureBinding[];
752
-
753
- /** What a key no open keymap binds does: `'modal'` swallows it and leaves the
754
- * keymap open; `'cascade'` closes the keymap and dispatches it as if none were. */
755
- type KeymapUnbound = 'modal' | 'cascade';
756
- /** What holds a level opened with `{ hold: true }` open: no key releases it, so it closes on Escape. */
757
- declare const KEYMAP_HOLD = "hold";
758
- /** One open level of a {@link KeymapStack}. */
759
- interface OpenKeymap {
760
- readonly id: string;
761
- readonly label: string;
762
- /** The keymap's bindings as declared, for a hint to list. */
763
- readonly bindings: readonly GestureBinding[];
764
- /** The gesture id of the leader holding this level open, while it is held,
765
- * or {@link KEYMAP_HOLD}. */
766
- readonly heldBy?: string;
767
- /** Whether input arrived while the leader was held, so its release closes the level. */
768
- readonly usedWhileHeld: boolean;
769
- }
770
- /** What the keymaps made of the raw input in progress: `'taken'` when one of
771
- * them handled it, `'cascaded'` when it went on past them, `null` before either. */
772
- type KeymapVerdict = 'taken' | 'cascaded' | null;
773
- /** The keymaps open on one surface, innermost last; a canvas's `keymaps` dep. */
774
- interface KeymapStack {
775
- /** Every open level, outermost first. */
776
- open(): readonly OpenKeymap[];
777
- innermost(): OpenKeymap | null;
778
- /** Open a level. `heldBy` names what holds it open; `bindings` are what a hint lists. */
779
- push(id: string, label: string, opts?: {
780
- heldBy?: string;
781
- bindings?: readonly GestureBinding[];
782
- }): void;
783
- /** Mark the levels the last raw input opened as held by `heldBy`: the key
784
- * that opened them is still down. False when it opened none. */
785
- holdOpened(heldBy: string): boolean;
786
- /** The leader `heldBy` came up: close its level, those above it and the
787
- * tapped ones beneath it if input arrived meanwhile, else leave it open
788
- * for one more input. */
789
- release(heldBy: string): void;
790
- /** Close the innermost level: Escape. */
791
- closeInnermost(): void;
792
- /** One of the keymaps took this input, or a pointer gesture went past them.
793
- * Marks held levels used and closes once-levels down to the innermost held one. */
794
- inputSeen(): void;
795
- /** A key no keymap binds was swallowed: held levels count it as input, and nothing closes. */
796
- swallowed(): void;
797
- /** This input went on past the keymaps: as `inputSeen`, remembered as a cascade. */
798
- cascaded(): void;
799
- /** What the keymaps made of the input in progress. */
800
- verdict(): KeymapVerdict;
801
- closeAll(): void;
802
- /** Run `fn` as one raw input: closes made inside apply when it returns. Nests. */
803
- batch<T>(fn: () => T): T;
804
- subscribe(listener: () => void): () => void;
805
- getVersion(): number;
806
- }
807
- /** An empty {@link KeymapStack}. `useGestureDispatcher` makes one per surface. */
808
- declare function createKeymapStack(): KeymapStack;
809
-
810
- /**
811
- * Re-exports the pure matcher primitives from `@weasel-js/gestures` and layers
812
- * the actions-layer binding-scope / matchBest logic on top.
813
- */
814
-
815
- /** Where a binding came from, which is also its priority: a held hotkey beats
816
- * the active tool, which beats bindings that are always in scope. */
817
- type BindingScope = 'ambient' | 'active' | 'hotkey';
818
- /** A binding paired with where it came from, ready to be matched against an
819
- * event. */
820
- interface ScopedBinding {
821
- binding: GestureBinding;
822
- scope: BindingScope;
823
- /** Id of the tool or contribution that declared this binding —
824
- * `'&'`-channel phase atoms resolve to it. `null` for a registered
825
- * action's own binding. */
826
- ownerId: string | null;
827
- }
828
- /** The binding that won a match. */
829
- interface MatchResult {
830
- binding: GestureBinding;
831
- scope: BindingScope;
832
- /** Id of the entry that declared the binding — propagated from
833
- * `ScopedBinding` so the dispatcher can record it as the handle owner. */
834
- ownerId: string | null;
835
- }
836
- /**
837
- * True when a spec's target actually consults the affordance hit rather than
838
- * only the body classification. `kindOf` predicates are handed the hit;
839
- * `affordance:<k>` matches on its `kind`. The body-class strings (`'empty'`,
840
- * `'selected-body'`, `'unselected-body'`) and the `kind:` forms resolve from
841
- * `bodyTarget` / `bodyKind` and never see it.
842
- *
843
- * A predicate declares which it is with `readsAffordance`; one that declares
844
- * nothing, which only untyped code can write, counts as not reading the hit.
845
- */
846
- declare function targetConsultsAffordance(specTarget: TargetSpec | undefined): boolean;
847
- /** The first of {@link matchSorted}'s matches, or null when nothing matches. */
848
- declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>): MatchResult | null;
849
- /**
850
- * Find every binding that matches the event, sorted by precedence
851
- * (best → worst). Ordering rules:
852
- *
853
- * - Scopes by priority: hotkey > active > ambient.
854
- * - Within a scope, more-specific bindings beat less-specific (see
855
- * `specificity()`).
856
- * - Among same-specificity bindings, first-declared wins.
857
- */
858
- declare function matchSorted(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, warn?: (message: string) => void): MatchResult[];
859
- /** {@link matchSorted}, plus what an exclusive claim on the event barred. */
860
- interface MatchOutcome {
861
- matches: MatchResult[];
862
- /** Bindings whose spec matched the event but which the claim barred, in
863
- * the same order `matches` uses. Filled only when `collectBarred` is set. */
864
- barred: MatchResult[];
865
- /** Owner of the exclusive claim, when one applied. */
866
- claimOwner?: string;
867
- }
868
- /**
869
- * {@link matchSorted} that can also report the bindings an exclusive claim
870
- * kept out. Collecting them costs a spec match per barred binding, so it is
871
- * opt-in — the dispatcher asks only when it is recording.
872
- */
873
- declare function matchSortedWithBarred(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, opts?: {
874
- collectBarred?: boolean;
875
- warn?: (message: string) => void;
876
- }): MatchOutcome;
877
-
878
- /**
879
- * How one input was resolved: every binding that matched it, what each filter
880
- * dropped, the order the rest were ranked in and why, and what the walk down
881
- * that order did. The dispatcher fills one per input in dev builds, and
882
- * `Dispatcher.explain` fills one on request without invoking anything.
883
- *
884
- * Plain data — no functions, no live objects — so it can be kept in a trace
885
- * buffer, serialized, or built by hand as a fixture.
886
- */
887
- interface DispatchRecord {
888
- kind: 'dispatch';
889
- ts: number;
890
- input: DispatchRecordInput;
891
- /** Every binding whose spec matched the input, in matcher order, including
892
- * those a filter then dropped. */
893
- matched: readonly RecordCandidate[];
894
- dropped: readonly DroppedCandidate[];
895
- /** The candidates that survived both filters, best first. */
896
- ranked: readonly RankedCandidate[];
897
- /** True when nothing was invoked: the record answers "what would happen". */
898
- predicted: boolean;
899
- /** Action id of the candidate that fired (or would), else `null`. */
900
- fired: string | null;
901
- outcome: 'handled' | 'unhandled';
902
- }
903
- /** The input a {@link DispatchRecord} describes, reduced to plain data. */
904
- interface DispatchRecordInput {
905
- /** `InputEvent.kind`: `'key'`, `'pointerdown'`, `'wheel'`, … */
906
- eventKind: string;
907
- /** For `key` / `key-held` events, the key id (`'Escape'`, `' '`, `'a'`). */
908
- key?: string;
909
- modifiers: {
910
- alt: boolean;
911
- ctrl: boolean;
912
- meta: boolean;
913
- shift: boolean;
914
- };
915
- /** The view the input was routed to; `null` for the surface's own camera. */
916
- viewId: string | null;
917
- /** World point, for events that carry one. */
918
- world?: {
919
- x: number;
920
- y: number;
921
- };
922
- /** Active mode id, when the host supplies a rule context. */
923
- mode?: string;
924
- }
925
- /** One matched binding. */
926
- interface RecordCandidate {
927
- actionId: string;
928
- /** The binding's spec in route grammar — one per arg alternative. */
929
- routes: readonly string[];
930
- scope: BindingScope;
931
- /** The tool or contribution that declared the binding; `null` for an action's own binding. */
932
- ownerId: string | null;
933
- /** Whether the binding's `views` names the routed view. */
934
- namesView: boolean;
935
- /** `specificity(spec)`: target, modifiers, phase, exact. */
936
- specificity: readonly [number, number, number, number];
937
- /** The action's `eligible` rule as text, when it declares one. */
938
- eligible?: string;
939
- }
940
- /** A matched candidate a filter removed before ranking, and which filter. */
941
- type DroppedCandidate =
942
- /** An exclusive claim on the input barred bindings that do not read the
943
- * claim's affordance. */
944
- {
945
- candidate: RecordCandidate;
946
- filter: 'claim';
947
- owner?: string;
948
- }
949
- /** The action's `eligible` rule does not hold. */
950
- | {
951
- candidate: RecordCandidate;
952
- filter: 'ineligible';
953
- rule: string;
954
- };
955
- /** One field of the `specificity` tuple, in the order it is compared. */
956
- type SpecificityPart = 'target' | 'mods' | 'phase' | 'exact';
957
- /** The ranking step that put a candidate below the one before it. */
958
- type PlacedBy = {
959
- step: 'first';
960
- }
961
- /** The one above names the routed view; this one does not. */
962
- | {
963
- step: 'view';
964
- }
965
- /** The one above sits in a higher scope tier. */
966
- | {
967
- step: 'tier';
968
- }
969
- /** Same tier; the one above is more specific in `part`. */
970
- | {
971
- step: 'specificity';
972
- part: SpecificityPart;
973
- }
974
- /** Tied; the one above is gated by an `eligible` rule that holds. */
975
- | {
976
- step: 'context';
977
- }
978
- /** Tied on everything; registration order. */
979
- | {
980
- step: 'order';
981
- };
982
- /** What the walk did with one ranked candidate. */
983
- type WalkStep = {
984
- kind: 'fired';
985
- }
986
- /** Predicted records only: this candidate would fire. Nothing was started,
987
- * so an ongoing action here may still bail at `start()` for real. */
988
- | {
989
- kind: 'would-fire';
990
- }
991
- /** `enabled()` returned this reason. */
992
- | {
993
- kind: 'declined';
994
- reason: string;
995
- }
996
- /** An ongoing action's `start()` returned an empty handle, or an
997
- * immediate one's `run()` returned `'pass'`. */
998
- | {
999
- kind: 'empty-handle';
1000
- }
1001
- /** An ongoing action bound to a press-time `pointerDown` spec; refused. */
1002
- | {
1003
- kind: 'misbound';
1004
- }
1005
- /** Its action was already tried higher up; each runs at most once. */
1006
- | {
1007
- kind: 'duplicate';
1008
- } | {
1009
- kind: 'no-such-action';
1010
- }
1011
- /** Below the winner; never asked. */
1012
- | {
1013
- kind: 'not-asked';
1014
- }
1015
- /** Below the winner, asked anyway (`evaluateShadowed`), and it passed. */
1016
- | {
1017
- kind: 'outranked';
1018
- };
1019
- /** A candidate that survived the filters: why it sits where it does, and
1020
- * what the walk did with it. */
1021
- interface RankedCandidate {
1022
- candidate: RecordCandidate;
1023
- placedBy: PlacedBy;
1024
- walk: WalkStep;
1025
- }
1026
-
1027
- /** One entry per "mode change" — a kit state transition that isn't a
1028
- * dispatched input event but explains why an input was (or wasn't) handled
1029
- * later. Pushed via {@link DispatchTrace.recordModeSwitch}. */
1030
- interface ModeSwitchLogEntry {
1031
- kind: 'mode';
1032
- ts: number;
1033
- /** Logical mode name, e.g. `'editAnchors.editingId'`. */
1034
- mode: string;
1035
- from: string | null;
1036
- to: string | null;
1037
- /** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
1038
- * `'selection-dropped target'`, etc. */
1039
- detail?: string;
1040
- }
1041
- /** A trace entry: one {@link DispatchRecord} per resolved input, or a mode switch. */
1042
- type TraceLogEntry = DispatchRecord | ModeSwitchLogEntry;
1043
- /** A rolling buffer that records in a dev build, and in any build while something watches it. */
1044
- interface TraceRing<T> {
1045
- /** Whether a push now keeps its entry. A caller building an entry checks first. */
1046
- recording(): boolean;
1047
- push(entry: T): void;
1048
- /** The entries pushed after `cursor`, oldest first, and the cursor to pass
1049
- * next time. Start from 0. Entries the buffer has dropped are skipped. */
1050
- since(cursor: number): {
1051
- entries: T[];
1052
- cursor: number;
1053
- };
1054
- /** Turn recording on until the returned release is called. */
1055
- watch(): () => void;
1056
- /** Hear each kept push. Returns the unsubscribe. */
1057
- subscribe(listener: () => void): () => void;
1058
- }
1059
- /** A {@link TraceRing} keeping the last `limit` entries. `dev` defaults to the build's. */
1060
- declare function createTraceRing<T>(limit?: number, dev?: boolean): TraceRing<T>;
1061
- /** One canvas's dispatch trace. `useGestureDispatcher` makes one, puts it on
1062
- * every `DispatcherContext` it builds, and publishes it as the `dispatchTrace` dep. */
1063
- interface DispatchTrace extends TraceRing<TraceLogEntry> {
1064
- recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
1065
- /** Offer the record for what a press at the pointer would do, built only
1066
- * when {@link DispatchTrace.live} reads it. `null` withdraws it. */
1067
- setLive(explain: (() => DispatchRecord) | null): void;
1068
- live(): DispatchRecord | null;
1069
- }
1070
- /** A {@link DispatchTrace} keeping the last `limit` entries. `dev` defaults to the build's. */
1071
- declare function createDispatchTrace(limit?: number, dev?: boolean): DispatchTrace;
1072
-
1073
- /**
1074
- * Live state read by rule evaluation. Built once per frame on the consuming
1075
- * surface — chrome-caps, the affordance pipeline, the dispatcher's
1076
- * eligibility filter — and discarded.
1077
- *
1078
- * Adding a new field is additive: existing rules don't change, new
1079
- * selector atoms can read it.
1080
- */
1081
- interface RuleCtx {
1082
- readonly focused: boolean;
1083
- readonly selection: readonly NodeId[];
1084
- readonly multiActive: boolean;
1085
- readonly modifiers: ModifierState;
1086
- readonly action: {
1087
- readonly kind: string | null;
1088
- readonly id: string | null;
1089
- };
1090
- readonly hover: NodeId | null;
1091
- /**
1092
- * How far the viewport is zoomed in, as one number — `1` at native scale.
1093
- *
1094
- * A scalar rather than the `View` this used to carry, because `zoomAtLeast`
1095
- * is the only selector that ever read one and a number is all it needs. A
1096
- * host whose viewport is a camera has no `View` to hand over and would
1097
- * otherwise have to invent one to use eligibility at all; it omits this
1098
- * instead, and `zoomAtLeast` declines rather than matching on a fiction.
1099
- */
1100
- readonly zoom?: number;
1101
- /** Active mode id. `'normal'` when no non-default mode is engaged. */
1102
- readonly mode: string;
1103
- /** Capability tags allowed by the active mode (the union of
1104
- * `ModeDefinition.allows` plus implicit tags). The `capability:`
1105
- * selector reads this to determine whether a tag is permitted. */
1106
- readonly allowedCapabilities: ReadonlySet<CapabilityTag>;
1107
- /** Whether the current selection may be resized. `<SceneCanvas>` folds
1108
- * `selectTool.resize.resizable` over the selection (true only when every
1109
- * selected node is resizable). Read by the `resizable:` selector to gate
1110
- * `selection.resize-handles`. Absent (legacy ctx builders) is treated as
1111
- * resizable — back-compat: handles show unless a consumer opts a node out. */
1112
- readonly selectionResizable?: boolean;
1113
- /** Whether a path is currently in anchor-edit mode. Read by the
1114
- * `editingAnchors:` selector, which gates the path-edit chrome.
1115
- *
1116
- * This is deliberately a fact about state, not about permission: the
1117
- * anchor overlay and the anchor hit-test must agree, and the thing they
1118
- * must agree on is "is there an edited path right now", which no
1119
- * capability or mode id answers. A mode that allows `edits-anchors`
1120
- * with nothing being edited should draw no anchors. Absent is treated
1121
- * as false. */
1122
- readonly editingAnchors?: boolean;
1123
- /** Device facts — pointer coarseness, hover capability, density.
1124
- *
1125
- * Absent (legacy ctx builders) is treated as
1126
- * {@link DEFAULT_DEVICE_PROFILE}: a fine pointer that can hover, at
1127
- * density 1. That is what the kit assumed before this field existed, so
1128
- * an absent profile is behavior-preserving by construction. */
1129
- readonly device?: DeviceProfile;
1130
- /** The view the input being dispatched landed in — a view id, or `null`
1131
- * for the surface's own camera. The dispatcher fills it; absent anywhere
1132
- * else a rule is evaluated. */
1133
- readonly viewId?: string | null;
1134
- }
1135
- /** The live state a `RuleCtx` is assembled from — the chrome context plus
1136
- * what the active mode allows. */
1137
- interface BuildRuleCtxArgs {
1138
- focused: boolean;
1139
- selection: readonly NodeId[];
1140
- multiActive: boolean;
1141
- modifiers: ModifierState;
1142
- action: {
1143
- kind: string | null;
1144
- id: string | null;
1145
- };
1146
- hover: NodeId | null;
1147
- /** See {@link RuleCtx.zoom}. */
1148
- zoom?: number;
1149
- mode: string;
1150
- allowedCapabilities: ReadonlySet<CapabilityTag>;
1151
- /** Optional — omitted means "resizable" (handles show). See {@link RuleCtx}. */
1152
- selectionResizable?: boolean;
1153
- /** Optional — omitted means "no path is being anchor-edited". */
1154
- editingAnchors?: boolean;
1155
- /** Optional — omitted means {@link DEFAULT_DEVICE_PROFILE}. */
1156
- device?: DeviceProfile;
1157
- }
1158
- /**
1159
- * Capability set to assume when no mode registry is wired.
1160
- *
1161
- * "No modes configured" means *everything the default mode permits*, not
1162
- * *nothing permitted*. The empty set is the wrong default: it makes every
1163
- * `capability:` rule evaluate false, so a kit default written the
1164
- * recommended way (capability rules outlive new modes; mode rules have to
1165
- * be edited) would silently hide its chrome for every consumer that hasn't
1166
- * opted into modality. `NORMAL.allows` plus the implicit tags is what a
1167
- * mode-less canvas actually behaves like.
1168
- */
1169
- declare const DEFAULT_ALLOWED_CAPABILITIES: ReadonlySet<CapabilityTag>;
1170
- /** Gather the current canvas and mode state into the context that action
1171
- * eligibility and chrome-visibility rules are evaluated against. */
1172
- declare function buildRuleCtx(args: BuildRuleCtxArgs): RuleCtx;
1173
-
1174
- /**
1175
- * A selector is a conjunction of key/value tests. Multiple keys at the same
1176
- * level AND together. Each key maps to a selector primitive in the evaluator.
1177
- */
1178
- interface Selector {
1179
- selection?: {
1180
- is?: number;
1181
- atLeast?: number;
1182
- empty?: boolean;
1183
- };
1184
- mode?: string | {
1185
- not: string;
1186
- } | {
1187
- in: readonly string[];
1188
- };
1189
- capability?: CapabilityTag | readonly CapabilityTag[] | {
1190
- in: readonly CapabilityTag[];
1191
- } | {
1192
- not: CapabilityTag;
1193
- };
1194
- gesturing?: boolean;
1195
- actionIs?: string;
1196
- modifierHeld?: keyof ModifierState;
1197
- focused?: boolean;
1198
- hovering?: boolean;
1199
- hoveringSelected?: boolean;
1200
- zoomAtLeast?: number;
1201
- /** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
1202
- * mode. Absent flag is treated as `false`. */
1203
- editingAnchors?: boolean;
1204
- /** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
1205
- * (resizable), so `{ resizable: true }` passes for legacy ctx builders
1206
- * that don't compute it. */
1207
- resizable?: boolean;
1208
- /** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
1209
- * (touch, most styluses). Absent device is treated as `false`. */
1210
- coarsePointer?: boolean;
1211
- /** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
1212
- * device is treated as `true`. */
1213
- canHover?: boolean;
1214
- }
1215
- /**
1216
- * Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
1217
- * over `Selector` leaves. `when` is the escape hatch — its closure is
1218
- * opaque to introspection and should be avoided when a declarative form
1219
- * exists. Empty `all` is true; empty `any` is false.
1220
- */
1221
- type Rule = Selector | {
1222
- all: readonly Rule[];
1223
- } | {
1224
- any: readonly Rule[];
1225
- } | {
1226
- not: Rule;
1227
- } | {
1228
- when: (ctx: RuleCtx) => boolean;
1229
- };
1230
- /** Constant rules. Kept here so they have a single source. */
1231
- declare const ALWAYS: Rule;
1232
- /** A rule that never passes. `any` of nothing is false. */
1233
- declare const NEVER: Rule;
1234
- /**
1235
- * Render a {@link Rule} as a short human-readable string, for inspectors and
1236
- * diagnostics that need to say WHICH rule decided something.
1237
- *
1238
- * ```
1239
- * { capability: 'edits-page' } → capability:edits-page
1240
- * { all: [{ mode: 'normal' }, { focused: true }] } → all(mode:normal, focused:true)
1241
- * { not: { selection: { empty: true } } } → not(selection:empty=true)
1242
- * { when: function hasPath() { … } } → when(hasPath)
1243
- * ```
1244
- *
1245
- * Never throws and always terminates.
1246
- */
1247
- declare function describeRule(rule: Rule): string;
1248
- /** Evaluate a rule against a context. Pure, and cheap enough to run per
1249
- * frame for every piece of chrome. */
1250
- declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
1251
- /**
1252
- * Composable eligibility predicate with a fluent surface. `cond(ctx)`
1253
- * evaluates its `Rule` tree, carried at `.rule`; the fluent methods return new
1254
- * Conditions wrapping new trees.
1255
- *
1256
- * **Chain semantics: strict left-to-right, no precedence.** `a.and(b).or(c)` is
1257
- * `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`. For grouped
1258
- * disjunction, name the subexpression or use the top-level `or(...)`.
1259
- */
1260
- interface Condition {
1261
- (ctx: RuleCtx): boolean;
1262
- readonly rule: Rule;
1263
- /** `this && other` */
1264
- and(other: Condition | Rule): Condition;
1265
- /** `this || other` */
1266
- or(other: Condition | Rule): Condition;
1267
- /** `this && !other` */
1268
- andNot(other: Condition | Rule): Condition;
1269
- /** `this || !other` */
1270
- orNot(other: Condition | Rule): Condition;
1271
- }
1272
-
1273
- /**
1274
- * Whether two rules can never hold at once — no `RuleCtx` passes both.
1275
- *
1276
- * Conservative: `true` is a proof, `false` means "may overlap". It reasons
1277
- * about the selector values themselves (two different `mode`s, an empty
1278
- * selection against a non-empty one, `focused: true` against `false`) and
1279
- * through `all` / `any` / `not`; a `when` closure is opaque, so it overlaps
1280
- * everything. The route-conflict check uses it to stay quiet about bindings
1281
- * that share a tuple but are never eligible together.
1282
- */
1283
- declare function rulesExclusive(a: Rule, b: Rule): boolean;
1284
- /**
1285
- * Whether `rule` can hold while `mode` is active. Only its `mode` and
1286
- * `capability` tests are settled by the mode; every other test could go either
1287
- * way, so `false` is a proof and `true` means "may hold".
1288
- */
1289
- declare function ruleCanHoldIn(rule: Rule, mode: ActiveMode): boolean;
1290
-
1291
- /** The detected half of a profile — everything except the derived scale. */
1292
- type DetectedDeviceFacts = Omit<DeviceProfile, 'targetScale'>;
1293
- /**
1294
- * Handle/hit multiplier applied on a coarse pointer.
1295
- *
1296
- * 8px handle → 14px; 24px rotation distance → 42px. Counting the
1297
- * surrounding grab zone, that lands in the Apple HIG 44pt / Material 48dp
1298
- * minimum-touch-target band. One constant so it is tunable in one place.
1299
- */
1300
- declare const COARSE_TARGET_SCALE = 1.75;
1301
- /**
1302
- * Fold consumer overrides over detected facts and derive `targetScale`.
1303
- *
1304
- * `targetScale` is re-derived AFTER the merge, so an override of
1305
- * `coarsePointer` alone scales the chrome as expected. An explicit
1306
- * `targetScale` override wins over the derivation.
1307
- */
1308
- declare function resolveDeviceProfile(detected: DetectedDeviceFacts, overrides?: Partial<DeviceProfile>): DeviceProfile;
1309
- /**
1310
- * Assumed when `matchMedia` is unavailable (SSR, jsdom) and used as the
1311
- * absent-means value for `RuleCtx.device`.
1312
- *
1313
- * Derived from `resolveDeviceProfile` rather than hand-written, so it can
1314
- * never drift from what the function would compute for the same facts. A
1315
- * mouse-like device is the safe default: it is what the kit has always
1316
- * assumed, so an absent profile changes nothing for existing consumers.
1317
- */
1318
- declare const DEFAULT_DEVICE_PROFILE: DeviceProfile;
956
+ interface ImmediateInvoker {
957
+ timing: 'immediate';
958
+ /** `params` carries the matched binding's opts.params. Invoked from the
959
+ * command palette, or anywhere else with no per-binding context, `params`
960
+ * is undefined; descriptors should default to a sensible variant.
961
+ *
962
+ * Return `'pass'` when, having looked at the event, there is nothing to do
963
+ * with it — a scroll already at the end of its range — and the dispatcher
964
+ * walks on to the next matching binding, as for a declined `enabled()`.
965
+ * Prediction cannot run the invoker, so it still names this action. */
966
+ run(deps: ActionDeps, params?: Record<string, unknown>): void | 'pass';
967
+ }
968
+ /** Phase-machine invocation. `start` opens the phase and returns the handle
969
+ * the dispatcher pumps. */
970
+ interface OngoingInvoker {
971
+ timing: 'ongoing';
972
+ start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
973
+ /**
974
+ * Screen travel, in CSS pixels, a pointer drag makes before the dispatcher calls `start`; a
975
+ * binding's own `dragThresholdPx` wins over it. Until then the gesture is held for this action and
976
+ * nothing else takes it, and a release short of it ends the gesture with `start` never called.
977
+ * Unset, the action starts once the press is a drag at all (`gestures.dragPx`).
978
+ */
979
+ dragThresholdPx?: number;
980
+ }
981
+ /** Pluggable invocation strategy for an Action. */
982
+ type Invoker = ImmediateInvoker | OngoingInvoker;
1319
983
 
1320
984
  /**
1321
985
  * A settings form an action carries: what a surface draws in place of a bare
@@ -1351,6 +1015,54 @@ declare function readActionSettings(action: Action, deps: ActionDeps | undefined
1351
1015
  /** The params a change of the field at `path` to `value` triggers the action with. */
1352
1016
  declare function actionSettingsParams(settings: ActionSettings, path: string, value: unknown): Record<string, unknown> | null;
1353
1017
 
1018
+ /**
1019
+ * GestureBinding — connects a GestureSpec to an Action id (with per-binding
1020
+ * options). Tools own arrays of these on their `bindings` field; ambient
1021
+ * gesture-bindings are registered globally.
1022
+ */
1023
+
1024
+ /** An interaction: a gesture spec composed with the id of the action it
1025
+ * invokes. Tools declare arrays of these; the dispatcher matches an incoming
1026
+ * input event against them and runs the winner's action. */
1027
+ interface GestureBinding {
1028
+ spec: GestureSpec;
1029
+ actionId: string;
1030
+ opts?: BindingOpts;
1031
+ }
1032
+ /**
1033
+ * A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
1034
+ * (no per-binding opts) or an object form that pairs a spec with
1035
+ * `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
1036
+ * Use the object form when two bindings for the same action differ only in
1037
+ * a runtime parameter — the dispatcher extracts `opts.params` and passes
1038
+ * them to `ImmediateInvoker.run` as its second argument.
1039
+ */
1040
+ type BoundGesture = GestureSpec | {
1041
+ spec: GestureSpec;
1042
+ opts: BindingOpts;
1043
+ };
1044
+ /**
1045
+ * The whole of an action `actionBindings` reads. Routing needs this much and
1046
+ * none of `Action`'s palette fields, so it is the narrowest type the
1047
+ * dispatcher's binding walk can be written against.
1048
+ */
1049
+ interface BindingSource {
1050
+ id: string;
1051
+ /** The gesture-spec form of the binding, read by the gesture dispatcher.
1052
+ * May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
1053
+ * or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
1054
+ * `{ spec, opts }` — use the object form for parametric actions where two
1055
+ * bindings for the same action differ only by `opts.params` (e.g. `flip`
1056
+ * with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
1057
+ * passes them to `ImmediateInvoker.run` as its second argument. */
1058
+ defaultBinding?: GestureSpec | BoundGesture[];
1059
+ }
1060
+ /**
1061
+ * Flatten an action's `defaultBinding` into `GestureBinding`s. A bare
1062
+ * `GestureSpec` has `kind` at top level; the object form has `spec`.
1063
+ */
1064
+ declare function actionBindings(action: BindingSource): GestureBinding[];
1065
+
1354
1066
  /** What content being routed brings to `Action.enabled`: a drop's or a
1355
1067
  * paste's items, or a carry's payload with no items. */
1356
1068
  interface Carried {
@@ -1411,6 +1123,15 @@ interface ActionPresentation {
1411
1123
  * triggers it with the params `settings.params` makes. Not combined with
1412
1124
  * `variants`. */
1413
1125
  settings?: ActionSettings;
1126
+ /** The params the invoker reads, as a form: one leaf per param, keyed by its name, with its label, range,
1127
+ * step, and default. A run that commits one history entry is kept as the tree's last operation, which a panel
1128
+ * draws as this form so the params can be changed after the fact: any immediate run, and a gesture whose
1129
+ * handle can `replay` (see `OngoingHandle.replay`). `ActionParamSchema` stays what types a tuning; this is
1130
+ * what a person sees.
1131
+ *
1132
+ * A function answers the form for one run from the params it resolved, for an action whose params depend
1133
+ * on what it was asked to do (`insert`'s on the kind of shape), or `null` for a run with none to show. */
1134
+ params?: PrefGroup | ((params: Readonly<Record<string, unknown>>) => PrefGroup | null);
1414
1135
  }
1415
1136
  /**
1416
1137
  * One entry a parametric action contributes to a bar, menu or palette.
@@ -1562,20 +1283,452 @@ interface ActionDispatch extends BindingSource {
1562
1283
  /**
1563
1284
  * Single registered action.
1564
1285
  */
1565
- interface Action extends ActionDispatch, ActionPresentation {
1566
- }
1286
+ interface Action extends ActionDispatch, ActionPresentation {
1287
+ }
1288
+ /**
1289
+ * The whole of an actions registry the dispatcher reads. It walks the list
1290
+ * once per event to build its actionId lookup and never mutates it, so the
1291
+ * routing side depends on this rather than on `ActionsRegistry`, whose
1292
+ * register / mute / trigger surface is the React provider's.
1293
+ */
1294
+ interface ActionSource {
1295
+ list(): readonly Action[];
1296
+ }
1297
+ /** `action`'s hover cursor for an invocation given `params`: its `cursor`,
1298
+ * asked when it is a function. */
1299
+ declare function actionCursor(action: Pick<Action, 'cursor'> | undefined, params?: Record<string, unknown>): CursorSpec | undefined;
1300
+
1301
+ /**
1302
+ * Tuning: what a host changes about an action it did not write — the params
1303
+ * every invocation starts from, and bindings added beside the action's own —
1304
+ * without restating the descriptor. `ActionsRegistry.tune` holds tunings and
1305
+ * applies them to whatever descriptor is live for the id.
1306
+ */
1307
+
1308
+ /** An action's params, or a thunk read each time the action asks for them. */
1309
+ type ActionParams<P = Record<string, unknown>> = Partial<P> | (() => Partial<P>);
1310
+ /** A binding added for an action: a gesture alone, or a gesture with the
1311
+ * options it passes — the {@link BoundGesture} shape, its params typed. */
1312
+ type TunedBinding<P = Record<string, unknown>> = GestureSpec | {
1313
+ spec: GestureSpec;
1314
+ opts: Omit<BindingOpts, 'params'> & {
1315
+ params?: ActionParams<P>;
1316
+ };
1317
+ };
1318
+ /** What a host changes about one action. */
1319
+ interface ActionTuning<P = Record<string, unknown>> {
1320
+ /** Params every invocation starts from. What a binding or a `trigger` call
1321
+ * passes is laid over them, so these fill in what it leaves out. A thunk is
1322
+ * read on every call, and an ongoing action resolving its `opts.params`
1323
+ * mid-gesture sees the value as it is then. */
1324
+ params?: ActionParams<P>;
1325
+ /** Bindings added after the action's own `defaultBinding`. */
1326
+ bindings?: readonly TunedBinding<P>[];
1327
+ /** Keys for some of the action's `modifierRoles`, by role id. A binding's
1328
+ * own `opts.roles` is laid over these. */
1329
+ roles?: RoleRebinds;
1330
+ /** Told the params the last-operation panel just set for the action, which hold for the rest of the session.
1331
+ * A tuning whose `params` thunk reads a host's store writes the ones it supplies back there, so the store, and
1332
+ * whatever draws it, says what the next run will use rather than overriding it. */
1333
+ keep?(params: Readonly<Record<string, unknown>>): void;
1334
+ }
1335
+ /** `action` with each tuning applied in turn, a later one's params over an
1336
+ * earlier one's. Applying a tuning to a copy it already tuned changes nothing. */
1337
+ declare function tuneAction(action: Action, tunings: readonly ActionTuning[]): Action;
1338
+
1339
+ /**
1340
+ * The last operation: the latest run of an action that declares `params` and
1341
+ * committed one history entry, kept so its params can be changed after the
1342
+ * fact. A change undoes that entry and runs the action again in its place: an
1343
+ * immediate action is run again with the params it was handed, and a gesture
1344
+ * is replayed by its handle (`OngoingHandle.replay`).
1345
+ */
1346
+
1347
+ /** One operation that can still be adjusted. */
1348
+ interface LastOperation {
1349
+ id: string;
1350
+ /** What a panel titles it: the form's name, else the action's label. */
1351
+ label: string;
1352
+ /** The action's `params` form. */
1353
+ schema: PrefGroup;
1354
+ /** What it ran with, one value per leaf of `schema`, by the leaf's path. */
1355
+ values: Readonly<Record<string, unknown>>;
1356
+ }
1357
+ /** The last operation of one actions tree, and the two things to do with it. */
1358
+ interface LastOperationStore {
1359
+ /** The operation, or `null` once anything else reached the history, or it was dismissed. */
1360
+ get(): LastOperation | null;
1361
+ subscribe(listener: () => void): () => void;
1362
+ /**
1363
+ * Undo the operation's entry, put back the selection it ran on, and run it again with `patch` laid over its
1364
+ * values (a gesture is replayed by its handle), keeping the entry that makes in place of the old one: the
1365
+ * history, or the journal open on it, still holds one entry for it. The merged values become the action's
1366
+ * params for the rest of the session, under any tuning, and each tuning that keeps them is handed them
1367
+ * (`ActionTuning.keep`). Returns whether it ran.
1368
+ */
1369
+ adjust(patch: Readonly<Record<string, unknown>>): boolean;
1370
+ /** Forget the operation; the scene and the history stay as they are. */
1371
+ dismiss(): void;
1372
+ }
1373
+ /** Every selection a host holds, captured: calling what `take` returns puts all of it back. */
1374
+ interface SelectionSnapshotDep {
1375
+ take(): () => void;
1376
+ }
1377
+ /** What the store reads from the scope an operation ran in. */
1378
+ interface LastOperationHost {
1379
+ history(): History | null;
1380
+ takeSelection(): (() => void) | null;
1381
+ /** The params a run of `id` handed `given` starts with: every tuning's, `given` laid over them. */
1382
+ paramsOf(id: string, given: Record<string, unknown> | undefined): Record<string, unknown>;
1383
+ /** Run the action live for `id` here with `params`, as `trigger` would. */
1384
+ rerun(id: string, params: Record<string, unknown>): void;
1385
+ /** Hand `params`, which a panel change made `id`'s for the session, to each tuning of `id` that keeps them. */
1386
+ keep(id: string, params: Readonly<Record<string, unknown>>): void;
1387
+ }
1388
+ /** The store one actions tree keeps at its root. `onSession` is told when an action's session params change. */
1389
+ declare class LastOperationKeeper implements LastOperationStore {
1390
+ private readonly onSession;
1391
+ private held;
1392
+ private busy;
1393
+ private readonly listeners;
1394
+ private readonly session;
1395
+ constructor(onSession: () => void);
1396
+ readonly get: () => LastOperation | null;
1397
+ readonly subscribe: (listener: () => void) => (() => void);
1398
+ readonly dismiss: () => void;
1399
+ readonly adjust: (patch: Readonly<Record<string, unknown>>) => boolean;
1400
+ /** The tuning holding `id`'s session params, laid under every other; `null` before it has any. */
1401
+ sessionTuning(id: string): ActionTuning | null;
1402
+ /** `action` with its run kept as the last operation when it commits one entry: an immediate action declaring
1403
+ * `params`, or an ongoing one declaring them whose gesture's handle can `replay`, and in either case only a
1404
+ * run its `params` answers a form for. Any other action comes back as it is. */
1405
+ wrap(action: Action, host: LastOperationHost): Action;
1406
+ /** `invoker` with each gesture whose handle can `replay` kept as the last operation when its commit leaves
1407
+ * one entry. The selection is taken at the press and the params at the release, as the commit read them. */
1408
+ private ongoing;
1409
+ private hold;
1410
+ private remember;
1411
+ }
1412
+
1413
+ /** What a key no open keymap binds does: `'modal'` swallows it and leaves the
1414
+ * keymap open; `'cascade'` closes the keymap and dispatches it as if none were. */
1415
+ type KeymapUnbound = 'modal' | 'cascade';
1416
+ /** What holds a level opened with `{ hold: true }` open: no key releases it, so it closes on Escape. */
1417
+ declare const KEYMAP_HOLD = "hold";
1418
+ /** One open level of a {@link KeymapStack}. */
1419
+ interface OpenKeymap {
1420
+ readonly id: string;
1421
+ readonly label: string;
1422
+ /** The keymap's bindings as declared, for a hint to list. */
1423
+ readonly bindings: readonly GestureBinding[];
1424
+ /** The gesture id of the leader holding this level open, while it is held,
1425
+ * or {@link KEYMAP_HOLD}. */
1426
+ readonly heldBy?: string;
1427
+ /** Whether input arrived while the leader was held, so its release closes the level. */
1428
+ readonly usedWhileHeld: boolean;
1429
+ }
1430
+ /** What the keymaps made of the raw input in progress: `'taken'` when one of
1431
+ * them handled it, `'cascaded'` when it went on past them, `null` before either. */
1432
+ type KeymapVerdict = 'taken' | 'cascaded' | null;
1433
+ /** The keymaps open on one surface, innermost last; a canvas's `keymaps` dep. */
1434
+ interface KeymapStack {
1435
+ /** Every open level, outermost first. */
1436
+ open(): readonly OpenKeymap[];
1437
+ innermost(): OpenKeymap | null;
1438
+ /** Open a level. `heldBy` names what holds it open; `bindings` are what a hint lists. */
1439
+ push(id: string, label: string, opts?: {
1440
+ heldBy?: string;
1441
+ bindings?: readonly GestureBinding[];
1442
+ }): void;
1443
+ /** Mark the levels the last raw input opened as held by `heldBy`: the key
1444
+ * that opened them is still down. False when it opened none. */
1445
+ holdOpened(heldBy: string): boolean;
1446
+ /** The leader `heldBy` came up: close its level, those above it and the
1447
+ * tapped ones beneath it if input arrived meanwhile, else leave it open
1448
+ * for one more input. */
1449
+ release(heldBy: string): void;
1450
+ /** Close the innermost level: Escape. */
1451
+ closeInnermost(): void;
1452
+ /** One of the keymaps took this input, or a pointer gesture went past them.
1453
+ * Marks held levels used and closes once-levels down to the innermost held one. */
1454
+ inputSeen(): void;
1455
+ /** A key no keymap binds was swallowed: held levels count it as input, and nothing closes. */
1456
+ swallowed(): void;
1457
+ /** This input went on past the keymaps: as `inputSeen`, remembered as a cascade. */
1458
+ cascaded(): void;
1459
+ /** What the keymaps made of the input in progress. */
1460
+ verdict(): KeymapVerdict;
1461
+ closeAll(): void;
1462
+ /** Run `fn` as one raw input: closes made inside apply when it returns. Nests. */
1463
+ batch<T>(fn: () => T): T;
1464
+ subscribe(listener: () => void): () => void;
1465
+ getVersion(): number;
1466
+ }
1467
+ /** An empty {@link KeymapStack}. `useGestureDispatcher` makes one per surface. */
1468
+ declare function createKeymapStack(): KeymapStack;
1469
+
1470
+ /**
1471
+ * Re-exports the pure matcher primitives from `@weasel-js/gestures` and layers
1472
+ * the actions-layer binding-scope / matchBest logic on top.
1473
+ */
1474
+
1475
+ /** Where a binding came from, which is also its priority: a held hotkey beats
1476
+ * the active tool, which beats bindings that are always in scope. */
1477
+ type BindingScope = 'ambient' | 'active' | 'hotkey';
1478
+ /** A binding paired with where it came from, ready to be matched against an
1479
+ * event. */
1480
+ interface ScopedBinding {
1481
+ binding: GestureBinding;
1482
+ scope: BindingScope;
1483
+ /** Id of the tool or contribution that declared this binding —
1484
+ * `'&'`-channel phase atoms resolve to it. `null` for a registered
1485
+ * action's own binding. */
1486
+ ownerId: string | null;
1487
+ }
1488
+ /** The binding that won a match. */
1489
+ interface MatchResult {
1490
+ binding: GestureBinding;
1491
+ scope: BindingScope;
1492
+ /** Id of the entry that declared the binding — propagated from
1493
+ * `ScopedBinding` so the dispatcher can record it as the handle owner. */
1494
+ ownerId: string | null;
1495
+ }
1496
+ /**
1497
+ * True when a spec's target actually consults the affordance hit rather than
1498
+ * only the body classification. `kindOf` predicates are handed the hit;
1499
+ * `affordance:<k>` matches on its `kind`. The body-class strings (`'empty'`,
1500
+ * `'selected-body'`, `'unselected-body'`) and the `kind:` forms resolve from
1501
+ * `bodyTarget` / `bodyKind` and never see it.
1502
+ *
1503
+ * A predicate declares which it is with `readsAffordance`; one that declares
1504
+ * nothing, which only untyped code can write, counts as not reading the hit.
1505
+ */
1506
+ declare function targetConsultsAffordance(specTarget: TargetSpec | undefined): boolean;
1507
+ /** The first of {@link matchSorted}'s matches, or null when nothing matches. */
1508
+ declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>): MatchResult | null;
1509
+ /**
1510
+ * Find every binding that matches the event, sorted by precedence
1511
+ * (best → worst). Ordering rules:
1512
+ *
1513
+ * - Scopes by priority: hotkey > active > ambient.
1514
+ * - Within a scope, more-specific bindings beat less-specific (see
1515
+ * `specificity()`).
1516
+ * - Among same-specificity bindings, first-declared wins.
1517
+ */
1518
+ declare function matchSorted(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, warn?: (message: string) => void): MatchResult[];
1519
+ /** {@link matchSorted}, plus what an exclusive claim on the event barred. */
1520
+ interface MatchOutcome {
1521
+ matches: MatchResult[];
1522
+ /** Bindings whose spec matched the event but which the claim barred, in
1523
+ * the same order `matches` uses. Filled only when `collectBarred` is set. */
1524
+ barred: MatchResult[];
1525
+ /** Owner of the exclusive claim, when one applied. */
1526
+ claimOwner?: string;
1527
+ }
1528
+ /**
1529
+ * {@link matchSorted} that can also report the bindings an exclusive claim
1530
+ * kept out. Collecting them costs a spec match per barred binding, so it is
1531
+ * opt-in — the dispatcher asks only when it is recording.
1532
+ */
1533
+ declare function matchSortedWithBarred(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, opts?: {
1534
+ collectBarred?: boolean;
1535
+ warn?: (message: string) => void;
1536
+ }): MatchOutcome;
1537
+
1567
1538
  /**
1568
- * The whole of an actions registry the dispatcher reads. It walks the list
1569
- * once per event to build its actionId lookup and never mutates it, so the
1570
- * routing side depends on this rather than on `ActionsRegistry`, whose
1571
- * register / mute / trigger surface is the React provider's.
1539
+ * How one input was resolved: every binding that matched it, what each filter
1540
+ * dropped, the order the rest were ranked in and why, and what the walk down
1541
+ * that order did. The dispatcher fills one per input in dev builds, and
1542
+ * `Dispatcher.explain` fills one on request without invoking anything.
1543
+ *
1544
+ * Plain data — no functions, no live objects — so it can be kept in a trace
1545
+ * buffer, serialized, or built by hand as a fixture.
1572
1546
  */
1573
- interface ActionSource {
1574
- list(): readonly Action[];
1547
+ interface DispatchRecord {
1548
+ kind: 'dispatch';
1549
+ ts: number;
1550
+ input: DispatchRecordInput;
1551
+ /** Every binding whose spec matched the input, in matcher order, including
1552
+ * those a filter then dropped. */
1553
+ matched: readonly RecordCandidate[];
1554
+ dropped: readonly DroppedCandidate[];
1555
+ /** The candidates that survived both filters, best first. */
1556
+ ranked: readonly RankedCandidate[];
1557
+ /** True when nothing was invoked: the record answers "what would happen". */
1558
+ predicted: boolean;
1559
+ /** Action id of the candidate that fired (or would), else `null`. */
1560
+ fired: string | null;
1561
+ outcome: 'handled' | 'unhandled';
1575
1562
  }
1576
- /** `action`'s hover cursor for an invocation given `params`: its `cursor`,
1577
- * asked when it is a function. */
1578
- declare function actionCursor(action: Pick<Action, 'cursor'> | undefined, params?: Record<string, unknown>): CursorSpec | undefined;
1563
+ /** The input a {@link DispatchRecord} describes, reduced to plain data. */
1564
+ interface DispatchRecordInput {
1565
+ /** `InputEvent.kind`: `'key'`, `'pointerdown'`, `'wheel'`, … */
1566
+ eventKind: string;
1567
+ /** For `key` / `key-held` events, the key id (`'Escape'`, `' '`, `'a'`). */
1568
+ key?: string;
1569
+ modifiers: {
1570
+ alt: boolean;
1571
+ ctrl: boolean;
1572
+ meta: boolean;
1573
+ shift: boolean;
1574
+ };
1575
+ /** The view the input was routed to; `null` for the surface's own camera. */
1576
+ viewId: string | null;
1577
+ /** World point, for events that carry one. */
1578
+ world?: {
1579
+ x: number;
1580
+ y: number;
1581
+ };
1582
+ /** Active mode id, when the host supplies a rule context. */
1583
+ mode?: string;
1584
+ }
1585
+ /** One matched binding. */
1586
+ interface RecordCandidate {
1587
+ actionId: string;
1588
+ /** The binding's spec in route grammar — one per arg alternative. */
1589
+ routes: readonly string[];
1590
+ scope: BindingScope;
1591
+ /** The tool or contribution that declared the binding; `null` for an action's own binding. */
1592
+ ownerId: string | null;
1593
+ /** Whether the binding's `views` names the routed view. */
1594
+ namesView: boolean;
1595
+ /** `specificity(spec)`: target, modifiers, phase, exact. */
1596
+ specificity: readonly [number, number, number, number];
1597
+ /** The action's `eligible` rule as text, when it declares one. */
1598
+ eligible?: string;
1599
+ }
1600
+ /** A matched candidate a filter removed before ranking, and which filter. */
1601
+ type DroppedCandidate =
1602
+ /** An exclusive claim on the input barred bindings that do not read the
1603
+ * claim's affordance. */
1604
+ {
1605
+ candidate: RecordCandidate;
1606
+ filter: 'claim';
1607
+ owner?: string;
1608
+ }
1609
+ /** The action's `eligible` rule does not hold. */
1610
+ | {
1611
+ candidate: RecordCandidate;
1612
+ filter: 'ineligible';
1613
+ rule: string;
1614
+ };
1615
+ /** One field of the `specificity` tuple, in the order it is compared. */
1616
+ type SpecificityPart = 'target' | 'mods' | 'phase' | 'exact';
1617
+ /** The ranking step that put a candidate below the one before it. */
1618
+ type PlacedBy = {
1619
+ step: 'first';
1620
+ }
1621
+ /** The one above names the routed view; this one does not. */
1622
+ | {
1623
+ step: 'view';
1624
+ }
1625
+ /** The one above sits in a higher scope tier. */
1626
+ | {
1627
+ step: 'tier';
1628
+ }
1629
+ /** Same tier; the one above is more specific in `part`. */
1630
+ | {
1631
+ step: 'specificity';
1632
+ part: SpecificityPart;
1633
+ }
1634
+ /** Tied; the one above is gated by an `eligible` rule that holds. */
1635
+ | {
1636
+ step: 'context';
1637
+ }
1638
+ /** Tied on everything; registration order. */
1639
+ | {
1640
+ step: 'order';
1641
+ };
1642
+ /** What the walk did with one ranked candidate. */
1643
+ type WalkStep = {
1644
+ kind: 'fired';
1645
+ }
1646
+ /** Predicted records only: this candidate would fire. Nothing was started,
1647
+ * so an ongoing action here may still bail at `start()` for real. */
1648
+ | {
1649
+ kind: 'would-fire';
1650
+ }
1651
+ /** `enabled()` returned this reason. */
1652
+ | {
1653
+ kind: 'declined';
1654
+ reason: string;
1655
+ }
1656
+ /** An ongoing action's `start()` returned an empty handle, or an
1657
+ * immediate one's `run()` returned `'pass'`. */
1658
+ | {
1659
+ kind: 'empty-handle';
1660
+ }
1661
+ /** An ongoing action bound to a press-time `pointerDown` spec; refused. */
1662
+ | {
1663
+ kind: 'misbound';
1664
+ }
1665
+ /** Its action was already tried higher up; each runs at most once. */
1666
+ | {
1667
+ kind: 'duplicate';
1668
+ } | {
1669
+ kind: 'no-such-action';
1670
+ }
1671
+ /** Below the winner; never asked. */
1672
+ | {
1673
+ kind: 'not-asked';
1674
+ }
1675
+ /** Below the winner, asked anyway (`evaluateShadowed`), and it passed. */
1676
+ | {
1677
+ kind: 'outranked';
1678
+ };
1679
+ /** A candidate that survived the filters: why it sits where it does, and
1680
+ * what the walk did with it. */
1681
+ interface RankedCandidate {
1682
+ candidate: RecordCandidate;
1683
+ placedBy: PlacedBy;
1684
+ walk: WalkStep;
1685
+ }
1686
+
1687
+ /** One entry per "mode change" — a kit state transition that isn't a
1688
+ * dispatched input event but explains why an input was (or wasn't) handled
1689
+ * later. Pushed via {@link DispatchTrace.recordModeSwitch}. */
1690
+ interface ModeSwitchLogEntry {
1691
+ kind: 'mode';
1692
+ ts: number;
1693
+ /** Logical mode name, e.g. `'editAnchors.editingId'`. */
1694
+ mode: string;
1695
+ from: string | null;
1696
+ to: string | null;
1697
+ /** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
1698
+ * `'selection-dropped target'`, etc. */
1699
+ detail?: string;
1700
+ }
1701
+ /** A trace entry: one {@link DispatchRecord} per resolved input, or a mode switch. */
1702
+ type TraceLogEntry = DispatchRecord | ModeSwitchLogEntry;
1703
+ /** A rolling buffer that records in a dev build, and in any build while something watches it. */
1704
+ interface TraceRing<T> {
1705
+ /** Whether a push now keeps its entry. A caller building an entry checks first. */
1706
+ recording(): boolean;
1707
+ push(entry: T): void;
1708
+ /** The entries pushed after `cursor`, oldest first, and the cursor to pass
1709
+ * next time. Start from 0. Entries the buffer has dropped are skipped. */
1710
+ since(cursor: number): {
1711
+ entries: T[];
1712
+ cursor: number;
1713
+ };
1714
+ /** Turn recording on until the returned release is called. */
1715
+ watch(): () => void;
1716
+ /** Hear each kept push. Returns the unsubscribe. */
1717
+ subscribe(listener: () => void): () => void;
1718
+ }
1719
+ /** A {@link TraceRing} keeping the last `limit` entries. `dev` defaults to the build's. */
1720
+ declare function createTraceRing<T>(limit?: number, dev?: boolean): TraceRing<T>;
1721
+ /** One canvas's dispatch trace. `useGestureDispatcher` makes one, puts it on
1722
+ * every `DispatcherContext` it builds, and publishes it as the `dispatchTrace` dep. */
1723
+ interface DispatchTrace extends TraceRing<TraceLogEntry> {
1724
+ recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
1725
+ /** Offer the record for what a press at the pointer would do, built only
1726
+ * when {@link DispatchTrace.live} reads it. `null` withdraws it. */
1727
+ setLive(explain: (() => DispatchRecord) | null): void;
1728
+ live(): DispatchRecord | null;
1729
+ }
1730
+ /** A {@link DispatchTrace} keeping the last `limit` entries. `dev` defaults to the build's. */
1731
+ declare function createDispatchTrace(limit?: number, dev?: boolean): DispatchTrace;
1579
1732
 
1580
1733
  /** Which canvas hit a menu answers. Every field given must match. */
1581
1734
  interface MenuWhen {
@@ -1691,6 +1844,60 @@ declare function createMenuHost(src: MenuSources & {
1691
1844
  activeTool(): string | null;
1692
1845
  }): MenuHost;
1693
1846
 
1847
+ /**
1848
+ * Tool options: the settings a tool's user adjusts while it is active, such as
1849
+ * a brush's size and strength. A tool declares them as a prefs group whose
1850
+ * number leaves are params of the action it binds; the host holds their values
1851
+ * (`toolOptionValues` dep) and `<SceneCanvas>` tunes the action from them. A tool
1852
+ * whose options name a size or a strength also gets step actions for them,
1853
+ * bound to `[` `]` and `Shift+[` `Shift+]` while it is the active tool.
1854
+ */
1855
+
1856
+ /** How a step action moves one option: by a factor, or by an amount. */
1857
+ interface ToolOptionStep {
1858
+ /** The option's key in `prefs`. */
1859
+ option: string;
1860
+ /** Each step multiplies (or divides) the value by this. */
1861
+ times?: number;
1862
+ /** Each step adds (or takes away) this. Used when `times` is absent. */
1863
+ plus?: number;
1864
+ }
1865
+ /** What a tool declares about its options. See {@link ToolDef.options}. */
1866
+ interface ToolOptions {
1867
+ /** The action the options are params of: each leaf of `prefs` is the param
1868
+ * of the same key. */
1869
+ actionId: string;
1870
+ /** The options, as number leaves keyed by param, with their bounds and
1871
+ * defaults. A host draws them from this (a group drawn `as: 'bar'` makes an
1872
+ * options row) and keeps their values in its prefs. */
1873
+ prefs: PrefGroup;
1874
+ /** The option `[` and `]` step: how large the tool acts. */
1875
+ size?: ToolOptionStep;
1876
+ /** The option `Shift+[` and `Shift+]` step: how hard it acts. */
1877
+ strength?: ToolOptionStep;
1878
+ }
1879
+ /** Where a host keeps tool options' values: the `toolOptionValues` dep. */
1880
+ interface ToolOptionValues {
1881
+ /** `toolId`'s values, sparse: an option missing from it reads its default.
1882
+ * The same object until one of them changes. */
1883
+ get(toolId: string): Readonly<Record<string, number>>;
1884
+ set(toolId: string, option: string, value: number): void;
1885
+ subscribe(listener: () => void): () => void;
1886
+ }
1887
+ /** A {@link ToolOptionValues} held in memory, for a host that keeps no prefs. */
1888
+ declare function createToolOptionValues(): ToolOptionValues;
1889
+ /** Every option's value for `toolId`: what `values` holds, else its default. */
1890
+ declare function toolOptionParams(toolId: string, options: ToolOptions, values: ToolOptionValues | undefined): Record<string, number>;
1891
+ /** `value` moved one step up (`dir` 1) or down (-1), within the leaf's bounds
1892
+ * and rounded to its `step`. */
1893
+ declare function steppedOption(leaf: PrefNumber, value: number, step: ToolOptionStep, dir: 1 | -1): number;
1894
+ /** The step actions for `toolId`'s size and strength options, and the key
1895
+ * bindings that reach them. */
1896
+ declare function toolOptionSteps(toolId: string, toolLabel: string, options: ToolOptions): {
1897
+ actions: Action[];
1898
+ bindings: GestureBinding[];
1899
+ };
1900
+
1694
1901
  /**
1695
1902
  * The tree both input registries hang on. A provider is a root; every canvas
1696
1903
  * mounts a scope under the registry in scope; a yoke sits between the two. A
@@ -1791,40 +1998,6 @@ declare function ActiveToolContextProviderIfRoot({ children, }: {
1791
1998
  children: ReactNode;
1792
1999
  }): react.JSX.Element;
1793
2000
 
1794
- /**
1795
- * Tuning: what a host changes about an action it did not write — the params
1796
- * every invocation starts from, and bindings added beside the action's own —
1797
- * without restating the descriptor. `ActionsRegistry.tune` holds tunings and
1798
- * applies them to whatever descriptor is live for the id.
1799
- */
1800
-
1801
- /** An action's params, or a thunk read each time the action asks for them. */
1802
- type ActionParams<P = Record<string, unknown>> = Partial<P> | (() => Partial<P>);
1803
- /** A binding added for an action: a gesture alone, or a gesture with the
1804
- * options it passes — the {@link BoundGesture} shape, its params typed. */
1805
- type TunedBinding<P = Record<string, unknown>> = GestureSpec | {
1806
- spec: GestureSpec;
1807
- opts: Omit<BindingOpts, 'params'> & {
1808
- params?: ActionParams<P>;
1809
- };
1810
- };
1811
- /** What a host changes about one action. */
1812
- interface ActionTuning<P = Record<string, unknown>> {
1813
- /** Params every invocation starts from. What a binding or a `trigger` call
1814
- * passes is laid over them, so these fill in what it leaves out. A thunk is
1815
- * read on every call, and an ongoing action resolving its `opts.params`
1816
- * mid-gesture sees the value as it is then. */
1817
- params?: ActionParams<P>;
1818
- /** Bindings added after the action's own `defaultBinding`. */
1819
- bindings?: readonly TunedBinding<P>[];
1820
- /** Keys for some of the action's `modifierRoles`, by role id. A binding's
1821
- * own `opts.roles` is laid over these. */
1822
- roles?: RoleRebinds;
1823
- }
1824
- /** `action` with each tuning applied in turn, a later one's params over an
1825
- * earlier one's. Applying a tuning to a copy it already tuned changes nothing. */
1826
- declare function tuneAction(action: Action, tunings: readonly ActionTuning[]): Action;
1827
-
1828
2001
  /**
1829
2002
  * The entries an action contributes to a bar, menu or palette — one per
1830
2003
  * variant, or the action itself when it declares none.
@@ -3045,7 +3218,9 @@ declare function modeShortcutSpec(shortcut: string): KeySpec;
3045
3218
  * The bindings ride the hotkey tier, so leaving a mode outranks what the same
3046
3219
  * key does otherwise (Escape clearing the selection), and they yield while a
3047
3220
  * gesture is in flight, so Escape mid-drag still cancels the drag first. Keys
3048
- * typed into an editable element never reach them.
3221
+ * typed into an editable element never reach them. A mode's `exit` and
3222
+ * `cancel`, pressed while a tool other than its `defaultTool` is active,
3223
+ * return to that tool instead (`returnToModeDefault`), so the next press leaves.
3049
3224
  */
3050
3225
  declare function modeShortcuts(registry: ModeRegistry, handlers: ModeShortcutHandlers): Contribution<never>;
3051
3226
 
@@ -3203,6 +3378,10 @@ interface Tool<TOverlay = KernelOverlay> extends Contribution<TOverlay> {
3203
3378
  * `opts.params.toolId` set so the invoker knows which tool to switch to).
3204
3379
  * A tool with none has no activation key. */
3205
3380
  keybinding?: ToolKeybinding;
3381
+ /** The settings its user adjusts while it is active; see `ToolDef.options`. */
3382
+ options?: ToolOptions;
3383
+ /** Chrome hidden while this is the active tool; see `ToolDef.hides`. */
3384
+ hides?: readonly string[];
3206
3385
  cursor?: CursorSpec | (() => CursorSpec);
3207
3386
  /** The tool became live: it took the active slot, or its `hotkey` went
3208
3387
  * down, while it held neither. A lifecycle moment has no pointer, hit or
@@ -3250,6 +3429,17 @@ interface ToolDef<TOverlay = KernelOverlay> {
3250
3429
  actions?: Action[];
3251
3430
  /** Menus this tool adds while it is active. */
3252
3431
  menus?: MenuDef[];
3432
+ /**
3433
+ * The settings its user adjusts while it is active, as params of the action
3434
+ * it binds: a brush's size and strength. `<SceneCanvas>` tunes that action
3435
+ * from the `toolOptionValues` dep, and `defineTool` adds step actions for the
3436
+ * size and strength, keyed to `[` `]` and `Shift+[` `Shift+]` while this is
3437
+ * the active tool. See {@link import('./toolOptions').ToolOptions}.
3438
+ */
3439
+ options?: ToolOptions;
3440
+ /** Chrome ids (`'selection.resize-handles'`) hidden, and not hit-tested,
3441
+ * while this is the active tool: chrome its own controls stand in for. */
3442
+ hides?: readonly string[];
3253
3443
  /** Hook name as exported from the kit barrel (e.g. `'useHandTool'`).
3254
3444
  * Set by built-in hooks for inspector / debugging. Consumer-authored
3255
3445
  * tools may set this to surface their hook name; omitted is fine.
@@ -3306,6 +3496,15 @@ interface ToolDef<TOverlay = KernelOverlay> {
3306
3496
  */
3307
3497
  declare function defineTool<TOverlay = KernelOverlay>(def: ToolDef<TOverlay>): Tool<TOverlay>;
3308
3498
 
3499
+ /**
3500
+ * Make the active mode's default tool active when another tool holds the
3501
+ * slot, and say whether it did. A key that leaves a mode calls this first, so
3502
+ * one press returns to the mode's tool and the next leaves: the mode shortcuts'
3503
+ * `exit` and `cancel`, and core's `exitPathEdit`. Reads the `activeTool` and
3504
+ * `modeDefaultTool` deps, which the caller must require.
3505
+ */
3506
+ declare function returnToModeDefault(deps: ActionDeps | undefined): boolean;
3507
+
3309
3508
  /**
3310
3509
  * Tool sets: a named group of tool ids, the unit a consumer opts into whole.
3311
3510
  * A set names ids, never tools, because a tool is built by a hook that holds
@@ -3754,12 +3953,20 @@ declare function pickMenu(defs: readonly MenuDef[], hit: MenuHit): MenuDef | und
3754
3953
  interface DepSchema {
3755
3954
  /** Currently active tool id + hotkey-hold stack. */
3756
3955
  activeTool: ActiveToolContextValue;
3956
+ /** Where the host keeps its tools' option values (`ToolDef.options`).
3957
+ * Absent: every option is its default, and the step actions do nothing. */
3958
+ toolOptionValues?: ToolOptionValues;
3757
3959
  /** The menu host of the `ActionsProvider` in scope. */
3758
3960
  menuHost?: MenuHost;
3759
3961
  /** The canvas's dispatch trace, for a dep to record a mode switch into. */
3760
3962
  dispatchTrace?: DispatchTrace;
3761
3963
  /** The keymaps open on the canvas: what a leader opens and a hint reads. */
3762
3964
  keymaps?: KeymapStack;
3965
+ /** Every selection the host holds, captured so the last operation can put it back before it reruns. */
3966
+ selectionSnapshot?: SelectionSnapshotDep;
3967
+ /** The tool the active mode starts on (`ModeDefinition.defaultTool`), when the canvas mounts it;
3968
+ * else null. `<SceneCanvas>` sources it. */
3969
+ modeDefaultTool?: () => string | null;
3763
3970
  }
3764
3971
  /** Every dep name declared by anything that has merged into {@link DepSchema}. */
3765
3972
  type DepName = keyof DepSchema;
@@ -3807,4 +4014,4 @@ type KernelOverlay = OverlaySchema extends {
3807
4014
  overlay: infer O;
3808
4015
  } ? O : unknown;
3809
4016
 
3810
- export { type BindingOverrides as $, type Action as A, type BoundGesture as B, type Contribution as C, type DepRegistry as D, type ActionParams as E, type ActionPresentation as F, type ActionSettings as G, type ActionSettingsState as H, type ActionShortcut as I, type ActionSource as J, type KeymapDescription as K, type LongPressOptions as L, type MenuHost as M, type NodeId as N, type OverlayPosition as O, type ActionTuning as P, type ActionTunings as Q, type RuleCtx as R, ScopeNode as S, ToolNode as T, type ActionUnavailable as U, type ActionsProp as V, type WorldPoint as W, type AnyTool as X, type AssemblyInputs as Y, BROWSER_RESERVED_CHORDS as Z, type BindingOpts as _, type DepName as a, type ModeSwitchLogEntry as a$, type BindingScope as a0, type BindingSource as a1, type BodyPredicate as a2, type Bounds as a3, type BuildRuleCtxArgs as a4, COARSE_TARGET_SCALE as a5, type CaptureGestureOptions as a6, type CaptureKeyOptions as a7, type CaptureKind as a8, type CapturedSpec as a9, type HandleKind as aA, type HitShape as aB, type HotkeyTrigger as aC, INGEST_STRING_MIMES as aD, type ImmediateInvoker as aE, type ImmediateParams as aF, type InvocationCtx as aG, type Invoker as aH, KEYMAP_HOLD as aI, type KeyBinding as aJ, type KeyReach as aK, type KeymapDef as aL, type KeymapRow as aM, type KeymapStack as aN, type KeymapUnbound as aO, type KeymapVerdict as aP, type LongPressState as aQ, type LongPressStore as aR, type MatchOutcome as aS, type MatchResult as aT, type MenuAccess as aU, type MenuCustomAt as aV, type MenuHit as aW, type MenuOpenRequest as aX, type MenuSources as aY, type MenuWhen as aZ, type ModeShortcutHandlers as a_, type Carried as aa, type Carry as ab, type CarryCallbacks as ac, type CarryPayload as ad, type CarryPointer as ae, type CarryStage as af, type Condition as ag, type Conflict as ah, type ContributionChrome as ai, type ContributionDeps as aj, type ContributionRouting as ak, DEFAULT_ALLOWED_CAPABILITIES as al, DEFAULT_DEVICE_PROFILE as am, type DebugSink as an, type DepListing as ao, type DetectedDeviceFacts as ap, type DeviceProfile as aq, type DispatchRecord as ar, type DispatchRecordInput as as, type DispatcherContext as at, type DragSample as au, type DroppedCandidate as av, type Eligibility as aw, type EligibilityState as ax, type GestureBinding as ay, type GestureKitSettings as az, type DepSource as b, assembleEntries as b$, type ModifierRole as b0, type ModifierRoles as b1, type ModifierState as b2, NEVER as b3, type OngoingHandle as b4, type OngoingInvoker as b5, type OngoingOverlay as b6, type OpenKeymap as b7, type OpenMenu as b8, type OverlayRole as b9, type SelectionApi as bA, type Selector as bB, type SpecificityPart as bC, TOOL_OFFHAND_ID as bD, type ToolDef as bE, type ToolEntry as bF, type ToolKeybinding as bG, type ToolModifiers as bH, type ToolOffhandBindingSpec as bI, type ToolPresentation as bJ, type ToolScopes as bK, type ToolSet as bL, type ToolSlot as bM, type TraceLogEntry as bN, type TraceRing as bO, type TunedBinding as bP, type UiOngoingControl as bQ, type ViewportGestureKind as bR, type WalkStep as bS, actionBindings as bT, actionCursor as bU, actionItemTitle as bV, actionItems as bW, actionSettingsParams as bX, actionShortcuts as bY, actionsTagged as bZ, admitIngestMime as b_, type OverlaySchema as ba, type OwnedMenuDef as bb, PREDICATE_TARGET as bc, type PendingIngestItem as bd, type PendingLongPress as be, type PlacedBy as bf, type Point2 as bg, type PointerSession as bh, type PointerSessionCallbacks as bi, type PointerSessionCancelReason as bj, type PointerSessionOptions as bk, type RankedCandidate as bl, type RecordCandidate as bm, type RegistryEntry as bn, type ReservedChord as bo, type ResizeAnchor as bp, type ResolveAllOptions as bq, type ResolveOnlyResult as br, type ResolvedBindingOverrides as bs, type ResolvedCandidate as bt, type RoleKeys as bu, type RoleRebinds as bv, type Rule as bw, type ScopedBinding as bx, type ScratchKey as by, type ScratchStore as bz, type ActionDeps as c, matchSortedWithBarred as c$, bindingId as c0, browserReservedChord as c1, buildDepsFromRequires as c2, buildRouteRegistry as c3, buildRuleCtx as c4, buildToolOffhandBindings as c5, captureGesture as c6, captureKey as c7, clientToCanvas as c8, clientToCanvasRect as c9, isControlHandle as cA, isCornerWidget as cB, isDebugEnabled as cC, isEmpty as cD, isPathEditPart as cE, isResizeHandle as cF, isRotateHandle as cG, isSegment as cH, isSelectedBody as cI, isTool as cJ, isToolSet as cK, isUnselectedBody as cL, itemTypesFromDataTransfer as cM, itemsFromClipboardData as cN, itemsFromDataTransfer as cO, itemsFromFiles as cP, keySpecShortcut as cQ, keymapContribution as cR, keymapLeaderBindingId as cS, keymapLeaderId as cT, keymapShortcuts as cU, keysHeld as cV, liveScope as cW, makeToolOffhandAction as cX, matchActionItem as cY, matchBest as cZ, matchSorted as c_, createDepRegistry as ca, createDispatchTrace as cb, createDispatcher as cc, createKeymapStack as cd, createLongPressStore as ce, createMenuHost as cf, createTraceRing as cg, defineTool as ch, deleteScratch as ci, describeKeymap as cj, describeRule as ck, dlog as cl, dwarn as cm, evaluate as cn, evaluateActive as co, evaluateEnabled as cp, findConflicts as cq, getScratch as cr, heldKeySpec as cs, historyKey as ct, holdPreview as cu, holdPreviewId as cv, isAnchor as cw, isAnchorOrControl as cx, isBalanceGrip as cy, isBody as cz, type ActionsRegistry as d, matchesKeyBinding as d0, mergeContributions as d1, mergeMenuDefs as d2, modeShortcutSpec as d3, modeShortcuts as d4, modifierStateOf as d5, multiTouchGestureId as d6, offhandKeyFor as d7, openPointerSession as d8, overlaysOf as d9, validateActionDefaultBinding as dA, validateActionId as dB, withoutTools as dC, overriddenBindings as da, pendingKindOf as db, pickMenu as dc, pointerGestureId as dd, readActionSettings as de, reportRouteConflicts as df, resolveBindingOverrides as dg, resolveDeviceProfile as dh, resolveMenu as di, resolveParams as dj, resolveSchemaMenu as dk, roleReader as dl, routesForSpec as dm, ruleCanHoldIn as dn, rulesExclusive as dp, scopeBindings as dq, scratchKey as dr, setScratch as ds, startCarry as dt, taggedActionItems as du, targetConsultsAffordance as dv, toolIdsOf as dw, toolSet as dx, toolSetHas as dy, tuneAction as dz, ActionDisabledReason as e, type ActionVariant as f, type MenuDef as g, DepNode as h, type KernelOverlay as i, type Tool as j, ActiveToolContextProvider as k, ActiveToolContextProviderIfRoot as l, type ActiveToolContextProviderProps as m, type ActiveToolContextValue as n, useOptionalActiveToolContext as o, type Dispatcher as p, type AffordanceHit as q, type DispatchTrace as r, type DepSchema as s, ALWAYS as t, useActiveToolContext as u, type ActionDispatch as v, type ActionEnabledResult as w, type ActionEntry as x, type ActionItem as y, type ActionParamSchema as z };
4017
+ export { type AssemblyInputs as $, type Action as A, type BoundGesture as B, type Contribution as C, type DepRegistry as D, type ActionEntry as E, type ActionItem as F, type ActionParamSchema as G, type ActionParams as H, type ActionPresentation as I, type ActionSettings as J, type KeymapDescription as K, LastOperationKeeper as L, type MenuHost as M, type NodeId as N, type OverlayPosition as O, type ActionSettingsState as P, type ActionShortcut as Q, type RuleCtx as R, ScopeNode as S, ToolNode as T, type ActionSource as U, type ActionTuning as V, type WorldPoint as W, type ActionTunings as X, type ActionUnavailable as Y, type ActionsProp as Z, type AnyTool as _, type DepName as a, type MenuSources as a$, BROWSER_RESERVED_CHORDS as a0, type BindingOpts as a1, type BindingOverrides as a2, type BindingScope as a3, type BindingSource as a4, type BodyPredicate as a5, type Bounds as a6, type BuildRuleCtxArgs as a7, COARSE_TARGET_SCALE as a8, type CaptureGestureOptions as a9, type EligibilityState as aA, type GestureBinding as aB, type GestureKitSettings as aC, type HandleKind as aD, type HitShape as aE, type HotkeyTrigger as aF, INGEST_STRING_MIMES as aG, type ImmediateInvoker as aH, type ImmediateParams as aI, type InvocationCtx as aJ, type Invoker as aK, KEYMAP_HOLD as aL, type KeyBinding as aM, type KeyReach as aN, type KeymapDef as aO, type KeymapRow as aP, type KeymapStack as aQ, type KeymapUnbound as aR, type KeymapVerdict as aS, type LongPressState as aT, type LongPressStore as aU, type MatchOutcome as aV, type MatchResult as aW, type MenuAccess as aX, type MenuCustomAt as aY, type MenuHit as aZ, type MenuOpenRequest as a_, type CaptureKeyOptions as aa, type CaptureKind as ab, type CapturedSpec as ac, type Carried as ad, type Carry as ae, type CarryCallbacks as af, type CarryPayload as ag, type CarryPointer as ah, type CarryStage as ai, type Condition as aj, type Conflict as ak, type ContributionChrome as al, type ContributionDeps as am, type ContributionRouting as an, DEFAULT_ALLOWED_CAPABILITIES as ao, DEFAULT_DEVICE_PROFILE as ap, type DebugSink as aq, type DepListing as ar, type DetectedDeviceFacts as as, type DeviceProfile as at, type DispatchRecord as au, type DispatchRecordInput as av, type DispatcherContext as aw, type DragSample as ax, type DroppedCandidate as ay, type Eligibility as az, type DepSource as b, actionCursor as b$, type MenuWhen as b0, type ModeShortcutHandlers as b1, type ModeSwitchLogEntry as b2, type ModifierRole as b3, type ModifierRoles as b4, type ModifierState as b5, NEVER as b6, type OngoingHandle as b7, type OngoingInvoker as b8, type OngoingOverlay as b9, type ScopedBinding as bA, type ScratchKey as bB, type ScratchStore as bC, type SelectionApi as bD, type SelectionSnapshotDep as bE, type Selector as bF, type SpecificityPart as bG, TOOL_OFFHAND_ID as bH, type ToolDef as bI, type ToolEntry as bJ, type ToolKeybinding as bK, type ToolModifiers as bL, type ToolOffhandBindingSpec as bM, type ToolOptionStep as bN, type ToolOptionValues as bO, type ToolOptions as bP, type ToolPresentation as bQ, type ToolScopes as bR, type ToolSet as bS, type ToolSlot as bT, type TraceLogEntry as bU, type TraceRing as bV, type TunedBinding as bW, type UiOngoingControl as bX, type ViewportGestureKind as bY, type WalkStep as bZ, actionBindings as b_, type OpenKeymap as ba, type OpenMenu as bb, type OverlayRole as bc, type OverlaySchema as bd, type OwnedMenuDef as be, PREDICATE_TARGET as bf, type PendingIngestItem as bg, type PendingLongPress as bh, type PlacedBy as bi, type Point2 as bj, type PointerSession as bk, type PointerSessionCallbacks as bl, type PointerSessionCancelReason as bm, type PointerSessionOptions as bn, type RankedCandidate as bo, type RecordCandidate as bp, type RegistryEntry as bq, type ReservedChord as br, type ResizeAnchor as bs, type ResolveAllOptions as bt, type ResolveOnlyResult as bu, type ResolvedBindingOverrides as bv, type ResolvedCandidate as bw, type RoleKeys as bx, type RoleRebinds as by, type Rule as bz, type ActionDeps as c, keymapLeaderId as c$, actionItemTitle as c0, actionItems as c1, actionSettingsParams as c2, actionShortcuts as c3, actionsTagged as c4, admitIngestMime as c5, assembleEntries as c6, bindingId as c7, browserReservedChord as c8, buildDepsFromRequires as c9, heldKeySpec as cA, historyKey as cB, holdPreview as cC, holdPreviewId as cD, isAnchor as cE, isAnchorOrControl as cF, isBalanceGrip as cG, isBody as cH, isControlHandle as cI, isCornerWidget as cJ, isDebugEnabled as cK, isEmpty as cL, isPathEditPart as cM, isResizeHandle as cN, isRotateHandle as cO, isSegment as cP, isSelectedBody as cQ, isTool as cR, isToolSet as cS, isUnselectedBody as cT, itemTypesFromDataTransfer as cU, itemsFromClipboardData as cV, itemsFromDataTransfer as cW, itemsFromFiles as cX, keySpecShortcut as cY, keymapContribution as cZ, keymapLeaderBindingId as c_, buildRouteRegistry as ca, buildRuleCtx as cb, buildToolOffhandBindings as cc, captureGesture as cd, captureKey as ce, clientToCanvas as cf, clientToCanvasRect as cg, createDepRegistry as ch, createDispatchTrace as ci, createDispatcher as cj, createKeymapStack as ck, createLongPressStore as cl, createMenuHost as cm, createToolOptionValues as cn, createTraceRing as co, defineTool as cp, deleteScratch as cq, describeKeymap as cr, describeRule as cs, dlog as ct, dwarn as cu, evaluate as cv, evaluateActive as cw, evaluateEnabled as cx, findConflicts as cy, getScratch as cz, type ActionsRegistry as d, keymapShortcuts as d0, keysHeld as d1, liveScope as d2, makeToolOffhandAction as d3, matchActionItem as d4, matchBest as d5, matchSorted as d6, matchSortedWithBarred as d7, matchesKeyBinding as d8, mergeContributions as d9, scratchKey as dA, setScratch as dB, startCarry as dC, steppedOption as dD, taggedActionItems as dE, targetConsultsAffordance as dF, toolIdsOf as dG, toolOptionParams as dH, toolOptionSteps as dI, toolSet as dJ, toolSetHas as dK, tuneAction as dL, validateActionDefaultBinding as dM, validateActionId as dN, withoutTools as dO, mergeMenuDefs as da, modeShortcutSpec as db, modeShortcuts as dc, modifierStateOf as dd, multiTouchGestureId as de, offhandKeyFor as df, openPointerSession as dg, overlaysOf as dh, overriddenBindings as di, pendingKindOf as dj, pickMenu as dk, pointerGestureId as dl, readActionSettings as dm, reportRouteConflicts as dn, resolveBindingOverrides as dp, resolveDeviceProfile as dq, resolveMenu as dr, resolveParams as ds, resolveSchemaMenu as dt, returnToModeDefault as du, roleReader as dv, routesForSpec as dw, ruleCanHoldIn as dx, rulesExclusive as dy, scopeBindings as dz, ActionDisabledReason as e, type ActionVariant as f, type LastOperationStore as g, type LastOperation as h, type MenuDef as i, DepNode as j, type KernelOverlay as k, type Tool as l, ActiveToolContextProvider as m, ActiveToolContextProviderIfRoot as n, type ActiveToolContextProviderProps as o, type ActiveToolContextValue as p, useOptionalActiveToolContext as q, type Dispatcher as r, type AffordanceHit as s, type DispatchTrace as t, useActiveToolContext as u, type LongPressOptions as v, type DepSchema as w, ALWAYS as x, type ActionDispatch as y, type ActionEnabledResult as z };