@weasel-js/routing 1.9.3 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/{chunk-LOP2JTTA.js → chunk-4DR6AJMN.js} +1093 -922
- package/dist/chunk-4DR6AJMN.js.map +1 -0
- package/dist/{index-CrbDNXT_.d.ts → index-Bm2QEV09.d.ts} +576 -706
- package/dist/index.d.ts +5 -4
- package/dist/index.js +27 -74
- package/dist/index.js.map +1 -1
- package/dist/{internal-B1B6gfOS.d.ts → internal-D0rDmi7t.d.ts} +46 -78
- package/dist/internal.d.ts +5 -4
- package/dist/react.d.ts +140 -98
- package/dist/react.js +870 -896
- package/dist/react.js.map +1 -1
- package/package.json +9 -7
- package/dist/chunk-LOP2JTTA.js.map +0 -1
|
@@ -1,54 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PoseLinear, View } from '@weasel-js/geom';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
|
+
import { Emitter } from '@weasel-js/registry';
|
|
5
|
+
import { CapabilityTag, ActiveMode, ModeRegistry } from '@weasel-js/modes';
|
|
4
6
|
import { CursorSpec } from '@weasel-js/cursor';
|
|
5
|
-
import { ClaimableGesture, GestureSpec, InputEvent, TargetSpec, KeySpec, ParsedModifiers, GestureName
|
|
6
|
-
import { Op } from '@weasel-js/history';
|
|
7
|
+
import { ClaimableGesture, GestureSpec, IngestItem, InputEvent, TargetSpec, KeyHeldSpec, TargetPredicate, KeySpec, ParsedModifiers, GestureName } from '@weasel-js/gestures';
|
|
7
8
|
import '@weasel-js/select';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The primitives the routing surface is typed in.
|
|
11
12
|
*
|
|
12
|
-
* Routing
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* which is declared in `@weasel-js/history` and re-exported by
|
|
18
|
-
* `core/ops/types`. The selection click policy's own vocabulary comes from
|
|
19
|
-
* `@weasel-js/select`, where the rules that read it live.
|
|
20
|
-
*
|
|
21
|
-
* Authority for the *behavior* behind each of these stays in core: `View` is
|
|
22
|
-
* what `core/viewport` maintains, `SelectionApi` is what `useSelection`
|
|
23
|
-
* returns, `DeviceProfile` is what `resolveDeviceProfile` detects. Changing
|
|
24
|
-
* one of these shapes is a change to core's contract that happens to be
|
|
25
|
-
* spelled here.
|
|
13
|
+
* Routing has to name a viewport, a rect, a node id, and a selection, but it
|
|
14
|
+
* never constructs, projects, or renders any of them. So they are declared
|
|
15
|
+
* here as structural interfaces with no dependencies, and `@weasel-js/core`
|
|
16
|
+
* re-exports each. The behavior behind them stays in core: changing one of
|
|
17
|
+
* these shapes is a change to core's contract that happens to be spelled here.
|
|
26
18
|
*/
|
|
27
19
|
|
|
28
20
|
/** An opaque scene-node identifier. Branded so a bare string cannot stand in. */
|
|
29
21
|
type NodeId = string & {
|
|
30
22
|
readonly __brand: 'NodeId';
|
|
31
23
|
};
|
|
32
|
-
|
|
33
|
-
* Viewport state. `(view.x, view.y)` is the world point currently rendered at
|
|
34
|
-
* the canvas top-left; `view.scale.x` / `view.scale.y` is pixels per world
|
|
35
|
-
* unit on each axis.
|
|
36
|
-
*
|
|
37
|
-
* The one outright 2D dead end in the routing surface: a `View` names no
|
|
38
|
-
* orientation, so a kernel with a camera cannot express its viewport as one.
|
|
39
|
-
*/
|
|
40
|
-
interface View {
|
|
41
|
-
x: number;
|
|
42
|
-
y: number;
|
|
43
|
-
scale: {
|
|
44
|
-
x: number;
|
|
45
|
-
y: number;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
24
|
+
|
|
48
25
|
/**
|
|
49
26
|
* Axis-aligned rectangle in world space. The optional `rotation` (radians,
|
|
50
|
-
* around the AABB center)
|
|
51
|
-
*
|
|
27
|
+
* around the AABB center) and `transform` (inside it, about the same center)
|
|
28
|
+
* let chrome carry the frame a box is drawn in without a parallel type;
|
|
29
|
+
* `poseFrameOf` composes the two.
|
|
52
30
|
*/
|
|
53
31
|
interface Bounds {
|
|
54
32
|
x: number;
|
|
@@ -56,6 +34,7 @@ interface Bounds {
|
|
|
56
34
|
width: number;
|
|
57
35
|
height: number;
|
|
58
36
|
rotation?: number;
|
|
37
|
+
transform?: PoseLinear;
|
|
59
38
|
}
|
|
60
39
|
/** The selection as routing reads and writes it. */
|
|
61
40
|
interface SelectionApi {
|
|
@@ -79,6 +58,7 @@ interface SelectionApi {
|
|
|
79
58
|
* Apply a click to the selection per the configured mode/extend key.
|
|
80
59
|
* - `single`: replaces selection with `[id]`, regardless of modifiers.
|
|
81
60
|
* - `multi`: with the extend key held, toggles `id` in/out of the selection;
|
|
61
|
+
* with the range key held, selects from the last pressed id through `id`;
|
|
82
62
|
* otherwise replaces with `[id]`.
|
|
83
63
|
*/
|
|
84
64
|
applyClick(id: NodeId, modifiers: {
|
|
@@ -140,8 +120,8 @@ type HitShape = {
|
|
|
140
120
|
/** What a recorded viewport change was: a translation or a scale change. */
|
|
141
121
|
type ViewportGestureKind = 'pan' | 'zoom';
|
|
142
122
|
/**
|
|
143
|
-
* Where
|
|
144
|
-
*
|
|
123
|
+
* Where an action reports its hit math so the debug overlay can draw it. An
|
|
124
|
+
* action reaches the canvas's sink as its `debug` dep; routing never calls one.
|
|
145
125
|
*/
|
|
146
126
|
interface DebugSink {
|
|
147
127
|
recordHitbox(id: string, kind: 'body' | 'handle' | 'rotation' | 'anchor', shape: HitShape): void;
|
|
@@ -196,6 +176,106 @@ type ResizeAnchor = {
|
|
|
196
176
|
y: 'min' | 'max' | 'free';
|
|
197
177
|
};
|
|
198
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The tree both input registries hang on. A provider is a root; every canvas
|
|
181
|
+
* mounts a scope under the registry in scope; a yoke sits between the two. A
|
|
182
|
+
* registration lives in the node it was made in, and a lookup starts at a leaf
|
|
183
|
+
* and walks up, so one canvas never answers for another.
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
/** What every node of one tree shares: a version and the listeners to it. */
|
|
187
|
+
interface ScopeTree {
|
|
188
|
+
version: number;
|
|
189
|
+
readonly listeners: Emitter;
|
|
190
|
+
}
|
|
191
|
+
declare class ScopeNode<N extends ScopeNode<N>> {
|
|
192
|
+
readonly parent: N | null;
|
|
193
|
+
readonly descends: boolean;
|
|
194
|
+
readonly tree: ScopeTree;
|
|
195
|
+
private readonly children;
|
|
196
|
+
private activeChild;
|
|
197
|
+
/** `descends`: whether reads made here answer from the active child down —
|
|
198
|
+
* true for a root or a yoke, which chrome reads through; false for a scope,
|
|
199
|
+
* which answers for itself even with a scope nested inside it. */
|
|
200
|
+
constructor(parent: N | null, descends: boolean);
|
|
201
|
+
/** Bump the tree's version and tell every listener in it. */
|
|
202
|
+
changed(): void;
|
|
203
|
+
/** Join the parent's children, newest last; returns the leave. */
|
|
204
|
+
mount(): () => void;
|
|
205
|
+
/** Make this node the one its ancestors resolve through. */
|
|
206
|
+
activate(): void;
|
|
207
|
+
/** The node a lookup made here starts from: down the active children, or the
|
|
208
|
+
* newest where nothing has been activated yet. A yoke with no members yet
|
|
209
|
+
* is passed over — there is no canvas behind it to answer. */
|
|
210
|
+
leaf(): N;
|
|
211
|
+
private newestMember;
|
|
212
|
+
/** Where a read made through this node starts. */
|
|
213
|
+
start(): N;
|
|
214
|
+
/** Whether this node is on the path a read from the root takes. */
|
|
215
|
+
isActive(): boolean;
|
|
216
|
+
/** This node, then each ancestor up to the root. */
|
|
217
|
+
chain(): Generator<N>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Which tool is active, plus the stack of tools temporarily held active by a
|
|
221
|
+
* hotkey (space-for-hand and the like). The dispatcher reads this to decide
|
|
222
|
+
* whose bindings are in scope. */
|
|
223
|
+
interface ActiveToolContextValue {
|
|
224
|
+
/** The focused tool's id, or `null` when no tool holds the active slot —
|
|
225
|
+
* a canvas can run on ambient bindings alone. */
|
|
226
|
+
active: string | null;
|
|
227
|
+
hotkeyStack: string[];
|
|
228
|
+
setActive(id: string | null): void;
|
|
229
|
+
pushHotkey(id: string): void;
|
|
230
|
+
/**
|
|
231
|
+
* Disengage a hotkey-held tool. With `id`, removes that tool's own entry
|
|
232
|
+
* wherever it now sits — holds do not have to come up in the order they went
|
|
233
|
+
* down, and popping the top would disengage a tool whose key is still held.
|
|
234
|
+
* With no id, pops the top.
|
|
235
|
+
*/
|
|
236
|
+
popHotkey(id?: string): void;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* One node of the tool tree. A node that `owns` a tool holds its state; one
|
|
240
|
+
* that does not — a scope inside a yoke — reads and writes its yoke's.
|
|
241
|
+
*/
|
|
242
|
+
declare class ToolNode extends ScopeNode<ToolNode> {
|
|
243
|
+
private state;
|
|
244
|
+
private value;
|
|
245
|
+
constructor(parent: ToolNode | null, descends: boolean, owns: boolean, initialActive: string | null);
|
|
246
|
+
/** The node whose tool a read here resolves to. */
|
|
247
|
+
owner(): ToolNode;
|
|
248
|
+
private update;
|
|
249
|
+
/** A stable value per owner and state, for `useSyncExternalStore`. */
|
|
250
|
+
snapshot(): ActiveToolContextValue;
|
|
251
|
+
subscribe: (listener: () => void) => (() => void);
|
|
252
|
+
}
|
|
253
|
+
/** Props for `<ActiveToolContextProvider>`. */
|
|
254
|
+
interface ActiveToolContextProviderProps {
|
|
255
|
+
children: ReactNode;
|
|
256
|
+
/** Seeds the active slot, and wins over the first `useTools` call's
|
|
257
|
+
* `active`. Omitted, the slot starts empty and that call seeds it. */
|
|
258
|
+
initialActive?: string | null;
|
|
259
|
+
}
|
|
260
|
+
/** Provides root active-tool state. Every canvas below has a tool of its own;
|
|
261
|
+
* reads and writes here reach the one last used. */
|
|
262
|
+
declare function ActiveToolContextProvider({ children, initialActive, }: ActiveToolContextProviderProps): react.JSX.Element;
|
|
263
|
+
/** The active-tool state in scope. Throws outside a provider; use
|
|
264
|
+
* `useOptionalActiveToolContext` where one is not guaranteed. */
|
|
265
|
+
declare function useActiveToolContext(): ActiveToolContextValue;
|
|
266
|
+
/**
|
|
267
|
+
* Like `useActiveToolContext`, but returns `null` when no
|
|
268
|
+
* `<ActiveToolContextProvider>` is in scope instead of throwing.
|
|
269
|
+
*/
|
|
270
|
+
declare function useOptionalActiveToolContext(): ActiveToolContextValue | null;
|
|
271
|
+
/**
|
|
272
|
+
* Mounts an `<ActiveToolContextProvider>` only when no parent provider is in
|
|
273
|
+
* scope; otherwise renders children unwrapped.
|
|
274
|
+
*/
|
|
275
|
+
declare function ActiveToolContextProviderIfRoot({ children, }: {
|
|
276
|
+
children: ReactNode;
|
|
277
|
+
}): react.JSX.Element;
|
|
278
|
+
|
|
199
279
|
/**
|
|
200
280
|
* Live state read by rule evaluation. Built once per frame on the consuming
|
|
201
281
|
* surface — chrome-caps, the affordance pipeline, the dispatcher's
|
|
@@ -368,25 +448,20 @@ declare const NEVER: Rule;
|
|
|
368
448
|
* { when: function hasPath() { … } } → when(hasPath)
|
|
369
449
|
* ```
|
|
370
450
|
*
|
|
371
|
-
* Never throws and always terminates
|
|
372
|
-
* on a cyclic value and renders the `when` arm as a useless `{}`.
|
|
451
|
+
* Never throws and always terminates.
|
|
373
452
|
*/
|
|
374
453
|
declare function describeRule(rule: Rule): string;
|
|
375
454
|
/** Evaluate a rule against a context. Pure, and cheap enough to run per
|
|
376
455
|
* frame for every piece of chrome. */
|
|
377
456
|
declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
|
|
378
457
|
/**
|
|
379
|
-
* Composable eligibility predicate with a fluent surface.
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
* Callable form `cond(ctx)` evaluates the tree against ctx. The fluent methods
|
|
384
|
-
* return new Conditions wrapping new trees.
|
|
458
|
+
* Composable eligibility predicate with a fluent surface. `cond(ctx)`
|
|
459
|
+
* evaluates its `Rule` tree, carried at `.rule`; the fluent methods return new
|
|
460
|
+
* Conditions wrapping new trees.
|
|
385
461
|
*
|
|
386
462
|
* **Chain semantics: strict left-to-right, no precedence.** `a.and(b).or(c)` is
|
|
387
|
-
* `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`.
|
|
388
|
-
*
|
|
389
|
-
* the subexpression or use the top-level `or(...)`.
|
|
463
|
+
* `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`. For grouped
|
|
464
|
+
* disjunction, name the subexpression or use the top-level `or(...)`.
|
|
390
465
|
*/
|
|
391
466
|
interface Condition {
|
|
392
467
|
(ctx: RuleCtx): boolean;
|
|
@@ -454,9 +529,6 @@ declare const DEFAULT_DEVICE_PROFILE: DeviceProfile;
|
|
|
454
529
|
* An Action's `invoker` field describes how it's run. `immediate` invokers
|
|
455
530
|
* fire once (delete, align, undo). `ongoing` invokers open a phase machine
|
|
456
531
|
* driven by the dispatcher (move, resize, pinch-zoom).
|
|
457
|
-
*
|
|
458
|
-
* See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`
|
|
459
|
-
* § "Types" for the full design.
|
|
460
532
|
*/
|
|
461
533
|
|
|
462
534
|
/** A 2D point in either world or screen coordinates. */
|
|
@@ -465,31 +537,20 @@ interface Point2 {
|
|
|
465
537
|
y: number;
|
|
466
538
|
}
|
|
467
539
|
/**
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
* Populated by the dispatcher when the `affordanceAt` thunk is provided to
|
|
471
|
-
* `useGestureDispatcher`. Tools / action invokers that only fire on a specific
|
|
472
|
-
* affordance (e.g. a resize handle) use this field as a guard — if the
|
|
473
|
-
* affordance is absent or is the wrong kind, they return `{}` and let other
|
|
474
|
-
* bindings handle the drag.
|
|
540
|
+
* Which UI affordance was hit at pointerdown. Populated by the dispatcher when
|
|
541
|
+
* the `affordanceAt` thunk is provided to `useGestureDispatcher`.
|
|
475
542
|
*
|
|
476
543
|
* `kind` is a discriminator string:
|
|
477
544
|
* - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
|
|
478
545
|
* `'handle:bottom-right'` — corner resize handles.
|
|
479
546
|
* - `'rotate-handle'` — the rotation affordance.
|
|
480
547
|
* - `'anchor:N'` — a path anchor at index N.
|
|
481
|
-
*
|
|
482
|
-
* `fixedPoint` is the world-space point that should remain stationary during
|
|
483
|
-
* the gesture. For resize handles this is the opposite (diagonally fixed)
|
|
484
|
-
* corner; for rotate it is the pivot.
|
|
485
|
-
*
|
|
486
|
-
* `targetIds` are the node ids this affordance belongs to.
|
|
487
548
|
*/
|
|
488
549
|
interface AffordanceHit {
|
|
489
550
|
/** Discriminator string, e.g. `'handle:bottom-right'`. */
|
|
490
551
|
kind: string;
|
|
491
552
|
/** Id of whatever produced this hit — a kit affordance's `id`, or the
|
|
492
|
-
* registered layer's id.
|
|
553
|
+
* registered layer's id. */
|
|
493
554
|
owner?: string;
|
|
494
555
|
/** `'exclusive'` means no binding may act on this point unless its target
|
|
495
556
|
* consults the affordance. `'shared'` (the default) competes on scope and
|
|
@@ -504,25 +565,18 @@ interface AffordanceHit {
|
|
|
504
565
|
};
|
|
505
566
|
/** Which nodes this affordance belongs to. */
|
|
506
567
|
targetIds?: string[];
|
|
507
|
-
/** Set when `kind` matches `'handle:*'
|
|
508
|
-
*
|
|
509
|
-
* Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
|
|
510
|
-
* leave this undefined. */
|
|
568
|
+
/** Set only when `kind` matches `'handle:*'`: which corner stays fixed
|
|
569
|
+
* during a resize. */
|
|
511
570
|
anchor?: ResizeAnchor;
|
|
512
|
-
/** Cursor to show while the pointer hovers this affordance (no
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
* action-cursor prediction, then to the active tool's cursor. */
|
|
571
|
+
/** Cursor to show while the pointer hovers this affordance (no gesture in
|
|
572
|
+
* flight). Unset falls through to action-cursor prediction, then to the
|
|
573
|
+
* active tool's cursor. */
|
|
516
574
|
cursor?: CursorSpec;
|
|
517
575
|
/**
|
|
518
576
|
* Free-form payload from whatever produced the hit, carried through to the
|
|
519
|
-
* matching action untouched.
|
|
520
|
-
*
|
|
521
|
-
* Kit affordances describe themselves fully in the fields above and leave
|
|
522
|
-
* this unset. It exists for affordances the kit doesn't know the shape of —
|
|
577
|
+
* matching action untouched. Kit affordances leave it unset; it exists for
|
|
523
578
|
* a registered layer's own chrome, where the hit-test already resolved
|
|
524
|
-
* *which* of its pieces was hit
|
|
525
|
-
* redo that work. `@weasel-js/hud` passes the hit widget here.
|
|
579
|
+
* *which* of its pieces was hit.
|
|
526
580
|
*/
|
|
527
581
|
payload?: unknown;
|
|
528
582
|
}
|
|
@@ -531,9 +585,7 @@ interface AffordanceHit {
|
|
|
531
585
|
* stylus state the originating `PointerEvent` carried.
|
|
532
586
|
*
|
|
533
587
|
* The stylus fields are absent for mouse/touch on browsers that don't report
|
|
534
|
-
* them, and for synthetic events.
|
|
535
|
-
* (e.g. `Stroke.vertexWidths` from a pencil stroke) read them off the samples
|
|
536
|
-
* their `insert` dep receives — see `apps/site/demos/StrokeAndFillDemo.tsx`.
|
|
588
|
+
* them, and for synthetic events.
|
|
537
589
|
*/
|
|
538
590
|
interface DragSample extends Point2 {
|
|
539
591
|
/** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
|
|
@@ -571,23 +623,19 @@ interface InvocationCtx {
|
|
|
571
623
|
* mutates the viewport itself (pan, view-zoom), because world-space
|
|
572
624
|
* deltas become self-referential as the view shifts mid-drag.
|
|
573
625
|
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
* provide them.
|
|
626
|
+
* Absent when the underlying pointer events carried no
|
|
627
|
+
* `clientX`/`clientY`.
|
|
577
628
|
*/
|
|
578
629
|
screenDelta?: Point2;
|
|
579
630
|
affordance?: AffordanceHit;
|
|
580
631
|
/**
|
|
581
632
|
* Full pointermove history for the current drag, in world space, with
|
|
582
633
|
* per-sample stylus state when the browser reported it.
|
|
583
|
-
* Accumulated by the dispatcher on every `pointermove` pump event.
|
|
584
634
|
* Available only during `onMove` and `onEnd` calls (not on `start`).
|
|
585
635
|
*
|
|
586
636
|
* During `onMove` this is the dispatcher's live accumulator, and it grows
|
|
587
637
|
* under you — read it, don't retain it. On `onEnd` it is a snapshot, so
|
|
588
638
|
* that is where an action that keeps the trail should take it.
|
|
589
|
-
* Used by `lassoSelectAction` to build its polygon vertex list and by
|
|
590
|
-
* `insertAction`'s pencil kind to carry the freehand stroke.
|
|
591
639
|
*/
|
|
592
640
|
points?: DragSample[];
|
|
593
641
|
};
|
|
@@ -609,12 +657,13 @@ interface InvocationCtx {
|
|
|
609
657
|
centroid: Point2;
|
|
610
658
|
/** Distance between the two primary pointers, in CSS pixels. */
|
|
611
659
|
spread: number;
|
|
660
|
+
/** Degrees the line between the two primary pointers has turned since
|
|
661
|
+
* the gesture began, clockwise positive. It keeps counting past a half
|
|
662
|
+
* turn. Zero where the events carry no `MultiTouchEvent.angle`. */
|
|
612
663
|
rotation: number;
|
|
613
664
|
/**
|
|
614
|
-
* Pinch-zoom geometry
|
|
615
|
-
* handle is in flight and a pointermove-pump fires.
|
|
665
|
+
* Pinch-zoom geometry, populated while a multitouch handle is in flight.
|
|
616
666
|
* `startSpread` is the spread at the moment the gesture began.
|
|
617
|
-
* `currentSpread` is the spread at the current frame.
|
|
618
667
|
*/
|
|
619
668
|
pinch?: {
|
|
620
669
|
startSpread: number;
|
|
@@ -635,33 +684,27 @@ interface InvocationCtx {
|
|
|
635
684
|
viewId?: string | null;
|
|
636
685
|
/**
|
|
637
686
|
* Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
|
|
638
|
-
* UI-driven ongoing actions (color picker, opacity slider)
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
* receive params via `BindingOpts.params` on `start` (the `opts` arg).
|
|
687
|
+
* UI-driven ongoing actions (color picker, opacity slider). The gesture
|
|
688
|
+
* dispatcher does not populate this field; gesture-driven actions receive
|
|
689
|
+
* params via `BindingOpts.params` on `start` (the `opts` arg).
|
|
642
690
|
*/
|
|
643
691
|
params?: Record<string, unknown>;
|
|
644
692
|
}
|
|
645
693
|
/** Per-invocation options the dispatcher reads from a `GestureBinding`'s
|
|
646
|
-
* `opts` field and passes to `OngoingInvoker.start`.
|
|
647
|
-
* behaviors; extensible. */
|
|
694
|
+
* `opts` field and passes to `OngoingInvoker.start`. */
|
|
648
695
|
interface BindingOpts {
|
|
649
696
|
/** Gesture behaviors the action's own invoker runs. Routing carries the
|
|
650
697
|
* array to the action and never reads an element, so the element type is
|
|
651
|
-
* open here; `@weasel-js/core` narrows it
|
|
652
|
-
* unknown, unknown>[]`, which is what a behavior is authored against. */
|
|
698
|
+
* open here; `@weasel-js/core` narrows it. */
|
|
653
699
|
behaviors?: unknown[];
|
|
654
|
-
/** Per-binding action parameters
|
|
655
|
-
*
|
|
656
|
-
* invokers, when needed). Loose typing (Record<string, unknown>) for
|
|
657
|
-
* now; consider per-action typing later via BindingOpts<A>.
|
|
700
|
+
/** Per-binding action parameters, which an immediate invoker reads via the
|
|
701
|
+
* second arg to `run`.
|
|
658
702
|
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
* via `resolveParams(opts?.params)`. */
|
|
703
|
+
* May also be a thunk evaluated each time the value is needed, so a tool
|
|
704
|
+
* can close over refs that mutate during a gesture (e.g. polygon `sides`
|
|
705
|
+
* adjusted mid-drag via ArrowUp). An ongoing invoker that wants the latest
|
|
706
|
+
* values at commit re-calls it inside `onEnd` via
|
|
707
|
+
* `resolveParams(opts?.params)`. */
|
|
665
708
|
params?: Record<string, unknown> | (() => Record<string, unknown>);
|
|
666
709
|
/**
|
|
667
710
|
* The views this binding is live in, by id, where `null` is the surface's
|
|
@@ -673,6 +716,13 @@ interface BindingOpts {
|
|
|
673
716
|
* drag bound inside a minimap view loses to the active tool's marquee.
|
|
674
717
|
*/
|
|
675
718
|
views?: readonly (string | null)[];
|
|
719
|
+
/**
|
|
720
|
+
* Says this binding is declared ahead of another on the same input on
|
|
721
|
+
* purpose: its action declines through `enabled()` when the input is not its
|
|
722
|
+
* to take, and the next binding fires. The dispatcher does that for any
|
|
723
|
+
* binding; this tells the route-conflict check the order is the design.
|
|
724
|
+
*/
|
|
725
|
+
yields?: boolean;
|
|
676
726
|
}
|
|
677
727
|
/** Resolve `BindingOpts.params` to a concrete record (calling the thunk if
|
|
678
728
|
* needed). Returns `undefined` for absent params. */
|
|
@@ -704,12 +754,10 @@ type OverlayRole =
|
|
|
704
754
|
* `OngoingHandle.overlay()` — a marquee rect, a lasso trail, the outline of a
|
|
705
755
|
* shape being dragged out, the line a slice would cut along.
|
|
706
756
|
*
|
|
707
|
-
* Every variant is world geometry and nothing else
|
|
708
|
-
* color, no coordinate space to choose
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
* normalizes this into `ResolvedOverlay`, which is what a painter consumes —
|
|
712
|
-
* `useDispatcherOverlayLayer` is core's, and it owns every style decision.
|
|
757
|
+
* Every variant is world geometry and nothing else — no `DrawCommand`, no
|
|
758
|
+
* color, no coordinate space to choose — so a surface rendering the same
|
|
759
|
+
* gesture some other way can still read it. The painter owns every style
|
|
760
|
+
* decision.
|
|
713
761
|
*/
|
|
714
762
|
type OngoingOverlay = {
|
|
715
763
|
kind: 'marquee';
|
|
@@ -735,13 +783,9 @@ type OngoingOverlay = {
|
|
|
735
783
|
shiftHeld: boolean;
|
|
736
784
|
} | {
|
|
737
785
|
/**
|
|
738
|
-
* An open run of world points the gesture is proposing —
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
* Geometry only, like every other variant: `role` is the whole of what
|
|
742
|
-
* an action says about appearance, and the painting surface is what
|
|
743
|
-
* turns it into a stroke. An action that picked its own color would be
|
|
744
|
-
* unpaintable by any renderer but the one it had in mind.
|
|
786
|
+
* An open run of world points the gesture is proposing — a cut line, a
|
|
787
|
+
* routed connector. `role` is the whole of what an action says about
|
|
788
|
+
* appearance.
|
|
745
789
|
*/
|
|
746
790
|
kind: 'polyline';
|
|
747
791
|
/** At least two points. Anything shorter is dropped in resolution. */
|
|
@@ -753,19 +797,14 @@ type OngoingOverlay = {
|
|
|
753
797
|
role?: OverlayRole;
|
|
754
798
|
} | {
|
|
755
799
|
/**
|
|
756
|
-
* Live insert-drag preview
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
* preview through the dispatcher overlay layer instead.
|
|
800
|
+
* Live insert-drag preview. Pre-commit there is no scene node to ghost
|
|
801
|
+
* via `previewIds()`/`previewPose()`, so insert paints its preview
|
|
802
|
+
* through the overlay instead.
|
|
760
803
|
*
|
|
761
|
-
* `
|
|
762
|
-
* the
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
* commit factory uses, so the preview matches the eventual node.
|
|
766
|
-
*
|
|
767
|
-
* `extras` is opaque (`unknown`) at the union level; the overlay
|
|
768
|
-
* renderer narrows on `shape` and casts the field shape it expects.
|
|
804
|
+
* `bounds` is the AABB of the current drag (start/current normalized).
|
|
805
|
+
* `extras` is the per-kind extras the action already collected; the
|
|
806
|
+
* overlay renderer narrows on `shape` and casts the field shape it
|
|
807
|
+
* expects.
|
|
769
808
|
*/
|
|
770
809
|
kind: 'insertPreview';
|
|
771
810
|
/** Which preview the dispatcher's overlay draws. The kit's own shape
|
|
@@ -779,11 +818,9 @@ type OngoingOverlay = {
|
|
|
779
818
|
height: number;
|
|
780
819
|
};
|
|
781
820
|
extras: unknown;
|
|
782
|
-
/** World-space point to paint a small "anchor" dot at
|
|
783
|
-
* click point
|
|
784
|
-
*
|
|
785
|
-
* click point, and for any shape in center mode where the dot
|
|
786
|
-
* marks the center the shape grows around. */
|
|
821
|
+
/** World-space point to paint a small "anchor" dot at, marking the
|
|
822
|
+
* click point — for radial shapes (polygon/star), where no vertex
|
|
823
|
+
* sits on it, and for any shape in center mode. */
|
|
787
824
|
anchorPoint?: {
|
|
788
825
|
x: number;
|
|
789
826
|
y: number;
|
|
@@ -796,42 +833,23 @@ type OngoingOverlay = {
|
|
|
796
833
|
interface OngoingHandle {
|
|
797
834
|
/**
|
|
798
835
|
* Optional logical action kind — a stable, human-readable tag the
|
|
799
|
-
* dispatcher exposes via `getActiveAction()`
|
|
800
|
-
*
|
|
801
|
-
* is currently in flight" without inspecting handles directly.
|
|
802
|
-
*
|
|
803
|
-
* Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
|
|
804
|
-
* `'pan'`, `'pinch'`.
|
|
836
|
+
* dispatcher exposes via `getActiveAction()`, e.g. `'marquee'`, `'move'`,
|
|
837
|
+
* `'resize'`, `'pan'`.
|
|
805
838
|
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
* meaningful to consumers.
|
|
809
|
-
*
|
|
810
|
-
* When omitted, the action is "anonymous" — `getActiveAction().kind`
|
|
811
|
-
* reports `null` even though a handle is in flight. This is fine for
|
|
812
|
-
* actions that don't have visible chrome of their own.
|
|
839
|
+
* When omitted, `getActiveAction().kind` reports `null` even though a
|
|
840
|
+
* handle is in flight.
|
|
813
841
|
*/
|
|
814
842
|
kind?: string;
|
|
815
843
|
onMove?(ctx: InvocationCtx): void;
|
|
816
844
|
onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
|
|
817
845
|
/**
|
|
818
|
-
* Optional preview surface
|
|
819
|
-
*
|
|
820
|
-
* An ongoing-action implementation may populate `previewIds()` +
|
|
821
|
-
* `previewPose(id)` to expose its in-flight preview state for the
|
|
822
|
-
* canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
|
|
823
|
-
* top of the committed scene during the gesture.
|
|
846
|
+
* Optional preview surface: the in-flight state the canvas's preview-ghost
|
|
847
|
+
* layer renders on top of the committed scene during the gesture.
|
|
824
848
|
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
* pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
|
|
830
|
-
* returns the interim pose for one of those ids (shape opaque — the
|
|
831
|
-
* canvas casts to its `TPose` parameter). The preview-ghost layer
|
|
832
|
-
* merges all sources via first-non-null semantics, with tool-side
|
|
833
|
-
* previews taking precedence over dispatcher-side (preserves
|
|
834
|
-
* backwards-compat during the registry-unification migration).
|
|
849
|
+
* `previewIds()` enumerates the displaced node ids; `previewPose(id)`
|
|
850
|
+
* returns the interim pose for one of them (shape opaque — the canvas
|
|
851
|
+
* casts to its `TPose` parameter). Returning `null`, or omitting the
|
|
852
|
+
* method, means "no preview this gesture".
|
|
835
853
|
*/
|
|
836
854
|
previewIds?(): Iterable<string> | null;
|
|
837
855
|
previewPose?(id: string): unknown | null;
|
|
@@ -844,40 +862,22 @@ interface OngoingHandle {
|
|
|
844
862
|
*/
|
|
845
863
|
previewOpaqueIds?(): Iterable<string> | null;
|
|
846
864
|
/**
|
|
847
|
-
* When `false`, the preview-ghost layer paints the ghost AND the
|
|
848
|
-
*
|
|
849
|
-
* `true
|
|
850
|
-
* during the gesture). Clone overrides to `false` so the original
|
|
851
|
-
* stays put and the ghost appears at the drag target.
|
|
865
|
+
* When `false`, the preview-ghost layer paints the ghost AND the source
|
|
866
|
+
* node stays visible at its committed pose, as a clone wants. Defaults to
|
|
867
|
+
* `true`: the ghost replaces the source during the gesture.
|
|
852
868
|
*/
|
|
853
869
|
previewHidesSource?: boolean;
|
|
854
870
|
/**
|
|
855
|
-
* Optional per-id preview *data
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
* `data
|
|
859
|
-
* ghost layer assembles a synthetic node from `{ ...node, pose:
|
|
860
|
-
* previewPose ?? node.pose, data: previewData ?? node.data }` before
|
|
861
|
-
* calling the scene slot's `drawOne`.
|
|
862
|
-
*
|
|
863
|
-
* Sources compose first-non-null per axis: an action can emit only
|
|
864
|
-
* `previewPose` (translation), only `previewData` (data-only edit),
|
|
865
|
-
* or both (pose + data both change, e.g. anchor drag on a data.path
|
|
866
|
-
* node where the bounds shift).
|
|
871
|
+
* Optional per-id preview *data*, for a gesture that mutates `node.data`
|
|
872
|
+
* (e.g. anchor-edit on nodes that store the polygon on `data.path`) rather
|
|
873
|
+
* than, or in addition to, the pose. Falls back to the committed
|
|
874
|
+
* `node.data` when null/absent.
|
|
867
875
|
*/
|
|
868
876
|
previewData?(id: string): unknown | null;
|
|
869
877
|
/**
|
|
870
|
-
* Optional chrome surface
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
* non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
|
|
874
|
-
* `useDispatcherOverlayLayer` to paint while the gesture is in flight.
|
|
875
|
-
* Returning `null` (or omitting the method) means "no overlay this
|
|
876
|
-
* gesture" — the canvas will skip this handle as a chrome source.
|
|
877
|
-
*
|
|
878
|
-
* Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
|
|
879
|
-
* which paints displaced scene-node silhouettes. Marquee and lasso
|
|
880
|
-
* gestures don't displace any node, but still need on-screen feedback.
|
|
878
|
+
* Optional chrome surface: a non-ghost visual (marquee rectangle, lasso
|
|
879
|
+
* polyline) painted while the gesture is in flight. Returning `null`, or
|
|
880
|
+
* omitting the method, means "no overlay this gesture".
|
|
881
881
|
*/
|
|
882
882
|
overlay?(): OngoingOverlay | null;
|
|
883
883
|
}
|
|
@@ -896,9 +896,7 @@ interface OngoingInvoker {
|
|
|
896
896
|
timing: 'ongoing';
|
|
897
897
|
start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
|
|
898
898
|
}
|
|
899
|
-
/** Pluggable invocation strategy for an Action.
|
|
900
|
-
* (`longPress`, `twoStage`, `modal`) extend this union without touching
|
|
901
|
-
* the `Action` type. */
|
|
899
|
+
/** Pluggable invocation strategy for an Action. */
|
|
902
900
|
type Invoker = ImmediateInvoker | OngoingInvoker;
|
|
903
901
|
|
|
904
902
|
/**
|
|
@@ -976,11 +974,9 @@ type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDis
|
|
|
976
974
|
*/
|
|
977
975
|
interface ActionPresentation {
|
|
978
976
|
label: string;
|
|
979
|
-
/** Inline-SVG icon for palette / toolbar surfaces.
|
|
980
|
-
* `
|
|
981
|
-
*
|
|
982
|
-
* metadata. May be a static `ReactNode` or a function (rare; useful
|
|
983
|
-
* for state-aware icons like a "lock" toggle). */
|
|
977
|
+
/** Inline-SVG icon for palette / toolbar surfaces. May be a static
|
|
978
|
+
* `ReactNode` or a function (rare; useful for state-aware icons like a
|
|
979
|
+
* "lock" toggle). */
|
|
984
980
|
icon?: ReactNode | (() => ReactNode);
|
|
985
981
|
/** Grouping key for palette/menu surfaces. Free-form string; the kit
|
|
986
982
|
* ships defaults for `'align'` (six edges/centers), `'distribute'`
|
|
@@ -1035,30 +1031,22 @@ interface ActionDispatch extends BindingSource {
|
|
|
1035
1031
|
scope?: 'hotkey';
|
|
1036
1032
|
/**
|
|
1037
1033
|
* @experimental
|
|
1038
|
-
* Optional predicate the
|
|
1039
|
-
* `true` when
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1034
|
+
* Optional predicate saying whether the action can run right now. Return
|
|
1035
|
+
* `true` when it can, or an `ActionDisabledReason` when it cannot. The
|
|
1036
|
+
* command palette greys out a disabled row, skips it in keyboard nav,
|
|
1037
|
+
* ignores clicks, and shows the reason next to the label. The dispatcher
|
|
1038
|
+
* skips a disabled action too, on a key as on a pointer, and falls through
|
|
1039
|
+
* to the next binding that matches. `ActionsRegistry.trigger` does not ask,
|
|
1040
|
+
* so the action's own `run` should still self-guard.
|
|
1044
1041
|
*
|
|
1045
1042
|
* **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
|
|
1046
|
-
* must not throw.
|
|
1047
|
-
* `
|
|
1048
|
-
* caught and treated as disabled with reason `'(predicate threw)'`.
|
|
1049
|
-
*
|
|
1050
|
-
* Snapshot-on-open semantics: the palette evaluates `enabled` once when
|
|
1051
|
-
* opened and does NOT re-evaluate on selection changes while open. Live
|
|
1052
|
-
* reactive updates are deferred — palette is short-lived.
|
|
1043
|
+
* must not throw. A throw is caught and treated as disabled with reason
|
|
1044
|
+
* `ActionDisabledReason.PredicateThrew`.
|
|
1053
1045
|
*
|
|
1054
|
-
* The
|
|
1055
|
-
*
|
|
1046
|
+
* The palette evaluates `enabled` once when opened and does NOT re-evaluate
|
|
1047
|
+
* on selection changes while open.
|
|
1056
1048
|
*
|
|
1057
|
-
*
|
|
1058
|
-
* `ImmediateInvoker.run`; callers (`evaluateEnabled` / the ActionBar) may
|
|
1059
|
-
* synthesize it from the surrounding `DepRegistry` so predicates can
|
|
1060
|
-
* inspect selection / scene / etc. Predicates that don't need deps just
|
|
1061
|
-
* ignore the arg.
|
|
1049
|
+
* `deps` is the same bag passed to `ImmediateInvoker.run`.
|
|
1062
1050
|
*
|
|
1063
1051
|
* `at` is the world point of the event being routed, when it has one — a
|
|
1064
1052
|
* click, a press, a long-press. An action that only applies somewhere
|
|
@@ -1066,11 +1054,30 @@ interface ActionDispatch extends BindingSource {
|
|
|
1066
1054
|
* falls through to the next binding and hover prediction shows the
|
|
1067
1055
|
* action's `cursor` only where it would act. Palettes and keystrokes
|
|
1068
1056
|
* pass no point; treat absent as "anywhere".
|
|
1057
|
+
*
|
|
1058
|
+
* `carried` is what a drop or a paste being routed brings: its items. For
|
|
1059
|
+
* a drag still in flight they hold a MIME and nothing else, since a
|
|
1060
|
+
* browser withholds contents until the drop. An action that takes only
|
|
1061
|
+
* some content refuses the rest here, so a drag it would not take is not
|
|
1062
|
+
* accepted and a paste it would not take stays the page's.
|
|
1069
1063
|
*/
|
|
1070
1064
|
enabled?: (deps?: ActionDeps, at?: {
|
|
1071
1065
|
x: number;
|
|
1072
1066
|
y: number;
|
|
1067
|
+
}, carried?: {
|
|
1068
|
+
items: readonly IngestItem[];
|
|
1073
1069
|
}) => true | ActionDisabledReason;
|
|
1070
|
+
/**
|
|
1071
|
+
* @experimental
|
|
1072
|
+
* Whether the action reads as held down right now: bold on the selected
|
|
1073
|
+
* text, snapping switched on. A toolbar button draws it as pressed.
|
|
1074
|
+
* Omitted = the action has no pressed state, which is not the same as
|
|
1075
|
+
* one that is off. Never read during dispatch.
|
|
1076
|
+
*
|
|
1077
|
+
* Under the same contract as `enabled`: pure, fast, and it must not
|
|
1078
|
+
* throw. `deps` is the bag `enabled` is given.
|
|
1079
|
+
*/
|
|
1080
|
+
active?: (deps?: ActionDeps) => boolean;
|
|
1074
1081
|
/**
|
|
1075
1082
|
* Declarative eligibility rule, evaluated against the current
|
|
1076
1083
|
* `RuleCtx` by the dispatcher before invoking `start()`. Omitted =
|
|
@@ -1087,12 +1094,11 @@ interface ActionDispatch extends BindingSource {
|
|
|
1087
1094
|
/**
|
|
1088
1095
|
* CSS cursor shown while the pointer hovers a spot where this action
|
|
1089
1096
|
* would win the drag — or, when the drag's winner declares no cursor,
|
|
1090
|
-
* the click. The hover-cursor pump
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
* pump via `AffordanceRegion.cursor` and never reach this field.
|
|
1097
|
+
* the click. The hover-cursor pump runs the same match walk a real press
|
|
1098
|
+
* takes, so the hint and the actual target stay in sync. Omitted = no
|
|
1099
|
+
* override (the active tool's `Tool.cursor` shows). Affordance hits are
|
|
1100
|
+
* resolved earlier in the pump via `AffordanceRegion.cursor` and never
|
|
1101
|
+
* reach this field.
|
|
1096
1102
|
*
|
|
1097
1103
|
* Static value only. Prediction runs `enabled()` but cannot run the
|
|
1098
1104
|
* invoker, so an action that matches yet bails at `start()` (empty
|
|
@@ -1110,19 +1116,12 @@ interface ActionDispatch extends BindingSource {
|
|
|
1110
1116
|
* ("you are panning"). An action can declare either, both, or neither;
|
|
1111
1117
|
* with only `cursor` set, the hover hint holds for the duration of the
|
|
1112
1118
|
* gesture.
|
|
1113
|
-
*
|
|
1114
|
-
* This is where mid-gesture cursors live now. They used to come from the
|
|
1115
|
-
* tool side — an `engaged` phase entry's cursor,
|
|
1116
|
-
* or a function-form `Tool.cursor` reading the gesture scratch out of the
|
|
1117
|
-
* tool-routing dispatcher. Both belonged to a pipeline whose whole job was
|
|
1118
|
-
* being taken over by bindings, and neither could describe a cursor for an
|
|
1119
|
-
* action a tool doesn't own.
|
|
1120
1119
|
*/
|
|
1121
1120
|
activeCursor?: CursorSpec;
|
|
1122
1121
|
}
|
|
1123
1122
|
/**
|
|
1124
1123
|
* @experimental
|
|
1125
|
-
* Single registered action.
|
|
1124
|
+
* Single registered action.
|
|
1126
1125
|
*/
|
|
1127
1126
|
interface Action extends ActionDispatch, ActionPresentation {
|
|
1128
1127
|
}
|
|
@@ -1182,64 +1181,17 @@ interface ActionEnabledResult {
|
|
|
1182
1181
|
* predicates should be sub-millisecond).
|
|
1183
1182
|
*/
|
|
1184
1183
|
declare function evaluateEnabled(action: Action, deps?: ActionDeps): ActionEnabledResult;
|
|
1185
|
-
|
|
1186
1184
|
/**
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
* and walks up, so one canvas never answers for another.
|
|
1191
|
-
*
|
|
1192
|
-
* Design: docs/proposals/2026-09-27-input-scopes.md.
|
|
1185
|
+
* @experimental
|
|
1186
|
+
* Safely evaluate an action's `active` predicate. `undefined` for an action
|
|
1187
|
+
* with no pressed state; a predicate that throws reads as not active.
|
|
1193
1188
|
*/
|
|
1194
|
-
|
|
1195
|
-
interface ScopeTree {
|
|
1196
|
-
version: number;
|
|
1197
|
-
readonly listeners: Set<() => void>;
|
|
1198
|
-
}
|
|
1199
|
-
declare class ScopeNode<N extends ScopeNode<N>> {
|
|
1200
|
-
readonly parent: N | null;
|
|
1201
|
-
readonly descends: boolean;
|
|
1202
|
-
readonly tree: ScopeTree;
|
|
1203
|
-
private readonly children;
|
|
1204
|
-
private activeChild;
|
|
1205
|
-
/** `descends`: whether reads made here answer from the active child down —
|
|
1206
|
-
* true for a root or a yoke, which chrome reads through; false for a scope,
|
|
1207
|
-
* which answers for itself even with a scope nested inside it. */
|
|
1208
|
-
constructor(parent: N | null, descends: boolean);
|
|
1209
|
-
/** Bump the tree's version and tell every listener in it. */
|
|
1210
|
-
changed(): void;
|
|
1211
|
-
/** Join the parent's children, newest last; returns the leave. */
|
|
1212
|
-
mount(): () => void;
|
|
1213
|
-
/** Make this node the one its ancestors resolve through. */
|
|
1214
|
-
activate(): void;
|
|
1215
|
-
/** The node a lookup made here starts from: down the active children, or the
|
|
1216
|
-
* newest where nothing has been activated yet. A yoke with no members yet
|
|
1217
|
-
* is passed over — there is no canvas behind it to answer. */
|
|
1218
|
-
leaf(): N;
|
|
1219
|
-
private newestMember;
|
|
1220
|
-
/** Where a read made through this node starts. */
|
|
1221
|
-
start(): N;
|
|
1222
|
-
/** Whether this node is on the path a read from the root takes. */
|
|
1223
|
-
isActive(): boolean;
|
|
1224
|
-
/** This node, then each ancestor up to the root. */
|
|
1225
|
-
chain(): Generator<N>;
|
|
1226
|
-
}
|
|
1189
|
+
declare function evaluateActive(action: Action, deps?: ActionDeps): boolean | undefined;
|
|
1227
1190
|
|
|
1228
1191
|
/**
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
* the dispatcher calls `registry.get('selection')` at invocation time to
|
|
1233
|
-
* build a typed Deps bag.
|
|
1234
|
-
*
|
|
1235
|
-
* `DepSchema` — the map of dep name → value type — is the single source of
|
|
1236
|
-
* truth for what `requires`/`register`/`get` accept. Its concrete fields live
|
|
1237
|
-
* in `./depSchema` (co-located with the dep value types). It is declared there
|
|
1238
|
-
* as a plain `export interface` rather than via a cross-module
|
|
1239
|
-
* `declare module './depRegistry'` augmentation: that augmentation does NOT
|
|
1240
|
-
* merge once rollup-plugin-dts flattens both files into one `.d.ts` chunk,
|
|
1241
|
-
* which silently emptied `DepSchema` for consumers. Consumers still extend it
|
|
1242
|
-
* via `declare module '@weasel-js/core'` augmentation.
|
|
1192
|
+
* The dep registry: named "live source" thunks. Actions declare
|
|
1193
|
+
* `requires: ['selection']`; the dispatcher calls `registry.get('selection')`
|
|
1194
|
+
* at invocation time to build a typed Deps bag.
|
|
1243
1195
|
*/
|
|
1244
1196
|
|
|
1245
1197
|
/** Holds the live sources an action's declared dependencies resolve to.
|
|
@@ -1248,6 +1200,15 @@ declare class ScopeNode<N extends ScopeNode<N>> {
|
|
|
1248
1200
|
interface DepRegistry {
|
|
1249
1201
|
register<K extends DepName>(name: K, source: () => DepSchema[K]): () => void;
|
|
1250
1202
|
get<K extends DepName>(name: K): DepSchema[K] | undefined;
|
|
1203
|
+
/** Hear that what the registry answers may have changed: a source was
|
|
1204
|
+
* registered or released anywhere in its tree, or an owner called
|
|
1205
|
+
* {@link DepRegistry.changed}. Returns the unsubscribe. A hand-built
|
|
1206
|
+
* registry whose answers never change leaves this and `changed` out. */
|
|
1207
|
+
subscribe?(listener: () => void): () => void;
|
|
1208
|
+
/** Say that a source's value changed. A source is a thunk read on demand,
|
|
1209
|
+
* so its owner is the only one who knows; chrome showing an action's
|
|
1210
|
+
* `enabled` or `active` state re-reads on this. */
|
|
1211
|
+
changed?(): void;
|
|
1251
1212
|
}
|
|
1252
1213
|
/** One node of the dep tree: the sources registered at it, over its parent's.
|
|
1253
1214
|
* Each name holds a stack of sources, newest live, so a displaced source is
|
|
@@ -1289,12 +1250,12 @@ type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift' | (string & {})
|
|
|
1289
1250
|
*
|
|
1290
1251
|
* Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
|
|
1291
1252
|
* is already plumbed through `<Canvas>` to `style.cursor` on the host. */
|
|
1292
|
-
interface ToolPresentation
|
|
1253
|
+
interface ToolPresentation {
|
|
1293
1254
|
/** Human-readable label, distinct from the `id`. Falls back to `id`. */
|
|
1294
1255
|
label?: string;
|
|
1295
|
-
/** Inline-SVG icon component output
|
|
1296
|
-
*
|
|
1297
|
-
icon?: react.ReactNode | ((
|
|
1256
|
+
/** Inline-SVG icon component output: a static `ReactNode`, or a function
|
|
1257
|
+
* returning one. */
|
|
1258
|
+
icon?: react.ReactNode | (() => react.ReactNode);
|
|
1298
1259
|
/** Palette grouping key. Tools sharing a group render contiguously
|
|
1299
1260
|
* with separators between groups. Free-form string; the kit
|
|
1300
1261
|
* recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
|
|
@@ -1374,9 +1335,8 @@ interface Contribution<TOverlay = KernelOverlay> extends ContributionRouting, Co
|
|
|
1374
1335
|
}
|
|
1375
1336
|
|
|
1376
1337
|
/**
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
* binding-scope / matchBest logic on top.
|
|
1338
|
+
* Re-exports the pure matcher primitives from `@weasel-js/gestures` and layers
|
|
1339
|
+
* the actions-layer binding-scope / matchBest logic on top.
|
|
1380
1340
|
*/
|
|
1381
1341
|
|
|
1382
1342
|
/** Where a binding came from, which is also its priority: a held hotkey beats
|
|
@@ -1405,26 +1365,13 @@ interface MatchResult {
|
|
|
1405
1365
|
* only the body classification. `kindOf` predicates are handed the hit;
|
|
1406
1366
|
* `affordance:<k>` matches on its `kind`. The body-class strings (`'empty'`,
|
|
1407
1367
|
* `'selected-body'`, `'unselected-body'`) and the `kind:` forms resolve from
|
|
1408
|
-
* `bodyTarget` / `bodyKind` and never see it
|
|
1409
|
-
* over empty canvas used to read as empty canvas.
|
|
1368
|
+
* `bodyTarget` / `bodyKind` and never see it.
|
|
1410
1369
|
*
|
|
1411
|
-
* A predicate
|
|
1412
|
-
*
|
|
1413
|
-
* that declare nothing.
|
|
1370
|
+
* A predicate declares which it is with `readsAffordance`; one that declares
|
|
1371
|
+
* nothing, which only untyped code can write, counts as not reading the hit.
|
|
1414
1372
|
*/
|
|
1415
1373
|
declare function targetConsultsAffordance(specTarget: TargetSpec | undefined): boolean;
|
|
1416
|
-
/**
|
|
1417
|
-
* Find the best-matching binding across all scopes.
|
|
1418
|
-
*
|
|
1419
|
-
* Precedence: hotkey > active > ambient.
|
|
1420
|
-
* Within a scope, more-specific bindings win; same-specificity bindings keep
|
|
1421
|
-
* registration order (see `matchSorted`).
|
|
1422
|
-
* Returns null when nothing matches.
|
|
1423
|
-
*
|
|
1424
|
-
* Note: this returns only the single best match. The dispatcher uses
|
|
1425
|
-
* `matchSorted` so it can fall through to lower-specificity matches when a
|
|
1426
|
-
* higher-specificity match's `enabled()` reports disabled.
|
|
1427
|
-
*/
|
|
1374
|
+
/** The first of {@link matchSorted}'s matches, or null when nothing matches. */
|
|
1428
1375
|
declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>): MatchResult | null;
|
|
1429
1376
|
/**
|
|
1430
1377
|
* Find every binding that matches the event, sorted by precedence
|
|
@@ -1433,12 +1380,7 @@ declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], is
|
|
|
1433
1380
|
* - Scopes by priority: hotkey > active > ambient.
|
|
1434
1381
|
* - Within a scope, more-specific bindings beat less-specific (see
|
|
1435
1382
|
* `specificity()`).
|
|
1436
|
-
* - Among same-specificity bindings, first-declared wins
|
|
1437
|
-
* preserves registration order — same tiebreaker as pre-specificity).
|
|
1438
|
-
*
|
|
1439
|
-
* Used by the dispatcher to implement specificity-ordered fall-through:
|
|
1440
|
-
* when the top match's `enabled()` returns a disabled reason (or the
|
|
1441
|
-
* action's `start()` returns an empty handle), the next match is tried.
|
|
1383
|
+
* - Among same-specificity bindings, first-declared wins.
|
|
1442
1384
|
*/
|
|
1443
1385
|
declare function matchSorted(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, warn?: (message: string) => void): MatchResult[];
|
|
1444
1386
|
/** {@link matchSorted}, plus what an exclusive claim on the event barred. */
|
|
@@ -1609,91 +1551,14 @@ interface RankedCandidate {
|
|
|
1609
1551
|
}
|
|
1610
1552
|
|
|
1611
1553
|
/**
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
* hotkey stack; matches input events via `matchSorted`; gates each candidate
|
|
1616
|
-
* on `enabled()`; then invokes `immediate` or `ongoing` invokers and tracks
|
|
1617
|
-
* in-flight handles.
|
|
1618
|
-
*
|
|
1619
|
-
* ## Precedence and fall-through
|
|
1620
|
-
* An exclusive affordance claim first bars every binding that does not
|
|
1621
|
-
* consult the affordance. The rest are ranked, best first, by:
|
|
1622
|
-
* 1. bindings naming the routed view ahead of every other
|
|
1623
|
-
* (`preferViewScoped`);
|
|
1624
|
-
* 2. scope tier: hotkey > active > ambient;
|
|
1625
|
-
* 3. specificity within a tier (`specificity()` in `matcher.ts`);
|
|
1626
|
-
* 4. an action gated by an `eligible` rule that holds now ahead of one with
|
|
1627
|
-
* no rule (`preferContextual`) — Escape while editing a path exits the
|
|
1628
|
-
* edit rather than resetting the tool;
|
|
1629
|
-
* 5. registration order.
|
|
1630
|
-
* Candidates whose `eligible` rule is false are dropped first. The dispatcher
|
|
1631
|
-
* walks the rest and fires the first action whose `enabled()` returns
|
|
1632
|
-
* `true`. If every candidate's `enabled()` returns a disabled reason, the
|
|
1633
|
-
* event is unhandled. This mirrors CSS-style specificity matching with a
|
|
1634
|
-
* `:not(:disabled)` filter, and lets a tool declare a high-specificity
|
|
1635
|
-
* binding (e.g. drag-on-empty → areaSelect) that gracefully falls through
|
|
1636
|
-
* to a lower-specificity ambient binding (e.g. drag → viewport.dragPan)
|
|
1637
|
-
* when its required deps aren't wired.
|
|
1638
|
-
*
|
|
1639
|
-
* ## gestureId scheme
|
|
1640
|
-
* - `key-held` ongoing actions: `key-held-<key>` (e.g. `key-held- ` for Space).
|
|
1641
|
-
* Chosen because key-held gestures are identified by the held key alone.
|
|
1642
|
-
* - `pointerdown` / drag ongoing actions: `pointer-<pointerId>`, taken from
|
|
1643
|
-
* the originating DOM `PointerEvent`. Each physical pointer — mouse, each
|
|
1644
|
-
* touch, the stylus — gets its own handle slot. Events with no
|
|
1645
|
-
* `pointerId` (synthesized probes, programmatic drags, most tests) key to
|
|
1646
|
-
* `pointer-mouse`, so a single synthetic pointer behaves as it always has.
|
|
1647
|
-
* - `multitouch` ongoing actions: `multitouch-<fingers>`.
|
|
1648
|
-
* - Fallback for any other kind that triggers an ongoing invoker: `ongoing-<kind>`.
|
|
1649
|
-
*
|
|
1650
|
-
* ## Action-lookup miss behavior
|
|
1651
|
-
* When `matchBest` resolves a binding whose `actionId` has no entry in
|
|
1652
|
-
* `ctx.actions.list()`, the dispatcher emits `console.warn` and returns
|
|
1653
|
-
* `'unhandled'`. The user's input gesture falls through as if unmatched.
|
|
1654
|
-
* This preserves input flow (nothing is swallowed silently) while flagging
|
|
1655
|
-
* the misconfiguration at dev time.
|
|
1554
|
+
* The dispatcher's contract: what a host hands it per input
|
|
1555
|
+
* (`DispatcherContext`) and what it answers with (`Dispatcher`).
|
|
1556
|
+
* `createDispatcher` in `dispatcher.ts` implements it.
|
|
1656
1557
|
*/
|
|
1657
1558
|
|
|
1658
|
-
/**
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
* Exported because the React seam (`useGestureDispatcher`) has to name the
|
|
1662
|
-
* same slot when it asks `dispatcher.inFlight()` whether a given pointer is
|
|
1663
|
-
* mid-drag. That used to be the string literal `'pointer-mouse'` on both
|
|
1664
|
-
* sides, which was correct only because `gestureIdFor` ignored the pointer id
|
|
1665
|
-
* entirely — the two lies cancelled out.
|
|
1666
|
-
*
|
|
1667
|
-
* @param pointerId - `PointerEvent.pointerId`, or `undefined` for a
|
|
1668
|
-
* synthesized event with no originating DOM pointer.
|
|
1669
|
-
*/
|
|
1670
|
-
declare function pointerGestureId(pointerId: number | undefined): string;
|
|
1671
|
-
/** One entry per "mode change" — a kit state transition that isn't a
|
|
1672
|
-
* dispatched input event but is load-bearing for understanding why an
|
|
1673
|
-
* input was (or wasn't) handled later. Pushed via `recordModeSwitch()`;
|
|
1674
|
-
* consumers include the editAnchors dep (editingId changes), and
|
|
1675
|
-
* could expand to active-tool / hotkey-stack / focus changes. */
|
|
1676
|
-
interface ModeSwitchLogEntry {
|
|
1677
|
-
kind: 'mode';
|
|
1678
|
-
ts: number;
|
|
1679
|
-
/** Logical mode name, e.g. `'editAnchors.editingId'`. */
|
|
1680
|
-
mode: string;
|
|
1681
|
-
from: string | null;
|
|
1682
|
-
to: string | null;
|
|
1683
|
-
/** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
|
|
1684
|
-
* `'selection-dropped target'`, etc. */
|
|
1685
|
-
detail?: string;
|
|
1686
|
-
}
|
|
1687
|
-
/** A trace entry: one {@link DispatchRecord} per resolved input, or a mode switch. */
|
|
1688
|
-
type TraceLogEntry = DispatchRecord | ModeSwitchLogEntry;
|
|
1689
|
-
/** Push a mode-switch record into the trace log. Safe to call from
|
|
1690
|
-
* anywhere; no-ops outside DEV. */
|
|
1691
|
-
declare function recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
|
|
1692
|
-
/** Everything the dispatcher must consult to route one input event: the
|
|
1693
|
-
* registered actions and their deps, which tool is active, which hotkeys are
|
|
1694
|
-
* held, and — optionally — the chrome state that action eligibility rules are
|
|
1695
|
-
* evaluated against. Rebuilt per event rather than held, so the dispatcher
|
|
1696
|
-
* itself stays stateless apart from in-flight gestures. */
|
|
1559
|
+
/** Everything the dispatcher must consult to route one input event. Rebuilt
|
|
1560
|
+
* per event rather than held, so the dispatcher itself stays stateless apart
|
|
1561
|
+
* from in-flight gestures. */
|
|
1697
1562
|
interface DispatcherContext {
|
|
1698
1563
|
/** All registered actions; the dispatcher walks `.defaultBinding` for ambient bindings. */
|
|
1699
1564
|
actions: ActionSource;
|
|
@@ -1732,17 +1597,19 @@ interface DispatcherContext {
|
|
|
1732
1597
|
* calls the handle's `onMove`. Safe to call many times.
|
|
1733
1598
|
* - `end(reason)` calls `onEnd(ctx, reason)` once and removes the handle
|
|
1734
1599
|
* from the in-flight map. Idempotent — further calls are no-ops.
|
|
1600
|
+
* - `active` is false once the handle has ended, whether through `end` or
|
|
1601
|
+
* by the dispatcher (`cancelAll`, or a later `beginUiOngoing` for the same
|
|
1602
|
+
* action). `update` and `end` do nothing from then on.
|
|
1735
1603
|
*/
|
|
1736
1604
|
interface UiOngoingControl {
|
|
1737
1605
|
readonly gestureId: string;
|
|
1606
|
+
readonly active: boolean;
|
|
1738
1607
|
update(params?: Record<string, unknown>): void;
|
|
1739
1608
|
end(reason: 'commit' | 'cancel'): void;
|
|
1740
1609
|
}
|
|
1741
1610
|
/**
|
|
1742
1611
|
* Successful `Dispatcher.resolveOnly` prediction: the binding + action that
|
|
1743
|
-
* would fire if `event` were dispatched for real.
|
|
1744
|
-
* descriptor so callers (the hover-cursor pump) can read metadata like
|
|
1745
|
-
* `Action.cursor` without a second registry lookup.
|
|
1612
|
+
* would fire if `event` were dispatched for real.
|
|
1746
1613
|
*/
|
|
1747
1614
|
interface ResolveOnlyResult {
|
|
1748
1615
|
actionId: string;
|
|
@@ -1795,14 +1662,12 @@ interface ResolveAllOptions {
|
|
|
1795
1662
|
* Evaluate eligibility and `enabled()` for candidates below the winner
|
|
1796
1663
|
* instead of short-circuiting them to `shadowed`.
|
|
1797
1664
|
*
|
|
1798
|
-
* Off by default
|
|
1799
|
-
*
|
|
1800
|
-
*
|
|
1665
|
+
* Off by default: the default walk's early exit is what keeps
|
|
1666
|
+
* `resolveOnly`'s `enabled()` call count identical to a real dispatch, and
|
|
1667
|
+
* `enabled()` predicates are only contractually pure — not free.
|
|
1801
1668
|
*
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
1804
|
-
* anyway". With it on, `shadowed` narrows to its precise meaning: this
|
|
1805
|
-
* candidate would have fired, but something above it did.
|
|
1669
|
+
* With it on, `shadowed` narrows to its precise meaning: this candidate
|
|
1670
|
+
* would have fired, but something above it did.
|
|
1806
1671
|
*/
|
|
1807
1672
|
evaluateShadowed?: boolean;
|
|
1808
1673
|
}
|
|
@@ -1824,12 +1689,10 @@ interface Dispatcher {
|
|
|
1824
1689
|
*/
|
|
1825
1690
|
handleInput(event: InputEvent, ctx: DispatcherContext): 'handled' | 'unhandled';
|
|
1826
1691
|
/**
|
|
1827
|
-
* Predict which action `event` would route to WITHOUT invoking it
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
* unhandled. Pure query: no invoker runs, no in-flight state changes, no
|
|
1832
|
-
* trace-log entry.
|
|
1692
|
+
* Predict which action `event` would route to WITHOUT invoking it: the
|
|
1693
|
+
* first candidate `handleInput`'s walk would fire, or `null` when the event
|
|
1694
|
+
* would go unhandled. Pure query: no invoker runs, no in-flight state
|
|
1695
|
+
* changes, no trace-log entry.
|
|
1833
1696
|
*
|
|
1834
1697
|
* Known divergence from a real dispatch: an ongoing invoker that matches
|
|
1835
1698
|
* but returns an empty handle at `start()` (runtime bail) makes the real
|
|
@@ -1840,25 +1703,15 @@ interface Dispatcher {
|
|
|
1840
1703
|
resolveOnly(event: InputEvent, ctx: DispatcherContext): ResolveOnlyResult | null;
|
|
1841
1704
|
/**
|
|
1842
1705
|
* Every binding that matches `event`, in dispatch precedence order, each
|
|
1843
|
-
* with a verdict explaining whether it would fire.
|
|
1844
|
-
* `resolveOnly`
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
1847
|
-
* `resolveOnly`'s walk would filter out are kept here and labelled
|
|
1848
|
-
* `ineligible` instead. Pure query: no invoker runs, no in-flight state
|
|
1849
|
-
* changes, no trace-log entry.
|
|
1706
|
+
* with a verdict explaining whether it would fire. Nothing is dropped:
|
|
1707
|
+
* candidates that `resolveOnly`'s walk would filter out are kept here and
|
|
1708
|
+
* labeled `ineligible` instead. Pure query, and it shares `resolveOnly`'s
|
|
1709
|
+
* known divergence from a real dispatch.
|
|
1850
1710
|
*
|
|
1851
1711
|
* `resolveOnly` is the first `would-fire` entry of this list.
|
|
1852
1712
|
*
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
1855
|
-
* real dispatch fall through, and this cannot see that.
|
|
1856
|
-
*
|
|
1857
|
-
* By default everything below the winner is `shadowed` without being asked,
|
|
1858
|
-
* which is what keeps this walk as cheap as the dispatch it replays. Pass
|
|
1859
|
-
* `{ evaluateShadowed: true }` to keep evaluating past the winner, so a
|
|
1860
|
-
* lower candidate that is ALSO ineligible or disabled says so — see
|
|
1861
|
-
* {@link ResolveAllOptions.evaluateShadowed}.
|
|
1713
|
+
* By default everything below the winner is `shadowed` without being asked
|
|
1714
|
+
* — see {@link ResolveAllOptions.evaluateShadowed}.
|
|
1862
1715
|
*/
|
|
1863
1716
|
resolveAll(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): ResolvedCandidate[];
|
|
1864
1717
|
/**
|
|
@@ -1871,7 +1724,6 @@ interface Dispatcher {
|
|
|
1871
1724
|
explain(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): DispatchRecord;
|
|
1872
1725
|
/**
|
|
1873
1726
|
* Synthesize an end-of-gesture for every in-flight ongoing handle.
|
|
1874
|
-
* Used by tool-switch cancellation (Q2 decision).
|
|
1875
1727
|
*/
|
|
1876
1728
|
cancelAll(reason: 'commit' | 'cancel'): void;
|
|
1877
1729
|
/**
|
|
@@ -1880,46 +1732,38 @@ interface Dispatcher {
|
|
|
1880
1732
|
*/
|
|
1881
1733
|
inFlight(): ReadonlyMap<string, OngoingHandle>;
|
|
1882
1734
|
/**
|
|
1883
|
-
*
|
|
1735
|
+
* Cursor for the gesture currently in flight, as declared, or `null` when nothing
|
|
1884
1736
|
* is. Reads `Action.activeCursor` (falling back to `Action.cursor`) off the
|
|
1885
1737
|
* action whose handle is open — the hover pump applies this instead of its
|
|
1886
1738
|
* prediction once a gesture starts, which is how `grab` becomes `grabbing`.
|
|
1887
1739
|
*/
|
|
1888
|
-
inFlightCursor():
|
|
1740
|
+
inFlightCursor(): CursorSpec | null;
|
|
1889
1741
|
/**
|
|
1890
|
-
* Read-only iterator over currently in-flight `OngoingHandle` instances
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
1893
|
-
* to walk each handle's `previewIds()` / `previewPose(id)` and render
|
|
1894
|
-
* dispatcher-driven gesture previews. Read-only by design:
|
|
1895
|
-
* external consumers must not mutate the in-flight map.
|
|
1742
|
+
* Read-only iterator over currently in-flight `OngoingHandle` instances,
|
|
1743
|
+
* for a preview layer to walk each handle's `previewIds()` /
|
|
1744
|
+
* `previewPose(id)`.
|
|
1896
1745
|
*/
|
|
1897
1746
|
getInFlightHandles(): Iterable<OngoingHandle>;
|
|
1898
1747
|
/**
|
|
1899
|
-
* Subscribe to in-flight state changes. The callback fires after
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
1748
|
+
* Subscribe to in-flight state changes. The callback fires after handle
|
|
1749
|
+
* start, every `onMove` pump, end, cancel, and cancel-all, and after any
|
|
1750
|
+
* other input handled while a gesture is in flight. Input that arrives
|
|
1751
|
+
* with nothing in flight and starts nothing does not fire it.
|
|
1903
1752
|
*
|
|
1904
1753
|
* Returns an unsubscribe function.
|
|
1905
1754
|
*/
|
|
1906
1755
|
subscribe(fn: () => void): () => void;
|
|
1907
1756
|
/**
|
|
1908
1757
|
* Monotonic counter bumped on exactly the events {@link subscribe} fires
|
|
1909
|
-
* on
|
|
1910
|
-
* with `subscribe` to drive a render off in-flight gesture state without
|
|
1911
|
-
* a `useReducer` force-rerender.
|
|
1758
|
+
* on: the snapshot half of the `useSyncExternalStore` contract.
|
|
1912
1759
|
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
1916
|
-
* counts — see `subscribe`).
|
|
1760
|
+
* A bump does **not** guarantee that anything observable changed (input
|
|
1761
|
+
* that matched no binding still counts while a gesture is in flight — see
|
|
1762
|
+
* `subscribe`).
|
|
1917
1763
|
*/
|
|
1918
1764
|
getVersion(): number;
|
|
1919
1765
|
/**
|
|
1920
|
-
* Snapshot of the currently active action
|
|
1921
|
-
* visibility rules, debug HUDs) that need to react to "what action
|
|
1922
|
-
* is in flight right now."
|
|
1766
|
+
* Snapshot of the currently active action.
|
|
1923
1767
|
*
|
|
1924
1768
|
* - `kind` — the `OngoingHandle.kind` reported by the in-flight
|
|
1925
1769
|
* handle (e.g. `'marquee'`, `'move'`). `null` when no action is
|
|
@@ -1930,26 +1774,15 @@ interface Dispatcher {
|
|
|
1930
1774
|
*
|
|
1931
1775
|
* When multiple handles are in flight simultaneously (e.g. a key-held
|
|
1932
1776
|
* action overlapping a pointer action), the most-recently-started
|
|
1933
|
-
* handle wins.
|
|
1934
|
-
* the one consumers care about.
|
|
1935
|
-
*
|
|
1936
|
-
* That rule used to be near-vacuous on the pointer side, because every
|
|
1937
|
-
* pointer shared one handle slot and two pointer drags could not coexist.
|
|
1938
|
-
* With per-pointer keying they can — but only via paths that bypass the
|
|
1939
|
-
* multi-pointer policy in `useGestureDispatcher` (which stops a second
|
|
1940
|
-
* finger from opening a drag while a pinch is live), such as a mouse and
|
|
1941
|
-
* a pen used together. Latest-start remains the right answer there.
|
|
1777
|
+
* handle wins.
|
|
1942
1778
|
*/
|
|
1943
1779
|
getActiveAction(): {
|
|
1944
1780
|
kind: string | null;
|
|
1945
1781
|
id: string | null;
|
|
1946
1782
|
};
|
|
1947
1783
|
/**
|
|
1948
|
-
* Start an ongoing action driven by UI (not a gesture)
|
|
1949
|
-
*
|
|
1950
|
-
* `action.invoker.start(ctx, { params })`, and registers the returned
|
|
1951
|
-
* handle in the in-flight map so `getInFlightHandles()` reports it —
|
|
1952
|
-
* enabling preview rendering via `SceneCanvas`.
|
|
1784
|
+
* Start an ongoing action driven by UI (not a gesture), and register its
|
|
1785
|
+
* handle in the in-flight map so `getInFlightHandles()` reports it.
|
|
1953
1786
|
*
|
|
1954
1787
|
* Returns `null` if `actionId` is unknown, the action's invoker is not
|
|
1955
1788
|
* ongoing, or `start` returned an empty handle.
|
|
@@ -1959,6 +1792,81 @@ interface Dispatcher {
|
|
|
1959
1792
|
*/
|
|
1960
1793
|
beginUiOngoing(actionId: string, deps: ActionDeps, params?: Record<string, unknown>): UiOngoingControl | null;
|
|
1961
1794
|
}
|
|
1795
|
+
|
|
1796
|
+
/**
|
|
1797
|
+
* Dev-only trace of what the dispatcher did: one record per resolved input and
|
|
1798
|
+
* one per mode switch, in a rolling buffer the dev tools read.
|
|
1799
|
+
*/
|
|
1800
|
+
|
|
1801
|
+
/** One entry per "mode change" — a kit state transition that isn't a
|
|
1802
|
+
* dispatched input event but explains why an input was (or wasn't) handled
|
|
1803
|
+
* later. Pushed via `recordModeSwitch()`. */
|
|
1804
|
+
interface ModeSwitchLogEntry {
|
|
1805
|
+
kind: 'mode';
|
|
1806
|
+
ts: number;
|
|
1807
|
+
/** Logical mode name, e.g. `'editAnchors.editingId'`. */
|
|
1808
|
+
mode: string;
|
|
1809
|
+
from: string | null;
|
|
1810
|
+
to: string | null;
|
|
1811
|
+
/** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
|
|
1812
|
+
* `'selection-dropped target'`, etc. */
|
|
1813
|
+
detail?: string;
|
|
1814
|
+
}
|
|
1815
|
+
/** A trace entry: one {@link DispatchRecord} per resolved input, or a mode switch. */
|
|
1816
|
+
type TraceLogEntry = DispatchRecord | ModeSwitchLogEntry;
|
|
1817
|
+
/** Push a mode-switch record into the trace log. Safe to call from
|
|
1818
|
+
* anywhere; no-ops outside DEV. */
|
|
1819
|
+
declare function recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* The slot an in-flight ongoing handle is keyed by:
|
|
1823
|
+
* - `key-held`: `key-held-<code>` (`key-held-Space`), so two keys that share a
|
|
1824
|
+
* `key`, such as the two Shifts, hold separately. An event with no code keys
|
|
1825
|
+
* by `key`.
|
|
1826
|
+
* - `pointerdown` and the drag that follows: `pointer-<pointerId>`. An event
|
|
1827
|
+
* with no `pointerId` (a synthesized probe, a programmatic drag, most tests)
|
|
1828
|
+
* keys to `pointer-mouse`.
|
|
1829
|
+
* - `multitouch`: `multitouch-<fingers>`.
|
|
1830
|
+
* - any other kind that starts an ongoing invoker: `ongoing-<kind>`.
|
|
1831
|
+
*/
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* The in-flight handle slot a pointer's gestures key into. Exported because
|
|
1835
|
+
* the React seam (`useGestureDispatcher`) has to name the same slot when it
|
|
1836
|
+
* asks `dispatcher.inFlight()` whether a given pointer is mid-drag.
|
|
1837
|
+
*
|
|
1838
|
+
* @param pointerId - `PointerEvent.pointerId`, or `undefined` for a
|
|
1839
|
+
* synthesized event with no originating DOM pointer.
|
|
1840
|
+
*/
|
|
1841
|
+
declare function pointerGestureId(pointerId: number | undefined): string;
|
|
1842
|
+
/** The in-flight handle slot a multitouch gesture of `fingers` fingers keys into. */
|
|
1843
|
+
declare function multiTouchGestureId(fingers: number): string;
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Dispatcher orchestrator — pure module, no React, no DOM.
|
|
1847
|
+
*
|
|
1848
|
+
* ## Precedence and fall-through
|
|
1849
|
+
* An exclusive affordance claim first bars every binding that does not
|
|
1850
|
+
* consult the affordance. The rest are ranked, best first, by:
|
|
1851
|
+
* 1. bindings naming the routed view ahead of every other
|
|
1852
|
+
* (`preferViewScoped`);
|
|
1853
|
+
* 2. scope tier: hotkey > active > ambient;
|
|
1854
|
+
* 3. specificity within a tier (`specificity()` in `matcher.ts`);
|
|
1855
|
+
* 4. an action gated by an `eligible` rule that holds now ahead of one with
|
|
1856
|
+
* no rule (`preferContextual`) — Escape while editing a path exits the
|
|
1857
|
+
* edit rather than resetting the tool;
|
|
1858
|
+
* 5. registration order.
|
|
1859
|
+
* Candidates whose `eligible` rule is false are dropped first. The dispatcher
|
|
1860
|
+
* walks the rest and fires the first action whose `enabled()` returns
|
|
1861
|
+
* `true`. If every candidate's `enabled()` returns a disabled reason, the
|
|
1862
|
+
* event is unhandled.
|
|
1863
|
+
*
|
|
1864
|
+
* ## Action-lookup miss behavior
|
|
1865
|
+
* When the walk reaches a binding whose `actionId` has no entry in
|
|
1866
|
+
* `ctx.actions.list()`, the dispatcher emits `console.warn` and returns
|
|
1867
|
+
* `'unhandled'`, so the gesture falls through as if unmatched.
|
|
1868
|
+
*/
|
|
1869
|
+
|
|
1962
1870
|
/** Build a dispatcher. `getAction` overrides how action ids are resolved;
|
|
1963
1871
|
* by default the `DispatcherContext`'s registry is used. */
|
|
1964
1872
|
declare function createDispatcher(opts?: {
|
|
@@ -1998,7 +1906,9 @@ interface ActionsRegistry {
|
|
|
1998
1906
|
/** Fire an immediate-invoker action by id. The optional `params` arg is
|
|
1999
1907
|
* forwarded to `ImmediateInvoker.run` as its second argument — use it for
|
|
2000
1908
|
* parametric actions (e.g. `trigger('tool.activate', { toolId: 'rect' })`).
|
|
2001
|
-
*
|
|
1909
|
+
* Returns whether it ran one: `false` for an unknown id, for an action
|
|
1910
|
+
* whose `enabled` refuses, and for an ongoing-invoker action, which
|
|
1911
|
+
* `trigger` cannot reach (see `begin`). `enabled` is asked with no point. */
|
|
2002
1912
|
trigger(id: string, params?: Record<string, unknown>): boolean;
|
|
2003
1913
|
/**
|
|
2004
1914
|
* Subscribe to registry mutations. The callback fires after any
|
|
@@ -2023,13 +1933,22 @@ interface ActionsRegistry {
|
|
|
2023
1933
|
* Returns a release that takes out only this dispatcher; a `null` wires
|
|
2024
1934
|
* nothing. */
|
|
2025
1935
|
setDispatcher(d: Dispatcher | null): () => void;
|
|
2026
|
-
/** Make this registry's scope the one chrome above it reaches
|
|
2027
|
-
* `begin` and `list` on any ancestor then answer from
|
|
2028
|
-
* calls it on the pointerdown and
|
|
1936
|
+
/** Make this registry's scope the one chrome above it reaches, and the one
|
|
1937
|
+
* keys go to: `trigger`, `begin` and `list` on any ancestor then answer from
|
|
1938
|
+
* here. A dispatcher calls it on the pointerdown and wheel that land in its
|
|
1939
|
+
* scope; moving DOM focus alone does not. */
|
|
2029
1940
|
activate(): void;
|
|
2030
|
-
/** Whether a lookup made from the root of this registry's tree starts here
|
|
2031
|
-
*
|
|
1941
|
+
/** Whether a lookup made from the root of this registry's tree starts here.
|
|
1942
|
+
* A dispatcher with no canvas to press in asks this before handling a
|
|
1943
|
+
* keystroke. */
|
|
2032
1944
|
isActive(): boolean;
|
|
1945
|
+
/** Whether this registry's scope is the one last activated on the page, in
|
|
1946
|
+
* any tree: the question a canvas's dispatcher asks before handling a key
|
|
1947
|
+
* or a paste that reached the page. No scope holds the keys before the
|
|
1948
|
+
* first activation. */
|
|
1949
|
+
holdsKeys(): boolean;
|
|
1950
|
+
/** Let go of the keys if this scope holds them, as its canvas unmounts. */
|
|
1951
|
+
dropKeys(): void;
|
|
2033
1952
|
}
|
|
2034
1953
|
/** Validate that `id` is usable as an action id in the route grammar.
|
|
2035
1954
|
* Rejects ids that start with a reserved sigil (would shadow future
|
|
@@ -2060,6 +1979,12 @@ declare function offhandKeyFor(trigger: HotkeyTrigger): string;
|
|
|
2060
1979
|
* `opts.params.toolId` so the invoker's `start` can pick out which tool
|
|
2061
1980
|
* to engage. */
|
|
2062
1981
|
declare function buildToolOffhandBindings(specs: readonly ToolOffhandBindingSpec[]): BoundGesture[];
|
|
1982
|
+
/** The `key-held` spec for holding `key` (a `KeyboardEvent.key`, or several).
|
|
1983
|
+
* A modifier key gets every modifier optional, since its own keydown reports
|
|
1984
|
+
* its flag set. Any other key engages with Shift or Alt already down and not
|
|
1985
|
+
* with Cmd or Ctrl, so a letter trigger leaves its browser chord alone.
|
|
1986
|
+
* Every hold-a-key binding builds its spec here, so that rule has one home. */
|
|
1987
|
+
declare function heldKeySpec(key: string | string[]): KeyHeldSpec;
|
|
2063
1988
|
/** Build the consolidated `tool.offhand` action — "hold a key to engage a
|
|
2064
1989
|
* secondary tool" (e.g. Space-for-hand). `bindings` enumerates one
|
|
2065
1990
|
* `{ spec: keyHeld, opts: { params: { toolId } } }` per tool; the invoker
|
|
@@ -2070,7 +1995,7 @@ declare function makeToolOffhandAction(bindings: BoundGesture[]): Action;
|
|
|
2070
1995
|
|
|
2071
1996
|
/**
|
|
2072
1997
|
* Named predicates for `defaultBinding.target.kindOf`. Each matches the
|
|
2073
|
-
*
|
|
1998
|
+
* `TargetPredicate` shape, declaring whether it reads the hit,
|
|
2074
1999
|
* so they drop into action descriptors without a wrapper:
|
|
2075
2000
|
*
|
|
2076
2001
|
* target: { kindOf: isAnchor }
|
|
@@ -2079,10 +2004,9 @@ declare function makeToolOffhandAction(bindings: BoundGesture[]): Action;
|
|
|
2079
2004
|
* a new predicate here is what enables every action that needs the same
|
|
2080
2005
|
* shape to read it. Drift prevention.
|
|
2081
2006
|
*/
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
type BodyPredicate = ((target: unknown, bodyTarget?: string) => boolean) & {
|
|
2007
|
+
|
|
2008
|
+
/** A predicate that resolves from `bodyTarget` alone. */
|
|
2009
|
+
type BodyPredicate = TargetPredicate & {
|
|
2086
2010
|
readsAffordance: false;
|
|
2087
2011
|
};
|
|
2088
2012
|
/** Matches any body hit — selected or unselected node body. */
|
|
@@ -2094,22 +2018,26 @@ declare const isUnselectedBody: BodyPredicate;
|
|
|
2094
2018
|
/** Empty-canvas hit — no node beneath cursor. */
|
|
2095
2019
|
declare const isEmpty: BodyPredicate;
|
|
2096
2020
|
/** Any corner resize handle — top-left/top-right/bottom-left/bottom-right. */
|
|
2097
|
-
declare const isResizeHandle:
|
|
2021
|
+
declare const isResizeHandle: TargetPredicate & {
|
|
2022
|
+
readsAffordance: true;
|
|
2023
|
+
};
|
|
2098
2024
|
/** The rotate-around-center handle above the bounding box. */
|
|
2099
|
-
declare const isRotateHandle:
|
|
2025
|
+
declare const isRotateHandle: TargetPredicate & {
|
|
2026
|
+
readsAffordance: true;
|
|
2027
|
+
};
|
|
2100
2028
|
/** Any path anchor (the on-curve dot). */
|
|
2101
|
-
declare const isAnchor:
|
|
2029
|
+
declare const isAnchor: TargetPredicate & {
|
|
2030
|
+
readsAffordance: true;
|
|
2031
|
+
};
|
|
2102
2032
|
/** Either control handle (controlIn or controlOut). */
|
|
2103
|
-
declare const isControlHandle:
|
|
2033
|
+
declare const isControlHandle: TargetPredicate & {
|
|
2034
|
+
readsAffordance: true;
|
|
2035
|
+
};
|
|
2104
2036
|
/** Any of the three anchor-editing affordances (anchor + both controls). */
|
|
2105
|
-
declare const isAnchorOrControl:
|
|
2037
|
+
declare const isAnchorOrControl: TargetPredicate & {
|
|
2038
|
+
readsAffordance: true;
|
|
2039
|
+
};
|
|
2106
2040
|
|
|
2107
|
-
/**
|
|
2108
|
-
* The long-press the gesture dispatcher is timing, as observable state: where
|
|
2109
|
-
* the press landed, when the hold started, how far through it is, and whether
|
|
2110
|
-
* any binding would fire on it. Feedback — a filling ring, a haptic — reads
|
|
2111
|
-
* this; nothing here decides what the feedback looks like.
|
|
2112
|
-
*/
|
|
2113
2041
|
/** A press being held toward a long-press. */
|
|
2114
2042
|
interface PendingLongPress {
|
|
2115
2043
|
pointerId: number;
|
|
@@ -2162,8 +2090,7 @@ interface LongPressState {
|
|
|
2162
2090
|
interface LongPressStore extends LongPressState {
|
|
2163
2091
|
set(next: PendingLongPress | null): void;
|
|
2164
2092
|
}
|
|
2165
|
-
|
|
2166
|
-
declare const LONG_PRESS_MS = 500;
|
|
2093
|
+
|
|
2167
2094
|
/** How `useGestureDispatcher` times, publishes and signals a long-press. */
|
|
2168
2095
|
interface LongPressOptions {
|
|
2169
2096
|
/** Hold time in ms. Default {@link LONG_PRESS_MS}. */
|
|
@@ -2181,6 +2108,7 @@ interface LongPressOptions {
|
|
|
2181
2108
|
*/
|
|
2182
2109
|
haptics?: boolean;
|
|
2183
2110
|
}
|
|
2111
|
+
/** A store holding the press currently being held, or none. Pass it as `LongPressOptions.state`. */
|
|
2184
2112
|
declare function createLongPressStore(): LongPressStore;
|
|
2185
2113
|
|
|
2186
2114
|
/**
|
|
@@ -2265,6 +2193,29 @@ declare function liveScope(id: string, eligibility: Eligibility, state: Eligibil
|
|
|
2265
2193
|
* of two same-specificity bindings in one tier wins.
|
|
2266
2194
|
*/
|
|
2267
2195
|
declare function scopeBindings(entries: Iterable<Contribution<unknown>>, state: EligibilityState): ScopedBinding[];
|
|
2196
|
+
/** What decides which entries are live, and in what order they are walked. */
|
|
2197
|
+
interface AssemblyInputs {
|
|
2198
|
+
focusedId: string | null;
|
|
2199
|
+
/** Ids held by a hotkey, oldest first, as the active-tool context keeps them. */
|
|
2200
|
+
hotkeyStack: readonly string[];
|
|
2201
|
+
/** The capability tags the active mode allows. Omitted: no mode system, so
|
|
2202
|
+
* every entry is allowed. */
|
|
2203
|
+
allowedCapabilities?: ReadonlySet<CapabilityTag>;
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* The entry set as routing walks it, with the state that tiers it. The
|
|
2207
|
+
* dispatcher's bindings and a canvas's tool overlays both come from here, so a
|
|
2208
|
+
* tool the dispatcher would not route to is never drawn, and two held tools
|
|
2209
|
+
* stack in one order.
|
|
2210
|
+
*
|
|
2211
|
+
* Held entries lead, newest hold first: the top of the stack is the one every
|
|
2212
|
+
* reader treats as engaged, so it wins a tie within the hotkey tier. A tagged
|
|
2213
|
+
* entry is live only while the mode allows every one of its tags.
|
|
2214
|
+
*/
|
|
2215
|
+
declare function assembleEntries<T extends Contribution<unknown>>(entries: Iterable<T>, inputs: AssemblyInputs): {
|
|
2216
|
+
ordered: T[];
|
|
2217
|
+
state: EligibilityState;
|
|
2218
|
+
};
|
|
2268
2219
|
|
|
2269
2220
|
/**
|
|
2270
2221
|
* Concatenate contribution bundles into one registry, preserving order —
|
|
@@ -2307,6 +2258,10 @@ declare function modeShortcutSpec(shortcut: string): KeySpec;
|
|
|
2307
2258
|
* `exit` / `discard` / `commit` / `cancel` while that mode is active, `entry`
|
|
2308
2259
|
* while it is not. Install it with `<SceneCanvas ambient>`.
|
|
2309
2260
|
*
|
|
2261
|
+
* An entry key one mode shares with another mode's leaving key is reported as
|
|
2262
|
+
* a route conflict unless `canEnter` is given: that is what keeps the entry
|
|
2263
|
+
* key from firing inside the other mode.
|
|
2264
|
+
*
|
|
2310
2265
|
* The bindings ride the hotkey tier, so leaving a mode outranks what the same
|
|
2311
2266
|
* key does otherwise (Escape clearing the selection), and they yield while a
|
|
2312
2267
|
* gesture is in flight, so Escape mid-drag still cancels the drag first. Keys
|
|
@@ -2315,9 +2270,8 @@ declare function modeShortcutSpec(shortcut: string): KeySpec;
|
|
|
2315
2270
|
declare function modeShortcuts(registry: ModeRegistry, handlers: ModeShortcutHandlers): Contribution<never>;
|
|
2316
2271
|
|
|
2317
2272
|
/**
|
|
2318
|
-
*
|
|
2319
|
-
*
|
|
2320
|
-
* only the fields meaningful to a caller-supplied tool-select key —
|
|
2273
|
+
* A tool's activation key, as `ToolDef.keybinding` declares it. Captures
|
|
2274
|
+
* only the fields meaningful to a tool-select key —
|
|
2321
2275
|
* dispatcher-internal fields (`skipInEditable`, `enabled`,
|
|
2322
2276
|
* `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
|
|
2323
2277
|
* not part of the configurable surface.
|
|
@@ -2337,104 +2291,36 @@ interface ToolKeybinding {
|
|
|
2337
2291
|
}
|
|
2338
2292
|
/** Modifier-key snapshot at event dispatch time. */
|
|
2339
2293
|
type ToolModifiers = ModifierState;
|
|
2340
|
-
/** Per-event context passed to every channel handler. `scratch` is typed
|
|
2341
|
-
* via the tool's `TScratch` parameter; it survives across a single
|
|
2342
|
-
* gesture (pointer-down through end/cancel) and is replaced on next
|
|
2343
|
-
* gesture start by `initScratch()`. */
|
|
2344
|
-
interface ToolCtx<TScratch = unknown> {
|
|
2345
|
-
worldX: number;
|
|
2346
|
-
worldY: number;
|
|
2347
|
-
modifiers: ToolModifiers;
|
|
2348
|
-
selection: SelectionApi;
|
|
2349
|
-
/** Adapter/scene access — opaque at this layer; tools that need it
|
|
2350
|
-
* cast to a known shape. This layer doesn't constrain it. */
|
|
2351
|
-
adapter: unknown;
|
|
2352
|
-
applyOps: (ops: Op[], label: string) => void;
|
|
2353
|
-
/** Current viewport. Reflects camera-position semantics — see
|
|
2354
|
-
* `View` JSDoc. */
|
|
2355
|
-
view: View;
|
|
2356
|
-
/** Mutate the viewport. In controlled mode this calls the consumer's
|
|
2357
|
-
* `onViewChange`; in uncontrolled mode it updates Canvas's internal
|
|
2358
|
-
* state. View changes are not undoable. */
|
|
2359
|
-
setView: (next: View) => void;
|
|
2360
|
-
/** Bounding rect of the canvas element in viewport coords. Used by
|
|
2361
|
-
* zoom/pan tools to convert event clientX/clientY to canvas-relative
|
|
2362
|
-
* anchors. */
|
|
2363
|
-
canvasRect: DOMRect;
|
|
2364
|
-
/** Optional debug sink. When `<Canvas debug={...}>` is enabled, Canvas
|
|
2365
|
-
* threads its sink here so tool-internal hit math (handle hitboxes,
|
|
2366
|
-
* rotation handle, etc.) lands in the same overlay as Canvas's own
|
|
2367
|
-
* bounds/origin records. Tools should call this conditionally with `?.`. */
|
|
2368
|
-
debug?: DebugSink;
|
|
2369
|
-
scratch: TScratch;
|
|
2370
|
-
}
|
|
2371
|
-
/**
|
|
2372
|
-
* What `onActivate` / `onDeactivate` receive: the tool's scratch, from
|
|
2373
|
-
* `initScratch()`. A lifecycle moment has no pointer, hit or modifier state to
|
|
2374
|
-
* report, so nothing else is populated; a callback needing the selection or
|
|
2375
|
-
* the scene closes over it, as the tool's actions do.
|
|
2376
|
-
*/
|
|
2377
|
-
interface ToolLifecycleCtx<TScratch = unknown> {
|
|
2378
|
-
scratch: TScratch;
|
|
2379
|
-
}
|
|
2380
|
-
/** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
|
|
2381
|
-
* `Bounds` type — the optional `rotation` field carries through so a tool
|
|
2382
|
-
* can report an oriented preview rect (e.g. mid-rotate). */
|
|
2383
|
-
type ToolBounds = Bounds;
|
|
2384
2294
|
/**
|
|
2385
2295
|
* The focus-declaring case of a `Contribution`: a mode the user switches
|
|
2386
|
-
* into, plus the hooks that only make sense for one (
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
2389
|
-
*/
|
|
2390
|
-
interface Tool<
|
|
2391
|
-
/**
|
|
2392
|
-
*
|
|
2393
|
-
*
|
|
2394
|
-
*
|
|
2395
|
-
* `useKeybindings.ts` picks this up and appends a binding entry to the
|
|
2396
|
-
* consolidated `tool.activate` action (with `opts.params.toolId` set so
|
|
2397
|
-
* the invoker knows which tool to switch to). */
|
|
2296
|
+
* into, plus the hooks that only make sense for one (activate/deactivate,
|
|
2297
|
+
* `cursor`). Everything else — bindings, actions, overlay, presentation — is
|
|
2298
|
+
* inherited.
|
|
2299
|
+
*/
|
|
2300
|
+
interface Tool<TOverlay = KernelOverlay> extends Contribution<TOverlay> {
|
|
2301
|
+
/** The key that activates this tool. `useKeybindings` appends a binding
|
|
2302
|
+
* entry for it to the consolidated `tool.activate` action (with
|
|
2303
|
+
* `opts.params.toolId` set so the invoker knows which tool to switch to).
|
|
2304
|
+
* A tool with none has no activation key. */
|
|
2398
2305
|
keybinding?: ToolKeybinding;
|
|
2399
|
-
|
|
2400
|
-
cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
|
|
2306
|
+
cursor?: CursorSpec | (() => CursorSpec);
|
|
2401
2307
|
/** The tool became live: it took the active slot, or its `hotkey` went
|
|
2402
|
-
* down, while it held neither.
|
|
2403
|
-
|
|
2308
|
+
* down, while it held neither. A lifecycle moment has no pointer, hit or
|
|
2309
|
+
* modifier state to report; a callback needing the selection or the scene
|
|
2310
|
+
* closes over it, as the tool's actions do. */
|
|
2311
|
+
onActivate?: () => void;
|
|
2404
2312
|
/** The tool stopped being live in both slots, or its canvas unmounted. */
|
|
2405
|
-
onDeactivate?: (
|
|
2406
|
-
/** Returns the in-flight preview pose for `id` if this tool is mid-gesture
|
|
2407
|
-
* on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
|
|
2408
|
-
* reflect live gesture state without reaching into hook internals. The
|
|
2409
|
-
* return type is `unknown` here because the Tool interface is pose-agnostic;
|
|
2410
|
-
* callers that know the pose shape (e.g. Canvas typed by `TPose`) cast at
|
|
2411
|
-
* the use site. */
|
|
2412
|
-
previewPose?: (id: string) => unknown;
|
|
2413
|
-
/** Returns the in-flight preview bounds for `id` if this tool is mid-gesture
|
|
2414
|
-
* on it; otherwise `null`. Optional companion to `previewPose` for tools that
|
|
2415
|
-
* can compute bounds without round-tripping through a geometry adapter. */
|
|
2416
|
-
previewBounds?: (id: string) => ToolBounds | null;
|
|
2417
|
-
/** Returns ids whose committed scene-render should be suppressed while this
|
|
2418
|
-
* tool is mid-gesture (e.g. cascade move's dragged + descendant ids whose
|
|
2419
|
-
* preview ghosts replace the committed pose). The standard scene slot
|
|
2420
|
-
* consults this alongside `previewPose` to avoid double-rendering. Returns
|
|
2421
|
-
* `null` when no gesture is in flight. */
|
|
2422
|
-
previewIds?: () => Iterable<string> | null;
|
|
2313
|
+
onDeactivate?: () => void;
|
|
2423
2314
|
}
|
|
2424
2315
|
/** Whether `entry` is a tool: a contribution that can hold focus, chosen from
|
|
2425
|
-
* a palette or held on a key. Only a tool's focus hooks (`
|
|
2426
|
-
* `
|
|
2427
|
-
declare function isTool<TOverlay>(entry: Contribution<TOverlay>): entry is Tool<
|
|
2316
|
+
* a palette or held on a key. Only a tool's focus hooks (`cursor`,
|
|
2317
|
+
* `onActivate`, `onDeactivate`) ever run. */
|
|
2318
|
+
declare function isTool<TOverlay>(entry: Contribution<TOverlay>): entry is Tool<TOverlay>;
|
|
2428
2319
|
/** Internal — which slot a tool occupies in the dispatch order. */
|
|
2429
2320
|
type ToolSlot = 'hotkey' | 'active' | 'ambient';
|
|
2430
|
-
/**
|
|
2431
|
-
* dispatchers that
|
|
2432
|
-
|
|
2433
|
-
* is too strict for containers that accept any concrete `Tool<T>`. */
|
|
2434
|
-
type AnyTool = Tool<any, any>;
|
|
2435
|
-
/** "A tool of any scratch type that draws `TOverlay`" — the container form for
|
|
2436
|
-
* registries that hold heterogeneous scratch shapes but one overlay type. */
|
|
2437
|
-
type AnyToolOf<TOverlay> = Tool<any, TOverlay>;
|
|
2321
|
+
/** A tool drawing any overlay type — the element type for registries and
|
|
2322
|
+
* dispatchers that never read the overlay. */
|
|
2323
|
+
type AnyTool = Tool<any>;
|
|
2438
2324
|
|
|
2439
2325
|
/**
|
|
2440
2326
|
* What a tool declares.
|
|
@@ -2445,7 +2331,7 @@ type AnyToolOf<TOverlay> = Tool<any, TOverlay>;
|
|
|
2445
2331
|
* definition is mostly `bindings`, plus presentation and any actions the tool
|
|
2446
2332
|
* itself introduces.
|
|
2447
2333
|
*/
|
|
2448
|
-
interface ToolDef<
|
|
2334
|
+
interface ToolDef<TOverlay = KernelOverlay> {
|
|
2449
2335
|
id: string;
|
|
2450
2336
|
/** Capability tags for modality eligibility. `defineTool` forwards these
|
|
2451
2337
|
* onto `Tool.eligibility.capabilities`, which is where every gate reads
|
|
@@ -2468,7 +2354,7 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
|
|
|
2468
2354
|
* Introspection-only — do not make this load-bearing in production.
|
|
2469
2355
|
* Read off the def via `Tool.def` (the reflection escape hatch). */
|
|
2470
2356
|
hookName?: string;
|
|
2471
|
-
presentation?: ToolPresentation
|
|
2357
|
+
presentation?: ToolPresentation;
|
|
2472
2358
|
/** Optional caller-supplied activation key. Most built-in tools have their
|
|
2473
2359
|
* activation key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`;
|
|
2474
2360
|
* this field is for tools that want their activation key to be
|
|
@@ -2486,17 +2372,10 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
|
|
|
2486
2372
|
hotkey?: HotkeyTrigger;
|
|
2487
2373
|
/** Fires as the tool becomes live in the active or hotkey slot. See
|
|
2488
2374
|
* `Tool.onActivate`. */
|
|
2489
|
-
onActivate?: (
|
|
2375
|
+
onActivate?: () => void;
|
|
2490
2376
|
/** Fires as the tool stops being live in either slot. */
|
|
2491
|
-
onDeactivate?: (
|
|
2492
|
-
cursor?: CursorSpec | ((
|
|
2493
|
-
/** Override the default scratch initializer. Default is `() => null`
|
|
2494
|
-
* cast to `TScratch`, which works for tools whose scratch is fresh
|
|
2495
|
-
* every gesture. Tools that need scratch identity to survive across
|
|
2496
|
-
* gesture boundaries (e.g. the pen tool's multi-click subpath state)
|
|
2497
|
-
* pass a stable-ref-returning thunk here. The factory forwards this
|
|
2498
|
-
* onto the returned `Tool.initScratch`. */
|
|
2499
|
-
initScratch?: () => TScratch;
|
|
2377
|
+
onDeactivate?: () => void;
|
|
2378
|
+
cursor?: CursorSpec | (() => CursorSpec);
|
|
2500
2379
|
/** Declarative gesture bindings, forwarded onto `Tool.bindings`. The
|
|
2501
2380
|
* gesture dispatcher consults these at active scope while this tool is the
|
|
2502
2381
|
* active one, and at hotkey scope while it is held. This is the tool's
|
|
@@ -2509,7 +2388,7 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
|
|
|
2509
2388
|
*
|
|
2510
2389
|
* The layer's `draw` closure should read dynamic state through refs or
|
|
2511
2390
|
* closures captured in the enclosing render scope, and gate on it there —
|
|
2512
|
-
* `if (!
|
|
2391
|
+
* `if (!state.something) return []` — rather than expecting the kit to
|
|
2513
2392
|
* swap layers as the gesture progresses.
|
|
2514
2393
|
*
|
|
2515
2394
|
* Pass an array to contribute several layers; they render in order. */
|
|
@@ -2520,20 +2399,10 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
|
|
|
2520
2399
|
}
|
|
2521
2400
|
|
|
2522
2401
|
/**
|
|
2523
|
-
* Build a `Tool
|
|
2524
|
-
*
|
|
2525
|
-
* This used to be a translator: `ToolDef` carried `initial` / `engaged` phase
|
|
2526
|
-
* tables of hit-keyed route handlers, and `defineTool` compiled them into the
|
|
2527
|
-
* imperative `pointer` / `drag` / `keyboard` / `wheel` handlers the
|
|
2528
|
-
* tool-routing dispatcher called. Both ends of that are gone — tools declare
|
|
2529
|
-
* `bindings` and the gesture dispatcher routes them — so what remains is
|
|
2530
|
-
* identity plumbing plus the id check and the cursor resolver.
|
|
2531
|
-
*
|
|
2532
|
-
* It stays a function rather than becoming a spread because the id validation
|
|
2533
|
-
* and the `initScratch` / `cursor` defaults are worth applying uniformly, and
|
|
2534
|
-
* because `Tool.def` gives reflection a handle on the authored form.
|
|
2402
|
+
* Build a `Tool` from a declarative `ToolDef`: validates the id, applies the
|
|
2403
|
+
* `cursor` default, and keeps the authored form on `Tool.def` for reflection.
|
|
2535
2404
|
*/
|
|
2536
|
-
declare function defineTool<
|
|
2405
|
+
declare function defineTool<TOverlay = KernelOverlay>(def: ToolDef<TOverlay>): Tool<TOverlay>;
|
|
2537
2406
|
|
|
2538
2407
|
/**
|
|
2539
2408
|
* One row in the route registry — a single `GestureBinding` on one tool,
|
|
@@ -2581,6 +2450,10 @@ interface RegistryEntry {
|
|
|
2581
2450
|
/** The binding's `opts.views`: the views it is live in. Absent when it is
|
|
2582
2451
|
* live in every view. */
|
|
2583
2452
|
views?: readonly (string | null)[];
|
|
2453
|
+
/** The binding's `opts.params`, as declared: a thunk is not called. */
|
|
2454
|
+
params?: unknown;
|
|
2455
|
+
/** The binding's `opts.yields`, when it is set. */
|
|
2456
|
+
yields?: true;
|
|
2584
2457
|
}
|
|
2585
2458
|
/**
|
|
2586
2459
|
* Stand-in target for a `{ kindOf }` predicate spec.
|
|
@@ -2617,10 +2490,8 @@ declare function buildRouteRegistry(entries: readonly Contribution<unknown>[]):
|
|
|
2617
2490
|
*/
|
|
2618
2491
|
declare function routesForSpec(spec: GestureSpec): readonly string[];
|
|
2619
2492
|
|
|
2620
|
-
/** Two or more
|
|
2621
|
-
* modifiers) tuple
|
|
2622
|
-
* arbitrarily (well, deterministically by slot order, but the author
|
|
2623
|
-
* probably didn't intend the duplication). */
|
|
2493
|
+
/** Two or more bindings declare the same exact (phase, gesture, arg, target,
|
|
2494
|
+
* modifiers) tuple, so slot order alone picks one. */
|
|
2624
2495
|
interface Conflict {
|
|
2625
2496
|
phase: 'initial' | 'engaged' | 'any';
|
|
2626
2497
|
gesture: GestureName;
|
|
@@ -2637,41 +2508,36 @@ interface Conflict {
|
|
|
2637
2508
|
* - Broad vs. narrow targets (e.g. an untargeted `click` alongside
|
|
2638
2509
|
* `click` on `empty`) — the dispatcher's specificity ordering resolves
|
|
2639
2510
|
* those cleanly, and the broad one is usually the intended fallback.
|
|
2640
|
-
* -
|
|
2641
|
-
* different inputs
|
|
2642
|
-
*
|
|
2643
|
-
* one
|
|
2644
|
-
*
|
|
2645
|
-
*
|
|
2646
|
-
*
|
|
2647
|
-
* - Two `{ kindOf }` predicate targets. The route grammar renders every
|
|
2648
|
-
* predicate as the single token {@link PREDICATE_TARGET}, so bucketing on
|
|
2649
|
-
* the rendered target alone reported select's `resize` / `rotate` / `move`
|
|
2650
|
-
* drags — three genuinely different predicates — as a three-way conflict.
|
|
2651
|
-
* Predicate entries bucket by function identity instead, which means two
|
|
2652
|
-
* *separately written but equivalent* predicates go unflagged. That's the
|
|
2653
|
-
* right way to be wrong here: this check has to be silent when nothing is
|
|
2654
|
-
* wrong or nobody will keep it on.
|
|
2655
|
-
*
|
|
2511
|
+
* - Modifier requirements no one press satisfies at equal rank: Shift
|
|
2512
|
+
* required against Alt required fire on different inputs, and Shift
|
|
2513
|
+
* required outranks Shift optional. An optional modifier against a
|
|
2514
|
+
* forbidden one is flagged, since both take the bare press.
|
|
2515
|
+
* - Two `{ kindOf }` predicate targets, unless they are the same function.
|
|
2516
|
+
* Predicates bucket by identity, so two separately written but
|
|
2517
|
+
* equivalent ones go unflagged.
|
|
2656
2518
|
* - Two actions whose `eligible` rules can never hold together — each
|
|
2657
2519
|
* mode's own Escape exit, say. See {@link rulesExclusive}.
|
|
2658
|
-
*
|
|
2659
2520
|
* - Two bindings whose `opts.views` keep them from being live and equally
|
|
2660
|
-
* ranked in any one view.
|
|
2521
|
+
* ranked in any one view.
|
|
2522
|
+
* - Two bindings of one action with equal params that tie on one platform
|
|
2523
|
+
* only: `mod` against `ctrl`, which are one key off a Mac and two on it.
|
|
2524
|
+
* The same pair tying on both platforms is a plain duplicate and is flagged.
|
|
2661
2525
|
*
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2526
|
+
* A binding whose action declines via `enabled()` so a lower-priority one
|
|
2527
|
+
* takes the gesture is flagged all the same, since seeing that intent would
|
|
2528
|
+
* mean evaluating the action, unless it says so with `opts.yields`. A pair
|
|
2529
|
+
* whose earlier binding yields is not a conflict; one whose later binding
|
|
2530
|
+
* yields still is, as the earlier one decides alone.
|
|
2665
2531
|
*
|
|
2666
|
-
*
|
|
2667
|
-
*
|
|
2668
|
-
*
|
|
2532
|
+
* Two bindings on the same tool can share a tuple, so a conflict may name
|
|
2533
|
+
* one tool twice.
|
|
2534
|
+
*
|
|
2535
|
+
* Feeding this a whole tool *registry* over-reports, because registry tools
|
|
2536
|
+
* take turns in the active slot and can't collide with each other — see
|
|
2537
|
+
* `findScopedConflicts`.
|
|
2669
2538
|
*/
|
|
2670
2539
|
declare function findConflicts(entries: readonly Contribution<unknown>[]): Conflict[];
|
|
2671
|
-
/**
|
|
2672
|
-
* The tool scopes as the dispatcher sees them — which is what decides whether
|
|
2673
|
-
* two same-tuple bindings can actually collide.
|
|
2674
|
-
*/
|
|
2540
|
+
/** The tool scopes as the dispatcher sees them. */
|
|
2675
2541
|
interface ToolScopes {
|
|
2676
2542
|
/** Tools eligible for the active slot, keyed by id or as a flat list. */
|
|
2677
2543
|
registry: readonly Contribution<unknown>[] | Readonly<Record<string, Contribution<unknown>>>;
|
|
@@ -2688,17 +2554,13 @@ interface ToolScopes {
|
|
|
2688
2554
|
/**
|
|
2689
2555
|
* Detect reachable route conflicts in an assembled tool set and report each one.
|
|
2690
2556
|
*
|
|
2691
|
-
*
|
|
2692
|
-
*
|
|
2693
|
-
* looked. Call it once wherever a tool set is assembled (`useTools` does),
|
|
2694
|
-
* behind a `process.env.NODE_ENV !== 'production'` guard.
|
|
2557
|
+
* Call it once wherever a tool set is assembled, behind a
|
|
2558
|
+
* `process.env.NODE_ENV !== 'production'` guard.
|
|
2695
2559
|
*
|
|
2696
2560
|
* **Warn, never throw.** A conflict between a consumer's tool and a kit tool
|
|
2697
|
-
* is
|
|
2698
|
-
* the
|
|
2699
|
-
*
|
|
2700
|
-
* always a bug, but the place to fail on that is the kit's own test suite
|
|
2701
|
-
* (`canvas/SceneCanvas.routeConflicts.test.tsx`), not a consumer's console.
|
|
2561
|
+
* is sometimes deliberate, since the loser can still take the gesture when
|
|
2562
|
+
* the winner declines through `enabled()`. A binding ordered that way on
|
|
2563
|
+
* purpose says so with `opts.yields`, and is not reported.
|
|
2702
2564
|
*
|
|
2703
2565
|
* @returns The conflicts found, so callers can dedupe repeat reports.
|
|
2704
2566
|
*/
|
|
@@ -2712,6 +2574,7 @@ declare function reportRouteConflicts(scopes: ToolScopes, warn?: (message: strin
|
|
|
2712
2574
|
* Extracted from the deleted singular `useKeybinding` hook so the remaining
|
|
2713
2575
|
* consumers don't depend on its disappearance.
|
|
2714
2576
|
*/
|
|
2577
|
+
|
|
2715
2578
|
/**
|
|
2716
2579
|
* A key plus a modifier policy, as {@link matchesKeyBinding} tests it against a
|
|
2717
2580
|
* `KeyboardEvent`. Every modifier left unset is forbidden, so `{ key: 'a' }`
|
|
@@ -2744,14 +2607,6 @@ interface KeyBinding {
|
|
|
2744
2607
|
/** Call `preventDefault` before the handler. Default `true`. */
|
|
2745
2608
|
preventDefault?: boolean;
|
|
2746
2609
|
}
|
|
2747
|
-
/** Returns true when the target is an input, textarea, or contenteditable element. */
|
|
2748
|
-
declare function isEditableTarget(target: EventTarget | null): boolean;
|
|
2749
|
-
/**
|
|
2750
|
-
* True when `e` is Space or Enter on a focused control those keys activate — a
|
|
2751
|
-
* button, a link, a checkbox. The key is that control's, so a page-level
|
|
2752
|
-
* listener that claims it cancels the control's own activation.
|
|
2753
|
-
*/
|
|
2754
|
-
declare function activatesFocusedControl(e: KeyboardEvent): boolean;
|
|
2755
2610
|
/**
|
|
2756
2611
|
* Pure matcher for `KeyBinding` against a `KeyboardEvent`. Only checks key +
|
|
2757
2612
|
* modifier policy — does NOT apply the editable-target skip or
|
|
@@ -2759,38 +2614,21 @@ declare function activatesFocusedControl(e: KeyboardEvent): boolean;
|
|
|
2759
2614
|
* gesture dispatcher so the rules can't drift.
|
|
2760
2615
|
*/
|
|
2761
2616
|
declare function matchesKeyBinding(e: KeyboardEvent, b: KeyBinding): boolean;
|
|
2762
|
-
type ModifierKeys = Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>;
|
|
2763
2617
|
/** Which history step `e` asks for: Mod+Z undoes, Shift+Mod+Z and Mod+Y redo. `null` for anything else. Takes a
|
|
2764
2618
|
* React keyboard event as readily as a DOM one. */
|
|
2765
|
-
declare function historyKey(e:
|
|
2619
|
+
declare function historyKey(e: Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>): 'undo' | 'redo' | null;
|
|
2766
2620
|
|
|
2767
2621
|
/**
|
|
2768
|
-
* Typed keys for the gesture/behavior scratch store.
|
|
2769
|
-
*
|
|
2770
|
-
*
|
|
2771
|
-
* mutable bag that behaviors use to stash state across `onDown` / `onMove`
|
|
2772
|
-
* / `onEnd` calls. The store is `unknown`-valued because behaviors are
|
|
2773
|
-
* tool-agnostic — a single behavior may run under different tools with
|
|
2774
|
-
* different scratch shapes. Without a typing convention every read site
|
|
2775
|
-
* has to `(ctx.scratch as <some shape>)[KEY]`, and a writer changing the
|
|
2776
|
-
* shape can silently break readers with no compile-time signal.
|
|
2777
|
-
*
|
|
2778
|
-
* `ScratchKey<T>` is a phantom-typed string that carries its payload type
|
|
2779
|
-
* via a branded interface. `getScratch` and `setScratch` use the brand to
|
|
2780
|
-
* narrow / constrain at the typing layer; at runtime the key is just its
|
|
2781
|
-
* underlying string and the store remains a plain `Record`.
|
|
2622
|
+
* Typed keys for the gesture/behavior scratch store, `ctx.scratch`.
|
|
2623
|
+
* `ScratchKey<T>` carries its payload type at the typing layer; at runtime
|
|
2624
|
+
* the key is just its underlying string and the store remains a plain `Record`.
|
|
2782
2625
|
*
|
|
2783
2626
|
* @example
|
|
2784
2627
|
* ```ts
|
|
2785
|
-
* // Module-level
|
|
2628
|
+
* // Module-level constant, shared by writer + reader:
|
|
2786
2629
|
* const LASSO_VERTICES = scratchKey<readonly { x: number; y: number }[]>('lasso.vertices');
|
|
2787
|
-
*
|
|
2788
|
-
* // Writer (a tool's onMove):
|
|
2789
2630
|
* setScratch(ctx.scratch, LASSO_VERTICES, vertices);
|
|
2790
|
-
*
|
|
2791
|
-
* // Reader (a behavior's onEnd):
|
|
2792
2631
|
* const vertices = getScratch(ctx.scratch, LASSO_VERTICES) ?? [];
|
|
2793
|
-
* // ^? readonly { x: number; y: number }[] | undefined
|
|
2794
2632
|
* ```
|
|
2795
2633
|
*/
|
|
2796
2634
|
/** Phantom-typed key for a scratch slot. The `__scratchKeyPayload` phantom
|
|
@@ -2805,13 +2643,9 @@ interface ScratchKey<T> {
|
|
|
2805
2643
|
* field already satisfies this; consumers don't need to construct one. */
|
|
2806
2644
|
type ScratchStore = Record<string, unknown>;
|
|
2807
2645
|
/**
|
|
2808
|
-
* Make a typed scratch key.
|
|
2809
|
-
* is
|
|
2810
|
-
*
|
|
2811
|
-
* Namespace the name by behavior or feature to avoid collisions
|
|
2812
|
-
* (`'behavior.field'` is a good convention). Two keys with the same
|
|
2813
|
-
* `name` refer to the same slot at runtime — the type parameter is a
|
|
2814
|
-
* compile-time contract, not a uniqueness guarantee.
|
|
2646
|
+
* Make a typed scratch key. Two keys with the same `name` refer to the same
|
|
2647
|
+
* slot at runtime — the type parameter is a compile-time contract, not a
|
|
2648
|
+
* uniqueness guarantee — so namespace the name (`'behavior.field'`).
|
|
2815
2649
|
*/
|
|
2816
2650
|
declare function scratchKey<T>(name: string): ScratchKey<T>;
|
|
2817
2651
|
/** Read a typed slot from the scratch store. Returns `undefined` if
|
|
@@ -2852,6 +2686,14 @@ declare function itemsFromFiles(files: File[]): IngestItem[];
|
|
|
2852
2686
|
/** Materialize a drop's `DataTransfer`. MUST be called during the `drop`
|
|
2853
2687
|
* event dispatch (see module doc). */
|
|
2854
2688
|
declare function itemsFromDataTransfer(dt: DataTransfer): Promise<IngestItem[]>;
|
|
2689
|
+
/**
|
|
2690
|
+
* What a drag still in flight says it carries: one item per flavor, MIME
|
|
2691
|
+
* only. A browser withholds contents until the drop, so nothing may read
|
|
2692
|
+
* `file` or `text` off these; they exist to ask which bindings would match.
|
|
2693
|
+
* A file the browser does not name yet (Firefox reports
|
|
2694
|
+
* `application/x-moz-file`, others only a `Files` type) is `UNKNOWN_MIME`.
|
|
2695
|
+
*/
|
|
2696
|
+
declare function itemTypesFromDataTransfer(dt: DataTransfer): readonly IngestItem[];
|
|
2855
2697
|
/** Materialize a paste's `clipboardData`. Fully synchronous. */
|
|
2856
2698
|
declare function itemsFromClipboardData(cd: DataTransfer): IngestItem[];
|
|
2857
2699
|
|
|
@@ -2872,9 +2714,41 @@ declare function clientToCanvasRect(rect: {
|
|
|
2872
2714
|
top: number;
|
|
2873
2715
|
}, clientX: number, clientY: number): [number, number];
|
|
2874
2716
|
|
|
2717
|
+
/**
|
|
2718
|
+
* Namespaced, opt-in console tracing for the kit.
|
|
2719
|
+
*
|
|
2720
|
+
* Enable in the browser:
|
|
2721
|
+
*
|
|
2722
|
+
* localStorage.setItem('weasel.debug', '*') // all namespaces
|
|
2723
|
+
* localStorage.setItem('weasel.debug', 'tools,dispatch') // specific ones
|
|
2724
|
+
* localStorage.setItem('weasel.debug', '1') // legacy "all"
|
|
2725
|
+
*
|
|
2726
|
+
* Each call site picks a stable namespace string. Off by default; the flag
|
|
2727
|
+
* is read once and cached per page session — reload to pick up a change.
|
|
2728
|
+
*
|
|
2729
|
+
* The one definition: `@weasel-js/core` and `@weasel-js/ui` pass these on, so
|
|
2730
|
+
* every package reads the flag from one cache.
|
|
2731
|
+
*
|
|
2732
|
+
* Known kit namespaces:
|
|
2733
|
+
* - tools — active-tool + hotkey transitions (useTools)
|
|
2734
|
+
* - dispatch — gesture start / end / cancel (dispatcher)
|
|
2735
|
+
* - drag-rect — useDragRect lifecycle
|
|
2736
|
+
* - drag-radial — useDragRadial lifecycle
|
|
2737
|
+
* - scene — restored-op replay gaps (history persistence)
|
|
2738
|
+
* - scene-canvas — SceneCanvas mount / unmount; adapter paste-translation warnings
|
|
2739
|
+
* - history — applyOps push, no-op suppression, serialize/restore
|
|
2740
|
+
* - clipboard — OS clipboard write/read failures, malformed payloads
|
|
2741
|
+
*/
|
|
2742
|
+
/** True when `namespace` (or `*`) is enabled. Pass no argument to test
|
|
2743
|
+
* whether *any* namespace is on (legacy global-flag semantics). */
|
|
2744
|
+
declare function isDebugEnabled(namespace?: string): boolean;
|
|
2875
2745
|
/** Conditional `console.debug` — no-op unless `namespace` (or `*`) is enabled.
|
|
2876
2746
|
* Each call is prefixed with `[namespace]` so the source is searchable. */
|
|
2877
2747
|
declare function dlog(namespace: string, ...args: unknown[]): void;
|
|
2748
|
+
/** Conditional `console.warn` — like `dlog` but at warn level. Use for
|
|
2749
|
+
* diagnostics that indicate something worth surfacing during debugging
|
|
2750
|
+
* (e.g. no-op op batches suppressed). */
|
|
2751
|
+
declare function dwarn(namespace: string, ...args: unknown[]): void;
|
|
2878
2752
|
|
|
2879
2753
|
/**
|
|
2880
2754
|
* @weasel-js/routing — binding-to-action routing.
|
|
@@ -2889,10 +2763,9 @@ declare function dlog(namespace: string, ...args: unknown[]): void;
|
|
|
2889
2763
|
* `Action.requires`, `DepRegistry.register`/`get` and `useDepSource` are all
|
|
2890
2764
|
* keyed on.
|
|
2891
2765
|
*
|
|
2892
|
-
* It
|
|
2893
|
-
*
|
|
2894
|
-
*
|
|
2895
|
-
* those merges them in:
|
|
2766
|
+
* It declares one entry, `activeTool`, because the value and the action that
|
|
2767
|
+
* requires it (`tool.offhand`) are both routing's. Whoever supplies the rest
|
|
2768
|
+
* merges them in:
|
|
2896
2769
|
*
|
|
2897
2770
|
* ```ts
|
|
2898
2771
|
* declare module '@weasel-js/routing' {
|
|
@@ -2900,15 +2773,12 @@ declare function dlog(namespace: string, ...args: unknown[]): void;
|
|
|
2900
2773
|
* }
|
|
2901
2774
|
* ```
|
|
2902
2775
|
*
|
|
2903
|
-
* `@weasel-js/core` merges the kit's own
|
|
2904
|
-
* `DepSchema`, so a consumer adds its own against `'@weasel-js/core'
|
|
2905
|
-
* never has to know this module exists. The merge carries through the
|
|
2906
|
-
* re-export in both directions — `scripts/smoke-consumer-bundle.mjs` asserts
|
|
2907
|
-
* it against the published `.d.ts`, which is where it once broke: a
|
|
2908
|
-
* cross-module augmentation stops merging when `rollup-plugin-dts` flattens
|
|
2909
|
-
* both files into one chunk.
|
|
2776
|
+
* `@weasel-js/core` merges the kit's own deps this way and re-exports
|
|
2777
|
+
* `DepSchema`, so a consumer adds its own against `'@weasel-js/core'`.
|
|
2910
2778
|
*/
|
|
2911
2779
|
interface DepSchema {
|
|
2780
|
+
/** Currently active tool id + hotkey-hold stack. */
|
|
2781
|
+
activeTool: ActiveToolContextValue;
|
|
2912
2782
|
}
|
|
2913
2783
|
/** Every dep name declared by anything that has merged into {@link DepSchema}. */
|
|
2914
2784
|
type DepName = keyof DepSchema;
|
|
@@ -2933,4 +2803,4 @@ type KernelOverlay = OverlaySchema extends {
|
|
|
2933
2803
|
overlay: infer O;
|
|
2934
2804
|
} ? O : unknown;
|
|
2935
2805
|
|
|
2936
|
-
export { type
|
|
2806
|
+
export { type DeviceProfile as $, type ActionsRegistry as A, type BindingOpts as B, type Contribution as C, type DepRegistry as D, type BindingScope as E, type BindingSource as F, type BodyPredicate as G, type BoundGesture as H, type Bounds as I, type BuildRuleCtxArgs as J, type KernelOverlay as K, type LongPressOptions as L, COARSE_TARGET_SCALE as M, type NodeId as N, type OverlayPosition as O, type Condition as P, type Conflict as Q, type RuleCtx as R, ScopeNode as S, ToolNode as T, type ContributionChrome as U, type ContributionDeps as V, type ContributionRouting as W, DEFAULT_ALLOWED_CAPABILITIES as X, DEFAULT_DEVICE_PROFILE as Y, type DebugSink as Z, type DetectedDeviceFacts as _, type DepName as a, actionBindings as a$, type DispatchRecord as a0, type DispatchRecordInput as a1, type DispatcherContext as a2, type DragSample as a3, type DroppedCandidate as a4, type Eligibility as a5, type EligibilityState as a6, type GestureBinding as a7, type HandleKind as a8, type HitShape as a9, type PointerSessionOptions as aA, type RankedCandidate as aB, type RecordCandidate as aC, type RegistryEntry as aD, type ResizeAnchor as aE, type ResolveAllOptions as aF, type ResolveOnlyResult as aG, type ResolvedCandidate as aH, type Rule as aI, type ScopedBinding as aJ, type ScratchKey as aK, type ScratchStore as aL, type SelectionApi as aM, type Selector as aN, type SpecificityPart as aO, TOOL_OFFHAND_ID as aP, type ToolDef as aQ, type ToolKeybinding as aR, type ToolModifiers as aS, type ToolOffhandBindingSpec as aT, type ToolPresentation as aU, type ToolScopes as aV, type ToolSlot as aW, type TraceLogEntry as aX, type UiOngoingControl as aY, type ViewportGestureKind as aZ, type WalkStep as a_, type HotkeyTrigger as aa, INGEST_STRING_MIMES as ab, type ImmediateInvoker as ac, type InvocationCtx as ad, type Invoker as ae, type KeyBinding as af, type LongPressState as ag, type LongPressStore as ah, type MatchOutcome as ai, type MatchResult as aj, type ModeShortcutHandlers as ak, type ModeSwitchLogEntry as al, type ModifierState as am, NEVER as an, type OngoingHandle as ao, type OngoingInvoker as ap, type OngoingOverlay as aq, type OverlayRole as ar, type OverlaySchema as as, PREDICATE_TARGET as at, type PendingLongPress as au, type PlacedBy as av, type Point2 as aw, type PointerSession as ax, type PointerSessionCallbacks as ay, type PointerSessionCancelReason as az, type DepSchema as b, validateActionId as b$, actionItems as b0, assembleEntries as b1, buildDepsFromRequires as b2, buildRouteRegistry as b3, buildRuleCtx as b4, buildToolOffhandBindings as b5, clientToCanvas as b6, clientToCanvasRect as b7, createDepRegistry as b8, createDispatcher as b9, itemsFromDataTransfer as bA, itemsFromFiles as bB, liveScope as bC, makeToolOffhandAction as bD, matchBest as bE, matchSorted as bF, matchSortedWithBarred as bG, matchesKeyBinding as bH, mergeContributions as bI, modeShortcutSpec as bJ, modeShortcuts as bK, multiTouchGestureId as bL, offhandKeyFor as bM, openPointerSession as bN, pointerGestureId as bO, recordModeSwitch as bP, reportRouteConflicts as bQ, resolveDeviceProfile as bR, resolveParams as bS, routesForSpec as bT, ruleCanHoldIn as bU, rulesExclusive as bV, scopeBindings as bW, scratchKey as bX, setScratch as bY, targetConsultsAffordance as bZ, validateActionDefaultBinding as b_, createLongPressStore as ba, defineTool as bb, deleteScratch as bc, describeRule as bd, dlog as be, dwarn as bf, evaluate as bg, evaluateActive as bh, evaluateEnabled as bi, findConflicts as bj, getScratch as bk, heldKeySpec as bl, historyKey as bm, isAnchor as bn, isAnchorOrControl as bo, isBody as bp, isControlHandle as bq, isDebugEnabled as br, isEmpty as bs, isResizeHandle as bt, isRotateHandle as bu, isSelectedBody as bv, isTool as bw, isUnselectedBody as bx, itemTypesFromDataTransfer as by, itemsFromClipboardData as bz, type Action as c, DepNode as d, type Tool as e, ActiveToolContextProvider as f, ActiveToolContextProviderIfRoot as g, type ActiveToolContextProviderProps as h, type ActiveToolContextValue as i, useOptionalActiveToolContext as j, type Dispatcher as k, type AffordanceHit as l, ALWAYS as m, type ActionDeps as n, ActionDisabledReason as o, type ActionDispatch as p, type ActionEnabledResult as q, type ActionEntry as r, type ActionItem as s, type ActionPresentation as t, useActiveToolContext as u, type ActionSource as v, type ActionVariant as w, type ActionsProp as x, type AnyTool as y, type AssemblyInputs as z };
|