@weasel-js/routing 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/chunk-Y5FHAA4U.js +1504 -0
- package/dist/chunk-Y5FHAA4U.js.map +1 -0
- package/dist/index-BOlkWrA-.d.ts +2446 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +405 -0
- package/dist/react.js +1304 -0
- package/dist/react.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,2446 @@
|
|
|
1
|
+
import { CapabilityTag } from '@weasel-js/modes';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import { CursorSpec } from '@weasel-js/cursor';
|
|
5
|
+
import { ClaimableGesture, GestureSpec, InputEvent, TargetSpec, ParsedModifiers, GestureName, IngestItem } from '@weasel-js/gestures';
|
|
6
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
7
|
+
import { Op } from '@weasel-js/history';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The primitives the routing surface is typed in.
|
|
11
|
+
*
|
|
12
|
+
* Routing decides which action a gesture runs. To do that it has to name a
|
|
13
|
+
* viewport, a rect, a node id and a selection — but it never constructs,
|
|
14
|
+
* projects or renders any of them. So they are declared here as structural
|
|
15
|
+
* interfaces with no dependencies, and `@weasel-js/core` re-exports each from
|
|
16
|
+
* the path its own call sites have always used. Same arrangement as `Op`,
|
|
17
|
+
* which is declared in `@weasel-js/history` and re-exported by
|
|
18
|
+
* `core/ops/types`.
|
|
19
|
+
*
|
|
20
|
+
* Authority for the *behavior* behind each of these stays in core: `View` is
|
|
21
|
+
* what `core/viewport` maintains, `SelectionApi` is what `useSelection`
|
|
22
|
+
* returns, `DeviceProfile` is what `resolveDeviceProfile` detects. Changing
|
|
23
|
+
* one of these shapes is a change to core's contract that happens to be
|
|
24
|
+
* spelled here.
|
|
25
|
+
*/
|
|
26
|
+
/** An opaque scene-node identifier. Branded so a bare string cannot stand in. */
|
|
27
|
+
type NodeId = string & {
|
|
28
|
+
readonly __brand: 'NodeId';
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Viewport state. `(view.x, view.y)` is the world point currently rendered at
|
|
32
|
+
* the canvas top-left; `view.scale.x` / `view.scale.y` is pixels per world
|
|
33
|
+
* unit on each axis.
|
|
34
|
+
*
|
|
35
|
+
* The one outright 2D dead end in the routing surface: a `View` names no
|
|
36
|
+
* orientation, so a kernel with a camera cannot express its viewport as one.
|
|
37
|
+
*/
|
|
38
|
+
interface View {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
scale: {
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Axis-aligned rectangle in world space. The optional `rotation` (radians,
|
|
48
|
+
* around the AABB center) lets chrome attach an orientation to an otherwise
|
|
49
|
+
* axis-aligned rect without a parallel type.
|
|
50
|
+
*/
|
|
51
|
+
interface Bounds {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
rotation?: number;
|
|
57
|
+
}
|
|
58
|
+
/** Selection click policy. `single` always replaces; `multi` toggles when the
|
|
59
|
+
* configured extend key is held, otherwise replaces. */
|
|
60
|
+
type SelectionMode = 'single' | 'multi';
|
|
61
|
+
/** Modifier key used to extend the selection in `multi` mode. */
|
|
62
|
+
type SelectionExtendKey = 'shift' | 'meta' | 'ctrl';
|
|
63
|
+
/** The selection as routing reads and writes it. */
|
|
64
|
+
interface SelectionApi {
|
|
65
|
+
/** Current selection. Re-renders trigger when this reference changes. */
|
|
66
|
+
current: readonly NodeId[];
|
|
67
|
+
/** Imperative read for use inside event callbacks (avoids stale closures). */
|
|
68
|
+
get(): NodeId[];
|
|
69
|
+
/** Replace selection. */
|
|
70
|
+
set(ids: NodeId[]): void;
|
|
71
|
+
/** Add id (multi-mode appends; single-mode replaces). */
|
|
72
|
+
add(id: NodeId): void;
|
|
73
|
+
/** Remove id from selection. */
|
|
74
|
+
remove(id: NodeId): void;
|
|
75
|
+
/** Toggle id in/out of selection. */
|
|
76
|
+
toggle(id: NodeId): void;
|
|
77
|
+
/** Clear selection. */
|
|
78
|
+
clear(): void;
|
|
79
|
+
/** True if id is selected. */
|
|
80
|
+
contains(id: NodeId): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Apply a click to the selection per the configured mode/extend key.
|
|
83
|
+
* - `single`: replaces selection with `[id]`, regardless of modifiers.
|
|
84
|
+
* - `multi`: with the extend key held, toggles `id` in/out of the selection;
|
|
85
|
+
* otherwise replaces with `[id]`.
|
|
86
|
+
*/
|
|
87
|
+
applyClick(id: NodeId, modifiers: {
|
|
88
|
+
shift: boolean;
|
|
89
|
+
meta: boolean;
|
|
90
|
+
ctrl: boolean;
|
|
91
|
+
}): void;
|
|
92
|
+
/** Pre-built methods for spreading into an adapter that needs them. */
|
|
93
|
+
adapterMethods: {
|
|
94
|
+
getSelection: () => NodeId[];
|
|
95
|
+
setSelection: (ids: NodeId[]) => void;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Facts about the device the canvas is running on, as the eligibility rules
|
|
100
|
+
* read them (`coarsePointer`, `canHover`). Deliberately not a form-factor
|
|
101
|
+
* concept — there is no `isPhone` here and there should never be one.
|
|
102
|
+
*/
|
|
103
|
+
interface DeviceProfile {
|
|
104
|
+
/** `matchMedia('(pointer: coarse)')` — the primary pointer is imprecise. */
|
|
105
|
+
readonly coarsePointer: boolean;
|
|
106
|
+
/** `matchMedia('(hover: hover)')` — the primary pointer can hover. */
|
|
107
|
+
readonly canHover: boolean;
|
|
108
|
+
/** Live device pixel ratio. */
|
|
109
|
+
readonly dpr: number;
|
|
110
|
+
/** Multiplier for handle sizes and hit radii. Derived from `coarsePointer`
|
|
111
|
+
* unless explicitly overridden. */
|
|
112
|
+
readonly targetScale: number;
|
|
113
|
+
}
|
|
114
|
+
/** Which kind of handle a recorded hit region belongs to. */
|
|
115
|
+
type HandleKind = 'corner' | 'rotation' | 'anchor';
|
|
116
|
+
/** The geometry a hit region actually tests against, as reported to the debug
|
|
117
|
+
* sink so the overlay can draw the real shape rather than its bounding box. */
|
|
118
|
+
type HitShape = {
|
|
119
|
+
kind: 'rect';
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
rotation?: number;
|
|
125
|
+
} | {
|
|
126
|
+
kind: 'circle';
|
|
127
|
+
cx: number;
|
|
128
|
+
cy: number;
|
|
129
|
+
r: number;
|
|
130
|
+
} | {
|
|
131
|
+
kind: 'path';
|
|
132
|
+
d: Path2D;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Where a tool reports its internal hit math so the debug overlay can draw it.
|
|
136
|
+
* Routing threads a sink through `ToolCtx` and never calls one.
|
|
137
|
+
*/
|
|
138
|
+
interface DebugSink {
|
|
139
|
+
recordHitbox(id: string, kind: 'body' | 'handle' | 'rotation' | 'anchor', shape: HitShape): void;
|
|
140
|
+
recordHandle(id: string, position: {
|
|
141
|
+
x: number;
|
|
142
|
+
y: number;
|
|
143
|
+
}, kind: HandleKind): void;
|
|
144
|
+
recordBounds(id: string, bounds: {
|
|
145
|
+
x: number;
|
|
146
|
+
y: number;
|
|
147
|
+
width: number;
|
|
148
|
+
height: number;
|
|
149
|
+
}): void;
|
|
150
|
+
recordOrigin(id: string, point: {
|
|
151
|
+
x: number;
|
|
152
|
+
y: number;
|
|
153
|
+
}): void;
|
|
154
|
+
recordSnapCandidate(point: {
|
|
155
|
+
x: number;
|
|
156
|
+
y: number;
|
|
157
|
+
}, accepted: boolean): void;
|
|
158
|
+
recordLayer(id: string, label: string, space: 'world' | 'screen', index: number): void;
|
|
159
|
+
/** Clears every non-snap array. Called at the start of each Canvas render. */
|
|
160
|
+
beginFrame(): void;
|
|
161
|
+
/** Clears the snap array. Called at gesture end. */
|
|
162
|
+
clearSnap(): void;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Snapshot of modifier-key state at dispatch. Distinct from
|
|
166
|
+
* `EventModifiers` in `@weasel-js/gestures`, which mirrors the DOM's
|
|
167
|
+
* `altKey`/`ctrlKey` spelling; this is the kit-facing one.
|
|
168
|
+
*/
|
|
169
|
+
interface ModifierState {
|
|
170
|
+
alt: boolean;
|
|
171
|
+
shift: boolean;
|
|
172
|
+
meta: boolean;
|
|
173
|
+
ctrl: boolean;
|
|
174
|
+
}
|
|
175
|
+
/** Which edges a resize gesture pins. `free` means the axis is unconstrained. */
|
|
176
|
+
type ResizeAnchor = {
|
|
177
|
+
x: 'min' | 'max' | 'free';
|
|
178
|
+
y: 'min' | 'max' | 'free';
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Live state read by rule evaluation. Built once per frame on the consuming
|
|
183
|
+
* surface — chrome-caps, the affordance pipeline, the dispatcher's
|
|
184
|
+
* eligibility filter — and discarded.
|
|
185
|
+
*
|
|
186
|
+
* Adding a new field is additive: existing rules don't change, new
|
|
187
|
+
* selector atoms can read it.
|
|
188
|
+
*/
|
|
189
|
+
interface RuleCtx {
|
|
190
|
+
readonly focused: boolean;
|
|
191
|
+
readonly selection: readonly NodeId[];
|
|
192
|
+
readonly multiActive: boolean;
|
|
193
|
+
readonly modifiers: ModifierState;
|
|
194
|
+
readonly action: {
|
|
195
|
+
readonly kind: string | null;
|
|
196
|
+
readonly id: string | null;
|
|
197
|
+
};
|
|
198
|
+
readonly hover: NodeId | null;
|
|
199
|
+
/**
|
|
200
|
+
* How far the viewport is zoomed in, as one number — `1` at native scale.
|
|
201
|
+
*
|
|
202
|
+
* A scalar rather than the `View` this used to carry, because `zoomAtLeast`
|
|
203
|
+
* is the only selector that ever read one and a number is all it needs. A
|
|
204
|
+
* host whose viewport is a camera has no `View` to hand over and would
|
|
205
|
+
* otherwise have to invent one to use eligibility at all; it omits this
|
|
206
|
+
* instead, and `zoomAtLeast` declines rather than matching on a fiction.
|
|
207
|
+
*/
|
|
208
|
+
readonly zoom?: number;
|
|
209
|
+
/** Active mode id. `'normal'` when no non-default mode is engaged. */
|
|
210
|
+
readonly mode: string;
|
|
211
|
+
/** Capability tags allowed by the active mode (the union of
|
|
212
|
+
* `ModeDefinition.allows` plus implicit tags). The `capability:`
|
|
213
|
+
* selector reads this to determine whether a tag is permitted. */
|
|
214
|
+
readonly allowedCapabilities: ReadonlySet<CapabilityTag>;
|
|
215
|
+
/** Whether the current selection may be resized. `<SceneCanvas>` folds
|
|
216
|
+
* `selectTool.resize.resizable` over the selection (true only when every
|
|
217
|
+
* selected node is resizable). Read by the `resizable:` selector to gate
|
|
218
|
+
* `selection.resize-handles`. Absent (legacy ctx builders) is treated as
|
|
219
|
+
* resizable — back-compat: handles show unless a consumer opts a node out. */
|
|
220
|
+
readonly selectionResizable?: boolean;
|
|
221
|
+
/** Whether a path is currently in anchor-edit mode. Read by the
|
|
222
|
+
* `editingAnchors:` selector, which gates the path-edit chrome.
|
|
223
|
+
*
|
|
224
|
+
* This is deliberately a fact about state, not about permission: the
|
|
225
|
+
* anchor overlay and the anchor hit-test must agree, and the thing they
|
|
226
|
+
* must agree on is "is there an edited path right now", which no
|
|
227
|
+
* capability or mode id answers. A mode that allows `edits-anchors`
|
|
228
|
+
* with nothing being edited should draw no anchors. Absent is treated
|
|
229
|
+
* as false. */
|
|
230
|
+
readonly editingAnchors?: boolean;
|
|
231
|
+
/** Device facts — pointer coarseness, hover capability, density.
|
|
232
|
+
*
|
|
233
|
+
* Absent (legacy ctx builders) is treated as
|
|
234
|
+
* {@link DEFAULT_DEVICE_PROFILE}: a fine pointer that can hover, at
|
|
235
|
+
* density 1. That is what the kit assumed before this field existed, so
|
|
236
|
+
* an absent profile is behavior-preserving by construction. */
|
|
237
|
+
readonly device?: DeviceProfile;
|
|
238
|
+
}
|
|
239
|
+
/** The live state a `RuleCtx` is assembled from — the chrome context plus
|
|
240
|
+
* what the active mode allows. */
|
|
241
|
+
interface BuildRuleCtxArgs {
|
|
242
|
+
focused: boolean;
|
|
243
|
+
selection: readonly NodeId[];
|
|
244
|
+
multiActive: boolean;
|
|
245
|
+
modifiers: ModifierState;
|
|
246
|
+
action: {
|
|
247
|
+
kind: string | null;
|
|
248
|
+
id: string | null;
|
|
249
|
+
};
|
|
250
|
+
hover: NodeId | null;
|
|
251
|
+
/** See {@link RuleCtx.zoom}. */
|
|
252
|
+
zoom?: number;
|
|
253
|
+
mode: string;
|
|
254
|
+
allowedCapabilities: ReadonlySet<CapabilityTag>;
|
|
255
|
+
/** Optional — omitted means "resizable" (handles show). See {@link RuleCtx}. */
|
|
256
|
+
selectionResizable?: boolean;
|
|
257
|
+
/** Optional — omitted means "no path is being anchor-edited". */
|
|
258
|
+
editingAnchors?: boolean;
|
|
259
|
+
/** Optional — omitted means {@link DEFAULT_DEVICE_PROFILE}. */
|
|
260
|
+
device?: DeviceProfile;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Capability set to assume when no mode registry is wired.
|
|
264
|
+
*
|
|
265
|
+
* "No modes configured" means *everything the default mode permits*, not
|
|
266
|
+
* *nothing permitted*. The empty set is the wrong default: it makes every
|
|
267
|
+
* `capability:` rule evaluate false, so a kit default written the
|
|
268
|
+
* recommended way (capability rules outlive new modes; mode rules have to
|
|
269
|
+
* be edited) would silently hide its chrome for every consumer that hasn't
|
|
270
|
+
* opted into modality. `NORMAL.allows` plus the implicit tags is what a
|
|
271
|
+
* mode-less canvas actually behaves like.
|
|
272
|
+
*/
|
|
273
|
+
declare const DEFAULT_ALLOWED_CAPABILITIES: ReadonlySet<CapabilityTag>;
|
|
274
|
+
/** Gather the current canvas and mode state into the context that action
|
|
275
|
+
* eligibility and chrome-visibility rules are evaluated against. */
|
|
276
|
+
declare function buildRuleCtx(args: BuildRuleCtxArgs): RuleCtx;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A selector is a conjunction of key/value tests. Multiple keys at the same
|
|
280
|
+
* level AND together. Each key maps to a selector primitive in the evaluator.
|
|
281
|
+
*/
|
|
282
|
+
interface Selector {
|
|
283
|
+
selection?: {
|
|
284
|
+
is?: number;
|
|
285
|
+
atLeast?: number;
|
|
286
|
+
empty?: boolean;
|
|
287
|
+
};
|
|
288
|
+
mode?: string | {
|
|
289
|
+
not: string;
|
|
290
|
+
} | {
|
|
291
|
+
in: readonly string[];
|
|
292
|
+
};
|
|
293
|
+
capability?: CapabilityTag | readonly CapabilityTag[] | {
|
|
294
|
+
in: readonly CapabilityTag[];
|
|
295
|
+
} | {
|
|
296
|
+
not: CapabilityTag;
|
|
297
|
+
};
|
|
298
|
+
gesturing?: boolean;
|
|
299
|
+
actionIs?: string;
|
|
300
|
+
modifierHeld?: keyof ModifierState;
|
|
301
|
+
focused?: boolean;
|
|
302
|
+
hovering?: boolean;
|
|
303
|
+
hoveringSelected?: boolean;
|
|
304
|
+
zoomAtLeast?: number;
|
|
305
|
+
/** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
|
|
306
|
+
* mode. Absent flag is treated as `false`. */
|
|
307
|
+
editingAnchors?: boolean;
|
|
308
|
+
/** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
|
|
309
|
+
* (resizable), so `{ resizable: true }` passes for legacy ctx builders
|
|
310
|
+
* that don't compute it. */
|
|
311
|
+
resizable?: boolean;
|
|
312
|
+
/** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
|
|
313
|
+
* (touch, most styluses). Absent device is treated as `false`. */
|
|
314
|
+
coarsePointer?: boolean;
|
|
315
|
+
/** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
|
|
316
|
+
* device is treated as `true`. */
|
|
317
|
+
canHover?: boolean;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
|
|
321
|
+
* over `Selector` leaves. `when` is the escape hatch — its closure is
|
|
322
|
+
* opaque to introspection and should be avoided when a declarative form
|
|
323
|
+
* exists. Empty `all` is true; empty `any` is false.
|
|
324
|
+
*/
|
|
325
|
+
type Rule = Selector | {
|
|
326
|
+
all: readonly Rule[];
|
|
327
|
+
} | {
|
|
328
|
+
any: readonly Rule[];
|
|
329
|
+
} | {
|
|
330
|
+
not: Rule;
|
|
331
|
+
} | {
|
|
332
|
+
when: (ctx: RuleCtx) => boolean;
|
|
333
|
+
};
|
|
334
|
+
/** Constant rules. Kept here so they have a single source. */
|
|
335
|
+
declare const ALWAYS: Rule;
|
|
336
|
+
/** A rule that never passes. `any` of nothing is false. */
|
|
337
|
+
declare const NEVER: Rule;
|
|
338
|
+
/**
|
|
339
|
+
* Render a {@link Rule} as a short human-readable string, for inspectors and
|
|
340
|
+
* diagnostics that need to say WHICH rule decided something.
|
|
341
|
+
*
|
|
342
|
+
* ```
|
|
343
|
+
* { capability: 'edits-page' } → capability:edits-page
|
|
344
|
+
* { all: [{ mode: 'normal' }, { focused: true }] } → all(mode:normal, focused:true)
|
|
345
|
+
* { not: { selection: { empty: true } } } → not(selection:empty=true)
|
|
346
|
+
* { when: function hasPath() { … } } → when(hasPath)
|
|
347
|
+
* ```
|
|
348
|
+
*
|
|
349
|
+
* Never throws and always terminates — unlike `JSON.stringify`, which throws
|
|
350
|
+
* on a cyclic value and renders the `when` arm as a useless `{}`.
|
|
351
|
+
*/
|
|
352
|
+
declare function describeRule(rule: Rule): string;
|
|
353
|
+
/** Evaluate a rule against a context. Pure, and cheap enough to run per
|
|
354
|
+
* frame for every piece of chrome. */
|
|
355
|
+
declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
|
|
356
|
+
/**
|
|
357
|
+
* Composable eligibility predicate with a fluent surface. Carries its
|
|
358
|
+
* underlying `Rule` tree at `.rule` so the dispatcher's eligibility filter and
|
|
359
|
+
* the chrome resolver can introspect and share trees.
|
|
360
|
+
*
|
|
361
|
+
* Callable form `cond(ctx)` evaluates the tree against ctx. The fluent methods
|
|
362
|
+
* return new Conditions wrapping new trees.
|
|
363
|
+
*
|
|
364
|
+
* **Chain semantics: strict left-to-right, no precedence.** `a.and(b).or(c)` is
|
|
365
|
+
* `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`. Mix `.and` and `.or`
|
|
366
|
+
* only when you mean left-to-right evaluation. For grouped disjunction, name
|
|
367
|
+
* the subexpression or use the top-level `or(...)`.
|
|
368
|
+
*/
|
|
369
|
+
interface Condition {
|
|
370
|
+
(ctx: RuleCtx): boolean;
|
|
371
|
+
readonly rule: Rule;
|
|
372
|
+
/** `this && other` */
|
|
373
|
+
and(other: Condition | Rule): Condition;
|
|
374
|
+
/** `this || other` */
|
|
375
|
+
or(other: Condition | Rule): Condition;
|
|
376
|
+
/** `this && !other` */
|
|
377
|
+
andNot(other: Condition | Rule): Condition;
|
|
378
|
+
/** `this || !other` */
|
|
379
|
+
orNot(other: Condition | Rule): Condition;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** The detected half of a profile — everything except the derived scale. */
|
|
383
|
+
type DetectedDeviceFacts = Omit<DeviceProfile, 'targetScale'>;
|
|
384
|
+
/**
|
|
385
|
+
* Handle/hit multiplier applied on a coarse pointer.
|
|
386
|
+
*
|
|
387
|
+
* 8px handle → 14px; 24px rotation distance → 42px. Counting the
|
|
388
|
+
* surrounding grab zone, that lands in the Apple HIG 44pt / Material 48dp
|
|
389
|
+
* minimum-touch-target band. One constant so it is tunable in one place.
|
|
390
|
+
*/
|
|
391
|
+
declare const COARSE_TARGET_SCALE = 1.75;
|
|
392
|
+
/**
|
|
393
|
+
* Fold consumer overrides over detected facts and derive `targetScale`.
|
|
394
|
+
*
|
|
395
|
+
* `targetScale` is re-derived AFTER the merge, so an override of
|
|
396
|
+
* `coarsePointer` alone scales the chrome as expected. An explicit
|
|
397
|
+
* `targetScale` override wins over the derivation.
|
|
398
|
+
*/
|
|
399
|
+
declare function resolveDeviceProfile(detected: DetectedDeviceFacts, overrides?: Partial<DeviceProfile>): DeviceProfile;
|
|
400
|
+
/**
|
|
401
|
+
* Assumed when `matchMedia` is unavailable (SSR, jsdom) and used as the
|
|
402
|
+
* absent-means value for `RuleCtx.device`.
|
|
403
|
+
*
|
|
404
|
+
* Derived from `resolveDeviceProfile` rather than hand-written, so it can
|
|
405
|
+
* never drift from what the function would compute for the same facts. A
|
|
406
|
+
* mouse-like device is the safe default: it is what the kit has always
|
|
407
|
+
* assumed, so an absent profile changes nothing for existing consumers.
|
|
408
|
+
*/
|
|
409
|
+
declare const DEFAULT_DEVICE_PROFILE: DeviceProfile;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Invoker — pluggable invocation strategy for an Action.
|
|
413
|
+
*
|
|
414
|
+
* An Action's `invoker` field describes how it's run. `immediate` invokers
|
|
415
|
+
* fire once (delete, align, undo). `ongoing` invokers open a phase machine
|
|
416
|
+
* driven by the dispatcher (move, resize, pinch-zoom).
|
|
417
|
+
*
|
|
418
|
+
* See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`
|
|
419
|
+
* § "Types" for the full design.
|
|
420
|
+
*/
|
|
421
|
+
|
|
422
|
+
/** A 2D point in either world or screen coordinates. */
|
|
423
|
+
interface Point2 {
|
|
424
|
+
x: number;
|
|
425
|
+
y: number;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Information about which UI affordance was hit at pointerdown.
|
|
429
|
+
*
|
|
430
|
+
* Populated by the dispatcher when the `affordanceAt` thunk is provided to
|
|
431
|
+
* `useGestureDispatcher`. Tools / action invokers that only fire on a specific
|
|
432
|
+
* affordance (e.g. a resize handle) use this field as a guard — if the
|
|
433
|
+
* affordance is absent or is the wrong kind, they return `{}` and let other
|
|
434
|
+
* bindings handle the drag.
|
|
435
|
+
*
|
|
436
|
+
* `kind` is a discriminator string:
|
|
437
|
+
* - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
|
|
438
|
+
* `'handle:bottom-right'` — corner resize handles.
|
|
439
|
+
* - `'rotate-handle'` — the rotation affordance.
|
|
440
|
+
* - `'anchor:N'` — a path anchor at index N.
|
|
441
|
+
*
|
|
442
|
+
* `fixedPoint` is the world-space point that should remain stationary during
|
|
443
|
+
* the gesture. For resize handles this is the opposite (diagonally fixed)
|
|
444
|
+
* corner; for rotate it is the pivot.
|
|
445
|
+
*
|
|
446
|
+
* `targetIds` are the node ids this affordance belongs to.
|
|
447
|
+
*/
|
|
448
|
+
interface AffordanceHit {
|
|
449
|
+
/** Discriminator string, e.g. `'handle:bottom-right'`. */
|
|
450
|
+
kind: string;
|
|
451
|
+
/** Id of whatever produced this hit — a kit affordance's `id`, or the
|
|
452
|
+
* registered layer's id. Read only by the dispatcher's dead-claim warning today. */
|
|
453
|
+
owner?: string;
|
|
454
|
+
/** `'exclusive'` means no binding may act on this point unless its target
|
|
455
|
+
* consults the affordance. `'shared'` (the default) competes on scope and
|
|
456
|
+
* specificity as bindings always have. */
|
|
457
|
+
strength?: 'exclusive' | 'shared';
|
|
458
|
+
/** Which gestures an exclusive claim bars. Omitted bars all of them. */
|
|
459
|
+
claimedKinds?: readonly ClaimableGesture[];
|
|
460
|
+
/** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
|
|
461
|
+
fixedPoint?: {
|
|
462
|
+
x: number;
|
|
463
|
+
y: number;
|
|
464
|
+
};
|
|
465
|
+
/** Which nodes this affordance belongs to. */
|
|
466
|
+
targetIds?: string[];
|
|
467
|
+
/** Set when `kind` matches `'handle:*'`. Identifies which corner stays
|
|
468
|
+
* fixed during a resize so consumers (resizeAction) don't re-parse `kind`.
|
|
469
|
+
* Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
|
|
470
|
+
* leave this undefined. */
|
|
471
|
+
anchor?: ResizeAnchor;
|
|
472
|
+
/** Cursor to show while the pointer hovers this affordance (no
|
|
473
|
+
* gesture in flight). Consumed by the hover-cursor pump in
|
|
474
|
+
* `useGestureDispatcher`; unset = the pump falls through to
|
|
475
|
+
* action-cursor prediction, then to the active tool's cursor. */
|
|
476
|
+
cursor?: CursorSpec;
|
|
477
|
+
/**
|
|
478
|
+
* Free-form payload from whatever produced the hit, carried through to the
|
|
479
|
+
* matching action untouched.
|
|
480
|
+
*
|
|
481
|
+
* Kit affordances describe themselves fully in the fields above and leave
|
|
482
|
+
* this unset. It exists for affordances the kit doesn't know the shape of —
|
|
483
|
+
* a registered layer's own chrome, where the hit-test already resolved
|
|
484
|
+
* *which* of its pieces was hit and the action would otherwise have to
|
|
485
|
+
* redo that work. `@weasel-js/hud` passes the hit widget here.
|
|
486
|
+
*/
|
|
487
|
+
payload?: unknown;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* One accumulated point on a drag trail: world-space position plus whatever
|
|
491
|
+
* stylus state the originating `PointerEvent` carried.
|
|
492
|
+
*
|
|
493
|
+
* The stylus fields are absent for mouse/touch on browsers that don't report
|
|
494
|
+
* them, and for synthetic events. Consumers that want pressure-driven output
|
|
495
|
+
* (e.g. `Stroke.vertexWidths` from a pencil stroke) read them off the samples
|
|
496
|
+
* their `insert` dep receives — see `apps/site/demos/VertexWidthsDemo.tsx`.
|
|
497
|
+
*/
|
|
498
|
+
interface DragSample extends Point2 {
|
|
499
|
+
/** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
|
|
500
|
+
pressure?: number;
|
|
501
|
+
/** Degrees, ±90. Zero for mouse/touch. */
|
|
502
|
+
tiltX?: number;
|
|
503
|
+
/** Degrees, ±90. Zero for mouse/touch. */
|
|
504
|
+
tiltY?: number;
|
|
505
|
+
}
|
|
506
|
+
/** Per-invocation runtime context the dispatcher hands to an Invoker.
|
|
507
|
+
* Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
|
|
508
|
+
* populated only for matching gesture kinds. */
|
|
509
|
+
interface InvocationCtx {
|
|
510
|
+
world: Point2;
|
|
511
|
+
/**
|
|
512
|
+
* The pointer in client/screen pixels, for an action that has to place DOM
|
|
513
|
+
* at the pointer. Absent on every invocation whose event carries no client
|
|
514
|
+
* coordinates — a keystroke, a UI-driven trigger, a synthetic probe — so
|
|
515
|
+
* read it with a fallback rather than assuming a point.
|
|
516
|
+
*
|
|
517
|
+
* A view-mutating drag wants `drag.screenDelta`, not the difference of two
|
|
518
|
+
* of these: world deltas are self-referential mid-pan, and so is any delta
|
|
519
|
+
* taken across a view change.
|
|
520
|
+
*/
|
|
521
|
+
screen?: Point2;
|
|
522
|
+
modifiers: ModifierState;
|
|
523
|
+
deps: ActionDeps;
|
|
524
|
+
drag?: {
|
|
525
|
+
start: Point2;
|
|
526
|
+
current: Point2;
|
|
527
|
+
delta: Point2;
|
|
528
|
+
/**
|
|
529
|
+
* Drag delta in client/screen coordinates (CSS pixels from the drag
|
|
530
|
+
* origin). Use this — never `delta` — for any action whose effect
|
|
531
|
+
* mutates the viewport itself (pan, view-zoom), because world-space
|
|
532
|
+
* deltas become self-referential as the view shifts mid-drag.
|
|
533
|
+
*
|
|
534
|
+
* Populated when the dispatcher received `clientX`/`clientY` on the
|
|
535
|
+
* underlying pointer events. Absent for legacy callers that don't
|
|
536
|
+
* provide them.
|
|
537
|
+
*/
|
|
538
|
+
screenDelta?: Point2;
|
|
539
|
+
affordance?: AffordanceHit;
|
|
540
|
+
/**
|
|
541
|
+
* Full pointermove history for the current drag, in world space, with
|
|
542
|
+
* per-sample stylus state when the browser reported it.
|
|
543
|
+
* Accumulated by the dispatcher on every `pointermove` pump event.
|
|
544
|
+
* Available only during `onMove` and `onEnd` calls (not on `start`).
|
|
545
|
+
* Used by `lassoSelectAction` to build its polygon vertex list and by
|
|
546
|
+
* `insertAction`'s pencil kind to carry the freehand stroke.
|
|
547
|
+
*/
|
|
548
|
+
points?: DragSample[];
|
|
549
|
+
};
|
|
550
|
+
wheel?: {
|
|
551
|
+
deltaX: number;
|
|
552
|
+
deltaY: number;
|
|
553
|
+
deltaZ: number;
|
|
554
|
+
};
|
|
555
|
+
multiTouch?: {
|
|
556
|
+
centroid: Point2;
|
|
557
|
+
spread: number;
|
|
558
|
+
rotation: number;
|
|
559
|
+
/**
|
|
560
|
+
* Pinch-zoom geometry. Populated by the dispatcher when a multitouch
|
|
561
|
+
* handle is in flight and a pointermove-pump fires.
|
|
562
|
+
* `startSpread` is the spread at the moment the gesture began.
|
|
563
|
+
* `currentSpread` is the spread at the current frame.
|
|
564
|
+
*/
|
|
565
|
+
pinch?: {
|
|
566
|
+
startSpread: number;
|
|
567
|
+
currentSpread: number;
|
|
568
|
+
centroid: Point2;
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
key?: {
|
|
572
|
+
key: string;
|
|
573
|
+
repeat: boolean;
|
|
574
|
+
};
|
|
575
|
+
/**
|
|
576
|
+
* Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
|
|
577
|
+
* UI-driven ongoing actions (color picker, opacity slider) so handles can
|
|
578
|
+
* read the current value on `start` and updated values on `onMove`. The
|
|
579
|
+
* gesture dispatcher does not populate this field; gesture-driven actions
|
|
580
|
+
* receive params via `BindingOpts.params` on `start` (the `opts` arg).
|
|
581
|
+
*/
|
|
582
|
+
params?: Record<string, unknown>;
|
|
583
|
+
}
|
|
584
|
+
/** Per-invocation options the dispatcher reads from a `GestureBinding`'s
|
|
585
|
+
* `opts` field and passes to `OngoingInvoker.start`. Today carries
|
|
586
|
+
* behaviors; extensible. */
|
|
587
|
+
interface BindingOpts {
|
|
588
|
+
/** Gesture behaviors the action's own invoker runs. Routing carries the
|
|
589
|
+
* array to the action and never reads an element, so the element type is
|
|
590
|
+
* open here; `@weasel-js/core` narrows it to `ActionBehavior<unknown,
|
|
591
|
+
* unknown, unknown>[]`, which is what a behavior is authored against. */
|
|
592
|
+
behaviors?: unknown[];
|
|
593
|
+
/** Per-binding action parameters. The action's invoker reads
|
|
594
|
+
* these via the second arg to `run` (or via InvocationCtx for ongoing
|
|
595
|
+
* invokers, when needed). Loose typing (Record<string, unknown>) for
|
|
596
|
+
* now; consider per-action typing later via BindingOpts<A>.
|
|
597
|
+
*
|
|
598
|
+
* params may also be a thunk evaluated each time the
|
|
599
|
+
* dispatcher (or invoker) needs the value. Thunks let tools close over
|
|
600
|
+
* refs that mutate during a gesture (e.g. polygon `sides` adjusted
|
|
601
|
+
* mid-drag via ArrowUp). For ongoing invokers that want the latest
|
|
602
|
+
* values at commit, the invoker can re-call the thunk inside `onEnd`
|
|
603
|
+
* via `resolveParams(opts?.params)`. */
|
|
604
|
+
params?: Record<string, unknown> | (() => Record<string, unknown>);
|
|
605
|
+
}
|
|
606
|
+
/** Resolve `BindingOpts.params` to a concrete record (calling the thunk if
|
|
607
|
+
* needed). Returns `undefined` for absent params. */
|
|
608
|
+
declare function resolveParams(params: BindingOpts['params']): Record<string, unknown> | undefined;
|
|
609
|
+
/** Convention-shaped action dependencies bag. Actions declare which
|
|
610
|
+
* contexts they consume; the dispatcher composes them per call.
|
|
611
|
+
* Consumer-side contexts (e.g. ColorContext) plug in by extending. */
|
|
612
|
+
interface ActionDeps {
|
|
613
|
+
selection?: unknown;
|
|
614
|
+
view?: unknown;
|
|
615
|
+
scene?: unknown;
|
|
616
|
+
pointer?: unknown;
|
|
617
|
+
activeTool?: unknown;
|
|
618
|
+
[k: string]: unknown;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* What a `polyline` overlay is, in a word — the narrowest thing an action can
|
|
622
|
+
* say about how its run should look without naming a paint.
|
|
623
|
+
*
|
|
624
|
+
* The named roles are the kit's; a painter maps each to a style and falls back
|
|
625
|
+
* to plain chrome for one it does not know, so a package outside core can coin
|
|
626
|
+
* its own and still be drawn.
|
|
627
|
+
*/
|
|
628
|
+
type OverlayRole =
|
|
629
|
+
/** Generic in-flight chrome — the marquee/lasso paint. */
|
|
630
|
+
'chrome'
|
|
631
|
+
/** A cut the gesture would make. */
|
|
632
|
+
| 'cut'
|
|
633
|
+
/** An edge the gesture would author between two points. */
|
|
634
|
+
| 'connector' | (string & {});
|
|
635
|
+
/**
|
|
636
|
+
* What an in-flight gesture proposes to paint *beside* the scene, returned by
|
|
637
|
+
* `OngoingHandle.overlay()` — a marquee rect, a lasso trail, the outline of a
|
|
638
|
+
* shape being dragged out, the line a slice would cut along.
|
|
639
|
+
*
|
|
640
|
+
* Every variant is world geometry and nothing else. No `DrawCommand`, no
|
|
641
|
+
* color, no coordinate space to choose: an action that reached for any of
|
|
642
|
+
* those would be publishing core's own 2D vocabulary, and a surface rendering
|
|
643
|
+
* the same gesture some other way could not read it. `resolveOverlays`
|
|
644
|
+
* normalizes this into `ResolvedOverlay`, which is what a painter consumes —
|
|
645
|
+
* `useDispatcherOverlayLayer` is core's, and it owns every style decision.
|
|
646
|
+
*/
|
|
647
|
+
type OngoingOverlay = {
|
|
648
|
+
kind: 'marquee';
|
|
649
|
+
start: {
|
|
650
|
+
x: number;
|
|
651
|
+
y: number;
|
|
652
|
+
};
|
|
653
|
+
current: {
|
|
654
|
+
x: number;
|
|
655
|
+
y: number;
|
|
656
|
+
};
|
|
657
|
+
shiftHeld: boolean;
|
|
658
|
+
} | {
|
|
659
|
+
kind: 'lasso';
|
|
660
|
+
vertices: ReadonlyArray<{
|
|
661
|
+
x: number;
|
|
662
|
+
y: number;
|
|
663
|
+
}>;
|
|
664
|
+
current: {
|
|
665
|
+
x: number;
|
|
666
|
+
y: number;
|
|
667
|
+
};
|
|
668
|
+
shiftHeld: boolean;
|
|
669
|
+
} | {
|
|
670
|
+
/**
|
|
671
|
+
* An open run of world points the gesture is proposing — the slice
|
|
672
|
+
* action's cut line, `@weasel-js/diagram`'s routed connector.
|
|
673
|
+
*
|
|
674
|
+
* Geometry only, like every other variant: `role` is the whole of what
|
|
675
|
+
* an action says about appearance, and the painting surface is what
|
|
676
|
+
* turns it into a stroke. An action that picked its own color would be
|
|
677
|
+
* unpaintable by any renderer but the one it had in mind.
|
|
678
|
+
*/
|
|
679
|
+
kind: 'polyline';
|
|
680
|
+
/** At least two points. Anything shorter is dropped in resolution. */
|
|
681
|
+
points: ReadonlyArray<{
|
|
682
|
+
x: number;
|
|
683
|
+
y: number;
|
|
684
|
+
}>;
|
|
685
|
+
/** What the run means. Omitted reads as {@link OverlayRole} `'chrome'`. */
|
|
686
|
+
role?: OverlayRole;
|
|
687
|
+
} | {
|
|
688
|
+
/**
|
|
689
|
+
* Live insert-drag preview — dispatched by `insertAction` while the
|
|
690
|
+
* user is dragging out a new shape. Pre-commit there is no scene node
|
|
691
|
+
* to ghost via `previewIds()`/`previewPose()`, so insert paints its
|
|
692
|
+
* preview through the dispatcher overlay layer instead.
|
|
693
|
+
*
|
|
694
|
+
* `shape` is the kit's built-in insert kind. `bounds` is the AABB of
|
|
695
|
+
* the current drag (start/current normalized). `extras` is the
|
|
696
|
+
* per-kind extras the action already collected — the overlay
|
|
697
|
+
* renderer rebuilds the shape using the same path builders the
|
|
698
|
+
* commit factory uses, so the preview matches the eventual node.
|
|
699
|
+
*
|
|
700
|
+
* `extras` is opaque (`unknown`) at the union level; the overlay
|
|
701
|
+
* renderer narrows on `shape` and casts the field shape it expects.
|
|
702
|
+
*/
|
|
703
|
+
kind: 'insertPreview';
|
|
704
|
+
/** Which preview the dispatcher's overlay draws. The kit's own shape
|
|
705
|
+
* kinds are the ones it knows how to render; a consumer-defined kind
|
|
706
|
+
* is legal and simply gets no live preview. */
|
|
707
|
+
shape: string;
|
|
708
|
+
bounds: {
|
|
709
|
+
x: number;
|
|
710
|
+
y: number;
|
|
711
|
+
width: number;
|
|
712
|
+
height: number;
|
|
713
|
+
};
|
|
714
|
+
extras: unknown;
|
|
715
|
+
/** World-space point to paint a small "anchor" dot at. Sells the
|
|
716
|
+
* click point as the drag's anchor — particularly useful for
|
|
717
|
+
* radial shapes (polygon/star) where no vertex sits on the
|
|
718
|
+
* click point, and for any shape in center mode where the dot
|
|
719
|
+
* marks the center the shape grows around. */
|
|
720
|
+
anchorPoint?: {
|
|
721
|
+
x: number;
|
|
722
|
+
y: number;
|
|
723
|
+
};
|
|
724
|
+
};
|
|
725
|
+
/** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
|
|
726
|
+
* `onMove` on subsequent input events of the same gesture and calls
|
|
727
|
+
* `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
|
|
728
|
+
* on pointercancel / blur / escape). */
|
|
729
|
+
interface OngoingHandle {
|
|
730
|
+
/**
|
|
731
|
+
* Optional logical action kind — a stable, human-readable tag the
|
|
732
|
+
* dispatcher exposes via `getActiveAction()` for chrome-visibility
|
|
733
|
+
* rules and any other surface that wants to react to "what action
|
|
734
|
+
* is currently in flight" without inspecting handles directly.
|
|
735
|
+
*
|
|
736
|
+
* Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
|
|
737
|
+
* `'pan'`, `'pinch'`.
|
|
738
|
+
*
|
|
739
|
+
* Distinct from the dispatcher's internal `gestureId` (`pointer-mouse`,
|
|
740
|
+
* `key-held-Space`, etc.) which keys per-pointer state and is not
|
|
741
|
+
* meaningful to consumers.
|
|
742
|
+
*
|
|
743
|
+
* When omitted, the action is "anonymous" — `getActiveAction().kind`
|
|
744
|
+
* reports `null` even though a handle is in flight. This is fine for
|
|
745
|
+
* actions that don't have visible chrome of their own.
|
|
746
|
+
*/
|
|
747
|
+
kind?: string;
|
|
748
|
+
onMove?(ctx: InvocationCtx): void;
|
|
749
|
+
onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
|
|
750
|
+
/**
|
|
751
|
+
* Optional preview surface — dispatcher-side ghost overlay.
|
|
752
|
+
*
|
|
753
|
+
* An ongoing-action implementation may populate `previewIds()` +
|
|
754
|
+
* `previewPose(id)` to expose its in-flight preview state for the
|
|
755
|
+
* canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
|
|
756
|
+
* top of the committed scene during the gesture.
|
|
757
|
+
*
|
|
758
|
+
* Returning `null` (or omitting the method entirely) means "no preview
|
|
759
|
+
* this gesture" — the canvas will skip this handle as a source.
|
|
760
|
+
*
|
|
761
|
+
* Semantics mirror the tool-side `Tool.previewIds` / `Tool.previewPose`
|
|
762
|
+
* pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
|
|
763
|
+
* returns the interim pose for one of those ids (shape opaque — the
|
|
764
|
+
* canvas casts to its `TPose` parameter). The preview-ghost layer
|
|
765
|
+
* merges all sources via first-non-null semantics, with tool-side
|
|
766
|
+
* previews taking precedence over dispatcher-side (preserves
|
|
767
|
+
* backwards-compat during the registry-unification migration).
|
|
768
|
+
*/
|
|
769
|
+
previewIds?(): Iterable<string> | null;
|
|
770
|
+
previewPose?(id: string): unknown | null;
|
|
771
|
+
/**
|
|
772
|
+
* Subset of `previewIds()` the ghost layer paints at full opacity. The
|
|
773
|
+
* ghost alpha says "this is in flight under the pointer"; a node the
|
|
774
|
+
* gesture merely displaces — a layout sibling reflowing into its
|
|
775
|
+
* destination slot — is not, and reads better settled. Honored at
|
|
776
|
+
* subtree-root granularity.
|
|
777
|
+
*/
|
|
778
|
+
previewOpaqueIds?(): Iterable<string> | null;
|
|
779
|
+
/**
|
|
780
|
+
* When `false`, the preview-ghost layer paints the ghost AND the
|
|
781
|
+
* source node stays visible at its committed pose. Defaults to
|
|
782
|
+
* `true` (move/resize/rotate semantics: ghost replaces the source
|
|
783
|
+
* during the gesture). Clone overrides to `false` so the original
|
|
784
|
+
* stays put and the ghost appears at the drag target.
|
|
785
|
+
*/
|
|
786
|
+
previewHidesSource?: boolean;
|
|
787
|
+
/**
|
|
788
|
+
* Optional per-id preview *data*. Falls back to the committed
|
|
789
|
+
* `node.data` when null/absent. Use when the gesture mutates
|
|
790
|
+
* `node.data` (e.g. anchor-edit on nodes that store the polygon on
|
|
791
|
+
* `data.path`) rather than (or in addition to) the pose. The preview-
|
|
792
|
+
* ghost layer assembles a synthetic node from `{ ...node, pose:
|
|
793
|
+
* previewPose ?? node.pose, data: previewData ?? node.data }` before
|
|
794
|
+
* calling the scene slot's `drawOne`.
|
|
795
|
+
*
|
|
796
|
+
* Sources compose first-non-null per axis: an action can emit only
|
|
797
|
+
* `previewPose` (translation), only `previewData` (data-only edit),
|
|
798
|
+
* or both (pose + data both change, e.g. anchor drag on a data.path
|
|
799
|
+
* node where the bounds shift).
|
|
800
|
+
*/
|
|
801
|
+
previewData?(id: string): unknown | null;
|
|
802
|
+
/**
|
|
803
|
+
* Optional chrome surface — dispatcher-side overlay layer.
|
|
804
|
+
*
|
|
805
|
+
* An ongoing-action implementation may populate `overlay()` to expose a
|
|
806
|
+
* non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
|
|
807
|
+
* `useDispatcherOverlayLayer` to paint while the gesture is in flight.
|
|
808
|
+
* Returning `null` (or omitting the method) means "no overlay this
|
|
809
|
+
* gesture" — the canvas will skip this handle as a chrome source.
|
|
810
|
+
*
|
|
811
|
+
* Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
|
|
812
|
+
* which paints displaced scene-node silhouettes. Marquee and lasso
|
|
813
|
+
* gestures don't displace any node, but still need on-screen feedback.
|
|
814
|
+
*/
|
|
815
|
+
overlay?(): OngoingOverlay | null;
|
|
816
|
+
}
|
|
817
|
+
/** Fire-once invocation. Runs to completion synchronously (or fires off an
|
|
818
|
+
* async side-effect; the registry doesn't wait). */
|
|
819
|
+
interface ImmediateInvoker {
|
|
820
|
+
timing: 'immediate';
|
|
821
|
+
/** `params` carries the matched binding's opts.params. Invoked from the
|
|
822
|
+
* command palette, or anywhere else with no per-binding context, `params`
|
|
823
|
+
* is undefined; descriptors should default to a sensible variant. */
|
|
824
|
+
run(deps: ActionDeps, params?: Record<string, unknown>): void;
|
|
825
|
+
}
|
|
826
|
+
/** Phase-machine invocation. `start` opens the phase and returns the handle
|
|
827
|
+
* the dispatcher pumps. */
|
|
828
|
+
interface OngoingInvoker {
|
|
829
|
+
timing: 'ongoing';
|
|
830
|
+
start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
|
|
831
|
+
}
|
|
832
|
+
/** Pluggable invocation strategy for an Action. Future variants
|
|
833
|
+
* (`longPress`, `twoStage`, `modal`) extend this union without touching
|
|
834
|
+
* the `Action` type. */
|
|
835
|
+
type Invoker = ImmediateInvoker | OngoingInvoker;
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* GestureBinding — connects a GestureSpec to an Action id (with per-binding
|
|
839
|
+
* options). Tools own arrays of these on their `bindings` field; ambient
|
|
840
|
+
* gesture-bindings are registered globally.
|
|
841
|
+
*
|
|
842
|
+
* See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
|
|
843
|
+
*/
|
|
844
|
+
|
|
845
|
+
/** An interaction: a gesture spec composed with the id of the action it
|
|
846
|
+
* invokes. Tools declare arrays of these; the dispatcher matches an incoming
|
|
847
|
+
* input event against them and runs the winner's action. */
|
|
848
|
+
interface GestureBinding {
|
|
849
|
+
spec: GestureSpec;
|
|
850
|
+
actionId: string;
|
|
851
|
+
opts?: BindingOpts;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* @experimental
|
|
855
|
+
* A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
|
|
856
|
+
* (no per-binding opts) or an object form that pairs a spec with
|
|
857
|
+
* `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
|
|
858
|
+
* Use the object form when two bindings for the same action differ only in
|
|
859
|
+
* a runtime parameter — the dispatcher extracts `opts.params` and passes
|
|
860
|
+
* them to `ImmediateInvoker.run` as its second argument.
|
|
861
|
+
*/
|
|
862
|
+
type BoundGesture = GestureSpec | {
|
|
863
|
+
spec: GestureSpec;
|
|
864
|
+
opts: BindingOpts;
|
|
865
|
+
};
|
|
866
|
+
/**
|
|
867
|
+
* @experimental
|
|
868
|
+
* The whole of an action `actionBindings` reads. Routing needs this much and
|
|
869
|
+
* none of `Action`'s palette fields, so it is the narrowest type the
|
|
870
|
+
* dispatcher's binding walk can be written against.
|
|
871
|
+
*/
|
|
872
|
+
interface BindingSource {
|
|
873
|
+
id: string;
|
|
874
|
+
/** The gesture-spec form of the binding, read by the gesture dispatcher.
|
|
875
|
+
* May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
|
|
876
|
+
* or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
|
|
877
|
+
* `{ spec, opts }` — use the object form for parametric actions where two
|
|
878
|
+
* bindings for the same action differ only by `opts.params` (e.g. `flip`
|
|
879
|
+
* with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
|
|
880
|
+
* passes them to `ImmediateInvoker.run` as its second argument. */
|
|
881
|
+
defaultBinding?: GestureSpec | BoundGesture[];
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* @experimental
|
|
885
|
+
* Flatten an action's `defaultBinding` into `GestureBinding`s. A bare
|
|
886
|
+
* `GestureSpec` has `kind` at top level; the object form has `spec`.
|
|
887
|
+
*/
|
|
888
|
+
declare function actionBindings(action: BindingSource): GestureBinding[];
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* @experimental
|
|
892
|
+
* Closed enum of reasons an action might report itself as disabled. The
|
|
893
|
+
* consumer (palette, menu, etc.) maps these symbolic values to display
|
|
894
|
+
* strings via its own label map.
|
|
895
|
+
*/
|
|
896
|
+
declare const ActionDisabledReason: {
|
|
897
|
+
readonly SelectionRequired: "selection-required";
|
|
898
|
+
readonly SceneEmpty: "scene-empty";
|
|
899
|
+
readonly NotApplicable: "not-applicable";
|
|
900
|
+
/** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
|
|
901
|
+
readonly PredicateThrew: "predicate-threw";
|
|
902
|
+
};
|
|
903
|
+
/** Why an action is unavailable right now. */
|
|
904
|
+
type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDisabledReason];
|
|
905
|
+
/**
|
|
906
|
+
* @experimental
|
|
907
|
+
* What a palette, menu or toolbar renders for an action. No part of this is
|
|
908
|
+
* read during dispatch.
|
|
909
|
+
*/
|
|
910
|
+
interface ActionPresentation {
|
|
911
|
+
label: string;
|
|
912
|
+
/** Inline-SVG icon for palette / toolbar surfaces. Mirrors
|
|
913
|
+
* `ToolPresentation.icon` so a generic `<ActionBar>` can render from
|
|
914
|
+
* action metadata the same way `<ToolPalette>` renders from tool
|
|
915
|
+
* metadata. May be a static `ReactNode` or a function (rare; useful
|
|
916
|
+
* for state-aware icons like a "lock" toggle). */
|
|
917
|
+
icon?: ReactNode | (() => ReactNode);
|
|
918
|
+
/** Grouping key for palette/menu surfaces. Free-form string; the kit
|
|
919
|
+
* ships defaults for `'align'` (six edges/centers), `'distribute'`
|
|
920
|
+
* (two axes), and recommends `'pathfinder'` for boolean ops. */
|
|
921
|
+
group?: string;
|
|
922
|
+
/** Display override for the keyboard shortcut. When omitted, palette
|
|
923
|
+
* surfaces derive a label from `defaultBinding` via their own
|
|
924
|
+
* formatter. */
|
|
925
|
+
shortcut?: string;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* @experimental
|
|
929
|
+
* What routing consults: the bindings that reach the action, the deps its
|
|
930
|
+
* invoker reads, the gates it passes, and the cursors it claims.
|
|
931
|
+
*/
|
|
932
|
+
interface ActionDispatch extends BindingSource {
|
|
933
|
+
/** Names of the deps this action's invoker reads (keys of `DepSchema`).
|
|
934
|
+
* The dispatcher (and `trigger`, when `requires` is present) resolves
|
|
935
|
+
* each name against the `DepRegistry` at invocation time and passes the
|
|
936
|
+
* resulting bag to the invoker. Dev builds warn when the invoker reads a
|
|
937
|
+
* dep it didn't declare here — see `buildDepsFromRequires`. */
|
|
938
|
+
requires?: readonly DepName[];
|
|
939
|
+
/** Pluggable invocation strategy. The gesture dispatcher routes matched
|
|
940
|
+
* bindings through `invoker.start` / `invoker.run` depending on timing.
|
|
941
|
+
* All kit-standard descriptors ship one; consumer-supplied actions
|
|
942
|
+
* without an invoker can still register but won't be triggered. */
|
|
943
|
+
invoker?: Invoker;
|
|
944
|
+
/** When set to `'hotkey'`, this action's `defaultBinding` rides the hotkey
|
|
945
|
+
* `BindingScope` instead of the ambient scope — meaning it beats any
|
|
946
|
+
* active-tool binding on the same input shape. Use for tool-switch
|
|
947
|
+
* shortcuts and global held-key triggers. Default: ambient. */
|
|
948
|
+
scope?: 'hotkey';
|
|
949
|
+
/**
|
|
950
|
+
* @experimental
|
|
951
|
+
* Optional predicate the command palette consults when rendering. Return
|
|
952
|
+
* `true` when the action is currently triggerable. Return a reason string
|
|
953
|
+
* (e.g. `'Selection required'`) when disabled — the palette greys out
|
|
954
|
+
* the row, skips it in keyboard nav, ignores clicks, and shows the
|
|
955
|
+
* reason next to the label. Keystroke dispatch (the registered binding)
|
|
956
|
+
* is unaffected; the action's own `run` should self-guard.
|
|
957
|
+
*
|
|
958
|
+
* **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
|
|
959
|
+
* must not throw. If a call throws or exceeds the budget in dev mode,
|
|
960
|
+
* `evaluateEnabled` logs a one-time warning per action id; throws are
|
|
961
|
+
* caught and treated as disabled with reason `'(predicate threw)'`.
|
|
962
|
+
*
|
|
963
|
+
* Snapshot-on-open semantics: the palette evaluates `enabled` once when
|
|
964
|
+
* opened and does NOT re-evaluate on selection changes while open. Live
|
|
965
|
+
* reactive updates are deferred — palette is short-lived.
|
|
966
|
+
*
|
|
967
|
+
* The reason set is a closed enum — to add a new reason, edit
|
|
968
|
+
* `ActionDisabledReason` and the consumer's display map.
|
|
969
|
+
*
|
|
970
|
+
* The optional `deps` argument is the same bag passed to
|
|
971
|
+
* `ImmediateInvoker.run`; callers (`evaluateEnabled` / the ActionBar) may
|
|
972
|
+
* synthesize it from the surrounding `DepRegistry` so predicates can
|
|
973
|
+
* inspect selection / scene / etc. Predicates that don't need deps just
|
|
974
|
+
* ignore the arg.
|
|
975
|
+
*/
|
|
976
|
+
enabled?: (deps?: ActionDeps) => true | ActionDisabledReason;
|
|
977
|
+
/**
|
|
978
|
+
* Declarative eligibility rule, evaluated against the current
|
|
979
|
+
* `RuleCtx` by the dispatcher before invoking `start()`. Omitted =
|
|
980
|
+
* always eligible.
|
|
981
|
+
*
|
|
982
|
+
* Accepts either a fluent `Condition` (callable with `.rule`) or a
|
|
983
|
+
* raw `Rule` tree; the dispatcher normalizes via `.rule` unwrap.
|
|
984
|
+
*
|
|
985
|
+
* Prefer `capability:`-based rules (e.g. `{ capability: 'transforms-selection' }`)
|
|
986
|
+
* over `mode:` rules — capability rules survive new modes being added
|
|
987
|
+
* that allow the same capability.
|
|
988
|
+
*/
|
|
989
|
+
eligible?: Rule | Condition;
|
|
990
|
+
/**
|
|
991
|
+
* CSS cursor shown while the pointer hovers a spot where this action
|
|
992
|
+
* would win the drag. The hover-cursor pump (in `useGestureDispatcher`)
|
|
993
|
+
* runs `Dispatcher.resolveOnly` on each idle pointermove — the same
|
|
994
|
+
* match walk a real pointerdown takes — and applies the winning
|
|
995
|
+
* action's `cursor`, so the hint and the actual click target stay in
|
|
996
|
+
* sync by construction. Omitted = no override (the active tool's
|
|
997
|
+
* `Tool.cursor` shows). Affordance hits are resolved earlier in the
|
|
998
|
+
* pump via `AffordanceRegion.cursor` and never reach this field.
|
|
999
|
+
*
|
|
1000
|
+
* Static value only. Prediction runs `enabled()` but cannot run the
|
|
1001
|
+
* invoker, so an action that matches yet bails at `start()` (empty
|
|
1002
|
+
* handle) may still show its cursor — keep `enabled` accurate for
|
|
1003
|
+
* actions that declare one.
|
|
1004
|
+
*/
|
|
1005
|
+
cursor?: CursorSpec;
|
|
1006
|
+
/**
|
|
1007
|
+
* CSS cursor shown while THIS action's ongoing handle is in flight —
|
|
1008
|
+
* grabbing while panning, `move` while dragging a selection, `crosshair`
|
|
1009
|
+
* while pulling a marquee.
|
|
1010
|
+
*
|
|
1011
|
+
* Separate from `cursor` because the two answer different questions:
|
|
1012
|
+
* `cursor` is a prediction ("a drag from here would pan"), this is a state
|
|
1013
|
+
* ("you are panning"). An action can declare either, both, or neither;
|
|
1014
|
+
* with only `cursor` set, the hover hint holds for the duration of the
|
|
1015
|
+
* gesture.
|
|
1016
|
+
*
|
|
1017
|
+
* This is where mid-gesture cursors live now. They used to come from the
|
|
1018
|
+
* tool side — `ViewportToolDef.engaged.cursor` for a phase-gated string,
|
|
1019
|
+
* or a function-form `Tool.cursor` reading the gesture scratch out of the
|
|
1020
|
+
* tool-routing dispatcher. Both belonged to a pipeline whose whole job was
|
|
1021
|
+
* being taken over by bindings, and neither could describe a cursor for an
|
|
1022
|
+
* action a tool doesn't own.
|
|
1023
|
+
*/
|
|
1024
|
+
activeCursor?: CursorSpec;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* @experimental
|
|
1028
|
+
* Single registered action. v1: one binding per action.
|
|
1029
|
+
*/
|
|
1030
|
+
interface Action extends ActionDispatch, ActionPresentation {
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* @experimental
|
|
1034
|
+
* The whole of an actions registry the dispatcher reads. It walks the list
|
|
1035
|
+
* once per event to build its actionId lookup and never mutates it, so the
|
|
1036
|
+
* routing side depends on this rather than on `ActionsRegistry`, whose
|
|
1037
|
+
* register / mute / trigger surface is the React provider's.
|
|
1038
|
+
*/
|
|
1039
|
+
interface ActionSource {
|
|
1040
|
+
list(): readonly Action[];
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* @experimental
|
|
1045
|
+
* Palette-side evaluation of `Action.enabled`. The dispatcher calls the
|
|
1046
|
+
* predicate raw as a gate; this is the surface that also enforces the
|
|
1047
|
+
* contract the predicate's JSDoc states — no throwing, under 4ms in dev.
|
|
1048
|
+
*/
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* @experimental
|
|
1052
|
+
* Result of evaluating an Action's `enabled` predicate.
|
|
1053
|
+
*/
|
|
1054
|
+
interface ActionEnabledResult {
|
|
1055
|
+
enabled: boolean;
|
|
1056
|
+
reason?: ActionDisabledReason;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* @experimental
|
|
1060
|
+
* Safely evaluate an action's `enabled` predicate. Returns `{enabled: true}`
|
|
1061
|
+
* when no predicate is supplied. Catches throws and treats them as disabled
|
|
1062
|
+
* with reason `'(predicate threw)'`. In dev mode, warns once per action id
|
|
1063
|
+
* when a single call exceeds 4ms (single frame at 240fps — generous; real
|
|
1064
|
+
* predicates should be sub-millisecond).
|
|
1065
|
+
*/
|
|
1066
|
+
declare function evaluateEnabled(action: Action, deps?: ActionDeps): ActionEnabledResult;
|
|
1067
|
+
|
|
1068
|
+
/** Holds the live sources an action's declared dependencies resolve to.
|
|
1069
|
+
* Sources are thunks, read at invocation time, so an action never captures
|
|
1070
|
+
* stale state. */
|
|
1071
|
+
interface DepRegistry {
|
|
1072
|
+
register<K extends DepName>(name: K, source: () => DepSchema[K]): () => void;
|
|
1073
|
+
get<K extends DepName>(name: K): DepSchema[K] | undefined;
|
|
1074
|
+
}
|
|
1075
|
+
/** Provides the dep registry for a canvas. `<SceneCanvas>` mounts one; a
|
|
1076
|
+
* consumer registering its own dep sources must be inside it. */
|
|
1077
|
+
declare function DepRegistryProvider({ children }: {
|
|
1078
|
+
children: ReactNode;
|
|
1079
|
+
}): react_jsx_runtime.JSX.Element;
|
|
1080
|
+
/** The dep registry in scope. Throws outside a `<DepRegistryProvider>`. */
|
|
1081
|
+
declare function useDepRegistry(): DepRegistry;
|
|
1082
|
+
/**
|
|
1083
|
+
* Like `useDepRegistry`, but returns `null` when no `<DepRegistryProvider>` is
|
|
1084
|
+
* in scope instead of throwing. Used by `useStandardActions` to preserve its
|
|
1085
|
+
* silent-no-op contract when neither provider is present.
|
|
1086
|
+
*/
|
|
1087
|
+
declare function useOptionalDepRegistry(): DepRegistry | null;
|
|
1088
|
+
/** Register a live source for `name` for the lifetime of the calling
|
|
1089
|
+
* component. The `source` thunk is called at dispatch time and should
|
|
1090
|
+
* return the latest value. */
|
|
1091
|
+
declare function useDepSource<K extends DepName>(name: K, source: () => DepSchema[K]): void;
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Shared dep-bag builder — the single implementation behind both the gesture
|
|
1095
|
+
* dispatcher's per-invocation `buildDeps` and `ActionsRegistry.trigger`'s
|
|
1096
|
+
* `requires` branch. Reads the action's declared `requires`, resolves each
|
|
1097
|
+
* name against the `DepRegistry`, and (dev builds only) wraps the bag in a
|
|
1098
|
+
* Proxy that warns on undeclared reads.
|
|
1099
|
+
*/
|
|
1100
|
+
|
|
1101
|
+
/** Build the deps bag for an action from its declared `requires`. */
|
|
1102
|
+
declare function buildDepsFromRequires(action: Action, depRegistry: DepRegistry): ActionDeps;
|
|
1103
|
+
|
|
1104
|
+
/** Hotkey-slot trigger key. The slot is engaged while this key is held —
|
|
1105
|
+
* hence "hotkey": active as long as the key is hot. `null` (or omitted)
|
|
1106
|
+
* means the tool is not eligible for the hotkey slot. */
|
|
1107
|
+
type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift';
|
|
1108
|
+
/** Presentation metadata for tool palettes / menus. Optional on every
|
|
1109
|
+
* tool — consumers that render a palette (`<ToolPalette>`) read these
|
|
1110
|
+
* fields to display the tool; consumers that don't can ignore them.
|
|
1111
|
+
*
|
|
1112
|
+
* Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
|
|
1113
|
+
* is already plumbed through `<Canvas>` to `style.cursor` on the host. */
|
|
1114
|
+
interface ToolPresentation<TScratch = unknown> {
|
|
1115
|
+
/** Human-readable label, distinct from the `id`. Falls back to `id`. */
|
|
1116
|
+
label?: string;
|
|
1117
|
+
/** Inline-SVG icon component output. May be a static `ReactNode` or a
|
|
1118
|
+
* function of scratch state (rare; useful for shape-aware affordances). */
|
|
1119
|
+
icon?: react.ReactNode | ((scratch?: TScratch) => react.ReactNode);
|
|
1120
|
+
/** Palette grouping key. Tools sharing a group render contiguously
|
|
1121
|
+
* with separators between groups. Free-form string; the kit
|
|
1122
|
+
* recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
|
|
1123
|
+
group?: string;
|
|
1124
|
+
/** Display override for the keyboard shortcut. When omitted the palette
|
|
1125
|
+
* derives one from `Tool.keybinding` via its own formatter. */
|
|
1126
|
+
shortcut?: string;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* When an entry's bindings are live. A set, not one value: the hand tool is
|
|
1130
|
+
* palette-selectable AND engaged by holding space, and both hold at once.
|
|
1131
|
+
*/
|
|
1132
|
+
interface Eligibility {
|
|
1133
|
+
/** Selectable as the focused entry — exclusive, one at a time. */
|
|
1134
|
+
focus?: boolean;
|
|
1135
|
+
/** Also live while this key is held. */
|
|
1136
|
+
offhand?: HotkeyTrigger;
|
|
1137
|
+
/** Live regardless of what is focused. */
|
|
1138
|
+
always?: boolean;
|
|
1139
|
+
/** Live only for input this entry's own affordances produced. */
|
|
1140
|
+
claimed?: boolean;
|
|
1141
|
+
/** Modality filter, applied wherever it would otherwise be live. */
|
|
1142
|
+
capabilities?: CapabilityTag[];
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Where an entry's overlay sits in the layer stack, relative to the
|
|
1146
|
+
* selection chrome. `'top'` is the default and renders above everything;
|
|
1147
|
+
* the other two exist for chrome that belongs under the selection handles
|
|
1148
|
+
* (a snap-target highlight, say). With no selection overlay in the stack,
|
|
1149
|
+
* all three collapse to `'top'`.
|
|
1150
|
+
*/
|
|
1151
|
+
type OverlayPosition = 'top' | 'before-selection' | 'after-selection';
|
|
1152
|
+
/**
|
|
1153
|
+
* What routing reads off an entry: when its bindings are live, which they
|
|
1154
|
+
* are, and the actions they name.
|
|
1155
|
+
*/
|
|
1156
|
+
interface ContributionRouting {
|
|
1157
|
+
id: string;
|
|
1158
|
+
eligibility: Eligibility;
|
|
1159
|
+
bindings?: GestureBinding[];
|
|
1160
|
+
actions?: Action[];
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* What an entry draws and how a palette shows it. None of it reaches the
|
|
1164
|
+
* dispatcher.
|
|
1165
|
+
*/
|
|
1166
|
+
interface ContributionChrome<TOverlay = unknown> {
|
|
1167
|
+
/** One layer, or several composed in the given order.
|
|
1168
|
+
*
|
|
1169
|
+
* Routing never reads a layer — it collects the live ones and hands them
|
|
1170
|
+
* back in scope order. `TOverlay` is what the consuming kernel draws;
|
|
1171
|
+
* `@weasel-js/core` binds it to `RenderLayer<unknown>`. */
|
|
1172
|
+
overlay?: TOverlay | TOverlay[];
|
|
1173
|
+
/** Defaults to `'top'`. Applies to every layer in `overlay`. */
|
|
1174
|
+
overlayPosition?: OverlayPosition;
|
|
1175
|
+
presentation?: ToolPresentation;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* A registry entry: what it contributes, and when it is eligible. Every role
|
|
1179
|
+
* is optional and independent — an entry that only routes input declares only
|
|
1180
|
+
* `bindings` and `actions`.
|
|
1181
|
+
*/
|
|
1182
|
+
interface Contribution<TOverlay = unknown> extends ContributionRouting, ContributionChrome<TOverlay> {
|
|
1183
|
+
/** Reflection escape hatch — the authored form, when there was one. */
|
|
1184
|
+
def?: unknown;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Configurable activation-key descriptor for tools that expose their
|
|
1189
|
+
* keybinding to the host (currently Lasso and Eyedropper). Captures
|
|
1190
|
+
* only the fields meaningful to a caller-supplied tool-select key —
|
|
1191
|
+
* dispatcher-internal fields (`skipInEditable`, `enabled`,
|
|
1192
|
+
* `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
|
|
1193
|
+
* not part of the configurable surface.
|
|
1194
|
+
*/
|
|
1195
|
+
interface ToolKeybinding {
|
|
1196
|
+
/** Key or list of keys to match (case-insensitive against `event.key`). */
|
|
1197
|
+
key: string | readonly string[];
|
|
1198
|
+
/** Require Cmd (mac) / Ctrl (others). Default `false`. */
|
|
1199
|
+
mod?: boolean;
|
|
1200
|
+
/** Require Alt. Default `false`. */
|
|
1201
|
+
alt?: boolean;
|
|
1202
|
+
/**
|
|
1203
|
+
* Shift policy. `undefined`/`false` forbids shift, `true` requires
|
|
1204
|
+
* shift, `'optional'` allows either.
|
|
1205
|
+
*/
|
|
1206
|
+
shift?: boolean | 'optional';
|
|
1207
|
+
}
|
|
1208
|
+
/** Modifier-key snapshot at event dispatch time. */
|
|
1209
|
+
type ToolModifiers = ModifierState;
|
|
1210
|
+
/** Per-event context passed to every channel handler. `scratch` is typed
|
|
1211
|
+
* via the tool's `TScratch` parameter; it survives across a single
|
|
1212
|
+
* gesture (pointer-down through end/cancel) and is replaced on next
|
|
1213
|
+
* gesture start by `initScratch()`. */
|
|
1214
|
+
interface ToolCtx<TScratch = unknown> {
|
|
1215
|
+
worldX: number;
|
|
1216
|
+
worldY: number;
|
|
1217
|
+
modifiers: ToolModifiers;
|
|
1218
|
+
selection: SelectionApi;
|
|
1219
|
+
/** Adapter/scene access — opaque at this layer; tools that need it
|
|
1220
|
+
* cast to a known shape. This layer doesn't constrain it. */
|
|
1221
|
+
adapter: unknown;
|
|
1222
|
+
applyOps: (ops: Op[], label: string) => void;
|
|
1223
|
+
/** Current viewport. Reflects camera-position semantics — see
|
|
1224
|
+
* `View` JSDoc. */
|
|
1225
|
+
view: View;
|
|
1226
|
+
/** Mutate the viewport. In controlled mode this calls the consumer's
|
|
1227
|
+
* `onViewChange`; in uncontrolled mode it updates Canvas's internal
|
|
1228
|
+
* state. View changes are not undoable. */
|
|
1229
|
+
setView: (next: View) => void;
|
|
1230
|
+
/** Bounding rect of the canvas element in viewport coords. Used by
|
|
1231
|
+
* zoom/pan tools to convert event clientX/clientY to canvas-relative
|
|
1232
|
+
* anchors. */
|
|
1233
|
+
canvasRect: DOMRect;
|
|
1234
|
+
/** Screen-space pointer coords relative to `canvasRect`. Useful for
|
|
1235
|
+
* viewport tools that pan/zoom in screen space (e.g. hand-pan
|
|
1236
|
+
* computes deltas in pixels, not world units). Optional — populated
|
|
1237
|
+
* by the dispatcher on pointer events; absent on keyboard events. */
|
|
1238
|
+
screenPoint?: {
|
|
1239
|
+
x: number;
|
|
1240
|
+
y: number;
|
|
1241
|
+
};
|
|
1242
|
+
/** Optional debug sink. When `<Canvas debug={...}>` is enabled, Canvas
|
|
1243
|
+
* threads its sink here so tool-internal hit math (handle hitboxes,
|
|
1244
|
+
* rotation handle, etc.) lands in the same overlay as Canvas's own
|
|
1245
|
+
* bounds/origin records. Tools should call this conditionally with `?.`. */
|
|
1246
|
+
debug?: DebugSink;
|
|
1247
|
+
scratch: TScratch;
|
|
1248
|
+
}
|
|
1249
|
+
/** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
|
|
1250
|
+
* `Bounds` type — the optional `rotation` field carries through so a tool
|
|
1251
|
+
* can report an oriented preview rect (e.g. mid-rotate). */
|
|
1252
|
+
type ToolBounds = Bounds;
|
|
1253
|
+
/**
|
|
1254
|
+
* The focus-declaring case of a `Contribution`: a mode the user switches
|
|
1255
|
+
* into, plus the hooks that only make sense for one (`initScratch`,
|
|
1256
|
+
* activate/deactivate, live preview, `cursor`). Everything else — bindings,
|
|
1257
|
+
* actions, overlay, presentation — is inherited.
|
|
1258
|
+
*/
|
|
1259
|
+
interface Tool<TScratch = unknown, TOverlay = unknown> extends Contribution<TOverlay> {
|
|
1260
|
+
/** Optional caller-supplied key. Most built-in tools have their activation
|
|
1261
|
+
* key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
|
|
1262
|
+
* is for tools that want their activation key to be configurable by the
|
|
1263
|
+
* host (currently Lasso and Eyedropper). The dynamic loop in
|
|
1264
|
+
* `useKeybindings.ts` picks this up and appends a binding entry to the
|
|
1265
|
+
* consolidated `tool.activate` action (with `opts.params.toolId` set so
|
|
1266
|
+
* the invoker knows which tool to switch to). */
|
|
1267
|
+
keybinding?: ToolKeybinding;
|
|
1268
|
+
initScratch?: () => TScratch;
|
|
1269
|
+
cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
|
|
1270
|
+
onActivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
1271
|
+
onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
1272
|
+
/** Returns the in-flight preview pose for `id` if this tool is mid-gesture
|
|
1273
|
+
* on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
|
|
1274
|
+
* reflect live gesture state without reaching into hook internals. The
|
|
1275
|
+
* return type is `unknown` here because the Tool interface is pose-agnostic;
|
|
1276
|
+
* callers that know the pose shape (e.g. Canvas typed by `TPose`) cast at
|
|
1277
|
+
* the use site. */
|
|
1278
|
+
previewPose?: (id: string) => unknown;
|
|
1279
|
+
/** Returns the in-flight preview bounds for `id` if this tool is mid-gesture
|
|
1280
|
+
* on it; otherwise `null`. Optional companion to `previewPose` for tools that
|
|
1281
|
+
* can compute bounds without round-tripping through a geometry adapter. */
|
|
1282
|
+
previewBounds?: (id: string) => ToolBounds | null;
|
|
1283
|
+
/** Returns ids whose committed scene-render should be suppressed while this
|
|
1284
|
+
* tool is mid-gesture (e.g. cascade move's dragged + descendant ids whose
|
|
1285
|
+
* preview ghosts replace the committed pose). The standard scene slot
|
|
1286
|
+
* consults this alongside `previewPose` to avoid double-rendering. Returns
|
|
1287
|
+
* `null` when no gesture is in flight. */
|
|
1288
|
+
previewIds?: () => Iterable<string> | null;
|
|
1289
|
+
}
|
|
1290
|
+
/** Internal — which slot a tool occupies in the dispatch order. */
|
|
1291
|
+
type ToolSlot = 'hotkey' | 'active' | 'ambient';
|
|
1292
|
+
/** Internal alias for "a Tool of any scratch type" — used in registries and
|
|
1293
|
+
* dispatchers that hold tools of heterogeneous scratch shapes. `any` is
|
|
1294
|
+
* intentional: `Tool<TScratch>` is invariant in TScratch, so `Tool<unknown>`
|
|
1295
|
+
* is too strict for containers that accept any concrete `Tool<T>`. */
|
|
1296
|
+
type AnyTool = Tool<any, any>;
|
|
1297
|
+
/** "A tool of any scratch type that draws `TOverlay`" — the container form for
|
|
1298
|
+
* registries that hold heterogeneous scratch shapes but one overlay type. */
|
|
1299
|
+
type AnyToolOf<TOverlay> = Tool<any, TOverlay>;
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Pure matcher primitives live in `@weasel-js/gestures`. This file
|
|
1303
|
+
* re-exports them for kit-internal consumers and layers the actions-layer
|
|
1304
|
+
* binding-scope / matchBest logic on top.
|
|
1305
|
+
*/
|
|
1306
|
+
|
|
1307
|
+
/** Where a binding came from, which is also its priority: a held hotkey beats
|
|
1308
|
+
* the active tool, which beats bindings that are always in scope. */
|
|
1309
|
+
type BindingScope = 'ambient' | 'active' | 'hotkey';
|
|
1310
|
+
/** A binding paired with where it came from, ready to be matched against an
|
|
1311
|
+
* event. */
|
|
1312
|
+
interface ScopedBinding {
|
|
1313
|
+
binding: GestureBinding;
|
|
1314
|
+
scope: BindingScope;
|
|
1315
|
+
/** Tool id that owns this binding — `'&'`-channel phase atoms resolve
|
|
1316
|
+
* to this. `null` for ambient bindings that came from a registered
|
|
1317
|
+
* Action with no owning tool. */
|
|
1318
|
+
ownerToolId: string | null;
|
|
1319
|
+
}
|
|
1320
|
+
/** The binding that won a match. */
|
|
1321
|
+
interface MatchResult {
|
|
1322
|
+
binding: GestureBinding;
|
|
1323
|
+
scope: BindingScope;
|
|
1324
|
+
/** Tool id that owns the binding — propagated from `ScopedBinding`
|
|
1325
|
+
* so the dispatcher can record it as the handle owner. */
|
|
1326
|
+
ownerToolId: string | null;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* True when a spec's target actually consults the affordance hit rather than
|
|
1330
|
+
* only the body classification. `kindOf` predicates are handed the hit;
|
|
1331
|
+
* `affordance:<k>` matches on its `kind`. The body-class strings (`'empty'`,
|
|
1332
|
+
* `'selected-body'`, `'unselected-body'`) and the `kind:` forms resolve from
|
|
1333
|
+
* `bodyTarget` / `bodyKind` and never see it — which is why chrome floating
|
|
1334
|
+
* over empty canvas used to read as empty canvas.
|
|
1335
|
+
*
|
|
1336
|
+
* A predicate carrying `readsAffordance: false` says the same about itself;
|
|
1337
|
+
* the kit's own body predicates do. Shape stays the fallback for predicates
|
|
1338
|
+
* that declare nothing.
|
|
1339
|
+
*/
|
|
1340
|
+
declare function targetConsultsAffordance(specTarget: TargetSpec | undefined): boolean;
|
|
1341
|
+
/** CSS-style specificity tuple for a GestureSpec. Higher tuple wins under
|
|
1342
|
+
* lexicographic compare. Dimensions, in order of precedence:
|
|
1343
|
+
*
|
|
1344
|
+
* [0] target — how much the spec's target narrows; see `targetRank`.
|
|
1345
|
+
* [1] mods — count of required modifier keys (shift/alt/ctrl/meta/mod).
|
|
1346
|
+
* `'optional'` does NOT count.
|
|
1347
|
+
* [2] phase — how much the spec's `phase` narrows; see `phaseRank`.
|
|
1348
|
+
* [3] exact — per-kind tiebreak: 2 for a drop/paste spec with a
|
|
1349
|
+
* non-empty `types` MIME filter, else 1.
|
|
1350
|
+
*
|
|
1351
|
+
* Identical tuples fall back to registration order in the matcher's
|
|
1352
|
+
* stable sort, preserving the pre-specificity tiebreaker. */
|
|
1353
|
+
declare function specificity(spec: GestureSpec): readonly [number, number, number, number];
|
|
1354
|
+
/**
|
|
1355
|
+
* Find the best-matching binding across all scopes.
|
|
1356
|
+
*
|
|
1357
|
+
* Precedence: hotkey > active > ambient.
|
|
1358
|
+
* Within a scope, more-specific bindings win; same-specificity bindings keep
|
|
1359
|
+
* registration order (see `matchSorted`).
|
|
1360
|
+
* Returns null when nothing matches.
|
|
1361
|
+
*
|
|
1362
|
+
* Note: this returns only the single best match. The dispatcher uses
|
|
1363
|
+
* `matchSorted` so it can fall through to lower-specificity matches when a
|
|
1364
|
+
* higher-specificity match's `enabled()` reports disabled.
|
|
1365
|
+
*/
|
|
1366
|
+
declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>): MatchResult | null;
|
|
1367
|
+
/**
|
|
1368
|
+
* Find every binding that matches the event, sorted by precedence
|
|
1369
|
+
* (best → worst). Ordering rules:
|
|
1370
|
+
*
|
|
1371
|
+
* - Scopes by priority: hotkey > active > ambient.
|
|
1372
|
+
* - Within a scope, more-specific bindings beat less-specific (see
|
|
1373
|
+
* `specificity()`).
|
|
1374
|
+
* - Among same-specificity bindings, first-declared wins (stable sort
|
|
1375
|
+
* preserves registration order — same tiebreaker as pre-specificity).
|
|
1376
|
+
*
|
|
1377
|
+
* Used by the dispatcher to implement specificity-ordered fall-through:
|
|
1378
|
+
* when the top match's `enabled()` returns a disabled reason (or the
|
|
1379
|
+
* action's `start()` returns an empty handle), the next match is tried.
|
|
1380
|
+
*/
|
|
1381
|
+
declare function matchSorted(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, warn?: (message: string) => void): MatchResult[];
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Dispatcher orchestrator — pure module, no React, no DOM.
|
|
1385
|
+
*
|
|
1386
|
+
* Assembles `ScopedBinding[]` from the actions registry, active tool, and
|
|
1387
|
+
* hotkey stack; matches input events via `matchSorted`; gates each candidate
|
|
1388
|
+
* on `enabled()`; then invokes `immediate` or `ongoing` invokers and tracks
|
|
1389
|
+
* in-flight handles.
|
|
1390
|
+
*
|
|
1391
|
+
* ## Specificity-ordered fall-through
|
|
1392
|
+
* `matchSorted` returns every matching binding in precedence order
|
|
1393
|
+
* (hotkey > active > ambient, first-declared within scope). The dispatcher
|
|
1394
|
+
* walks that list and fires the first action whose `enabled()` returns
|
|
1395
|
+
* `true`. If every candidate's `enabled()` returns a disabled reason, the
|
|
1396
|
+
* event is unhandled. This mirrors CSS-style specificity matching with a
|
|
1397
|
+
* `:not(:disabled)` filter, and lets a tool declare a high-specificity
|
|
1398
|
+
* binding (e.g. drag-on-empty → areaSelect) that gracefully falls through
|
|
1399
|
+
* to a lower-specificity ambient binding (e.g. drag → viewport.dragPan)
|
|
1400
|
+
* when its required deps aren't wired.
|
|
1401
|
+
*
|
|
1402
|
+
* ## gestureId scheme
|
|
1403
|
+
* - `key-held` ongoing actions: `key-held-<key>` (e.g. `key-held- ` for Space).
|
|
1404
|
+
* Chosen because key-held gestures are identified by the held key alone.
|
|
1405
|
+
* - `pointerdown` / drag ongoing actions: `pointer-<pointerId>`, taken from
|
|
1406
|
+
* the originating DOM `PointerEvent`. Each physical pointer — mouse, each
|
|
1407
|
+
* touch, the stylus — gets its own handle slot. Events with no
|
|
1408
|
+
* `pointerId` (synthesized probes, programmatic drags, most tests) key to
|
|
1409
|
+
* `pointer-mouse`, so a single synthetic pointer behaves as it always has.
|
|
1410
|
+
* - `multitouch` ongoing actions: `multitouch-<fingers>`.
|
|
1411
|
+
* - Fallback for any other kind that triggers an ongoing invoker: `ongoing-<kind>`.
|
|
1412
|
+
*
|
|
1413
|
+
* ## Action-lookup miss behavior
|
|
1414
|
+
* When `matchBest` resolves a binding whose `actionId` has no entry in
|
|
1415
|
+
* `ctx.actions.list()`, the dispatcher emits `console.warn` and returns
|
|
1416
|
+
* `'unhandled'`. The user's input gesture falls through as if unmatched.
|
|
1417
|
+
* This preserves input flow (nothing is swallowed silently) while flagging
|
|
1418
|
+
* the misconfiguration at dev time.
|
|
1419
|
+
*/
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* The in-flight handle slot a pointer's gestures key into.
|
|
1423
|
+
*
|
|
1424
|
+
* Exported because the React seam (`useGestureDispatcher`) has to name the
|
|
1425
|
+
* same slot when it asks `dispatcher.inFlight()` whether a given pointer is
|
|
1426
|
+
* mid-drag. That used to be the string literal `'pointer-mouse'` on both
|
|
1427
|
+
* sides, which was correct only because `gestureIdFor` ignored the pointer id
|
|
1428
|
+
* entirely — the two lies cancelled out.
|
|
1429
|
+
*
|
|
1430
|
+
* @param pointerId - `PointerEvent.pointerId`, or `undefined` for a
|
|
1431
|
+
* synthesized event with no originating DOM pointer.
|
|
1432
|
+
*/
|
|
1433
|
+
declare function pointerGestureId(pointerId: number | undefined): string;
|
|
1434
|
+
/** One entry per `handleInput` call. */
|
|
1435
|
+
interface DispatchLogEntry {
|
|
1436
|
+
kind: 'dispatch';
|
|
1437
|
+
ts: number;
|
|
1438
|
+
eventKind: string;
|
|
1439
|
+
/** For `key` / `key-held` events, the key id (`'Escape'`, `' '`, `'a'`).
|
|
1440
|
+
* Lives on the event side of the entry — `fired` should describe
|
|
1441
|
+
* what dispatched, not which key triggered it. */
|
|
1442
|
+
key?: string;
|
|
1443
|
+
candidates: Array<{
|
|
1444
|
+
actionId: string;
|
|
1445
|
+
scope: BindingScope;
|
|
1446
|
+
enabledResult: boolean | string;
|
|
1447
|
+
}>;
|
|
1448
|
+
fired: string | null;
|
|
1449
|
+
outcome: 'handled' | 'unhandled';
|
|
1450
|
+
}
|
|
1451
|
+
/** One entry per "mode change" — a kit state transition that isn't a
|
|
1452
|
+
* dispatched input event but is load-bearing for understanding why an
|
|
1453
|
+
* input was (or wasn't) handled later. Pushed via `recordModeSwitch()`;
|
|
1454
|
+
* consumers include the editAnchors dep (editingId changes), and
|
|
1455
|
+
* could expand to active-tool / hotkey-stack / focus changes. */
|
|
1456
|
+
interface ModeSwitchLogEntry {
|
|
1457
|
+
kind: 'mode';
|
|
1458
|
+
ts: number;
|
|
1459
|
+
/** Logical mode name, e.g. `'editAnchors.editingId'`. */
|
|
1460
|
+
mode: string;
|
|
1461
|
+
from: string | null;
|
|
1462
|
+
to: string | null;
|
|
1463
|
+
/** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
|
|
1464
|
+
* `'selection-dropped target'`, etc. */
|
|
1465
|
+
detail?: string;
|
|
1466
|
+
}
|
|
1467
|
+
type TraceLogEntry = DispatchLogEntry | ModeSwitchLogEntry;
|
|
1468
|
+
/** Push a mode-switch record into the trace log. Safe to call from
|
|
1469
|
+
* anywhere; no-ops outside DEV. */
|
|
1470
|
+
declare function recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
|
|
1471
|
+
/** Everything the dispatcher must consult to route one input event: the
|
|
1472
|
+
* registered actions and their deps, which tool is active, which hotkeys are
|
|
1473
|
+
* held, and — optionally — the chrome state that action eligibility rules are
|
|
1474
|
+
* evaluated against. Rebuilt per event rather than held, so the dispatcher
|
|
1475
|
+
* itself stays stateless apart from in-flight gestures. */
|
|
1476
|
+
interface DispatcherContext {
|
|
1477
|
+
/** All registered actions; the dispatcher walks `.defaultBinding` for ambient bindings. */
|
|
1478
|
+
actions: ActionSource;
|
|
1479
|
+
/** Dep sources keyed by name. */
|
|
1480
|
+
depRegistry: DepRegistry;
|
|
1481
|
+
/** Active tool's id (from ActiveToolContext). */
|
|
1482
|
+
activeToolId: string;
|
|
1483
|
+
/** Held-hotkey stack, top of stack last. */
|
|
1484
|
+
hotkeyStack: readonly string[];
|
|
1485
|
+
/** Lookup for tool definitions. */
|
|
1486
|
+
toolsById: ReadonlyMap<string, Tool>;
|
|
1487
|
+
/** Platform flag for `mod` shorthand resolution. */
|
|
1488
|
+
isMac: boolean;
|
|
1489
|
+
/**
|
|
1490
|
+
* Thunk returning a fresh `RuleCtx` for the current frame — the routed
|
|
1491
|
+
* view's, when the surface hosts several. When it answers, the dispatcher
|
|
1492
|
+
* filters matched candidates by their declared `Action.eligible` rule
|
|
1493
|
+
* (omitted => always eligible). Absent, or answering `undefined`, applies
|
|
1494
|
+
* no eligibility filtering.
|
|
1495
|
+
*/
|
|
1496
|
+
getRuleCtx?: () => RuleCtx | undefined;
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Filter `matches` to only those whose backing action has no `eligible`
|
|
1500
|
+
* rule, or whose rule evaluates true against `ruleCtx`. Exported for
|
|
1501
|
+
* unit testing.
|
|
1502
|
+
*
|
|
1503
|
+
* @internal
|
|
1504
|
+
*/
|
|
1505
|
+
declare function filterEligible<M extends {
|
|
1506
|
+
binding: {
|
|
1507
|
+
actionId: string;
|
|
1508
|
+
};
|
|
1509
|
+
}>(matches: readonly M[], actionLookup: (id: string) => Action | undefined, ruleCtx: RuleCtx): M[];
|
|
1510
|
+
/**
|
|
1511
|
+
* Handle returned by `Dispatcher.beginUiOngoing()` for driving an
|
|
1512
|
+
* ongoing invoker from a UI control (color picker, slider).
|
|
1513
|
+
*
|
|
1514
|
+
* - `update(params)` rebuilds an `InvocationCtx` with the new params and
|
|
1515
|
+
* calls the handle's `onMove`. Safe to call many times.
|
|
1516
|
+
* - `end(reason)` calls `onEnd(ctx, reason)` once and removes the handle
|
|
1517
|
+
* from the in-flight map. Idempotent — further calls are no-ops.
|
|
1518
|
+
*/
|
|
1519
|
+
interface UiOngoingControl {
|
|
1520
|
+
readonly gestureId: string;
|
|
1521
|
+
update(params?: Record<string, unknown>): void;
|
|
1522
|
+
end(reason: 'commit' | 'cancel'): void;
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Successful `Dispatcher.resolveOnly` prediction: the binding + action that
|
|
1526
|
+
* would fire if `event` were dispatched for real. `action` is the resolved
|
|
1527
|
+
* descriptor so callers (the hover-cursor pump) can read metadata like
|
|
1528
|
+
* `Action.cursor` without a second registry lookup.
|
|
1529
|
+
*/
|
|
1530
|
+
interface ResolveOnlyResult {
|
|
1531
|
+
actionId: string;
|
|
1532
|
+
action: Action;
|
|
1533
|
+
scope: BindingScope;
|
|
1534
|
+
/** Tool id owning the winning binding; `null` for ambient action bindings. */
|
|
1535
|
+
ownerToolId: string | null;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* One candidate from `Dispatcher.resolveAll` — a binding that matched the
|
|
1539
|
+
* event, with why it did or didn't get to fire.
|
|
1540
|
+
*
|
|
1541
|
+
* Verdicts:
|
|
1542
|
+
* - `would-fire` — eligible, `enabled()` passed, and nothing above it fired.
|
|
1543
|
+
* At most one candidate per call carries this.
|
|
1544
|
+
* - `ineligible` — the action's `eligible` rule evaluated false against the
|
|
1545
|
+
* live `RuleCtx`. `reason` is the rule, serialized.
|
|
1546
|
+
* - `disabled` — `enabled()` returned a disabled reason, carried verbatim.
|
|
1547
|
+
* - `shadowed` — never asked, for one of two reasons: something above it
|
|
1548
|
+
* already fired, or it is a repeat binding of the action that itself won
|
|
1549
|
+
* higher in the list (several bindings may point at one action, and the
|
|
1550
|
+
* dispatcher runs each action at most once). A repeat of an action that was
|
|
1551
|
+
* already judged `ineligible` or `disabled` is NOT shadowed — it inherits
|
|
1552
|
+
* that action's verdict, since that is the reason it doesn't fire.
|
|
1553
|
+
*/
|
|
1554
|
+
interface ResolvedCandidate {
|
|
1555
|
+
actionId: string;
|
|
1556
|
+
action: Action;
|
|
1557
|
+
binding: GestureBinding;
|
|
1558
|
+
scope: BindingScope;
|
|
1559
|
+
ownerToolId: string | null;
|
|
1560
|
+
/** The tuple from `specificity(binding.spec)`, surfaced so a reader can see
|
|
1561
|
+
* why one candidate outranks another rather than inferring it. */
|
|
1562
|
+
specificity: readonly [number, number, number, number];
|
|
1563
|
+
verdict: {
|
|
1564
|
+
kind: 'would-fire';
|
|
1565
|
+
} | {
|
|
1566
|
+
kind: 'ineligible';
|
|
1567
|
+
reason: string;
|
|
1568
|
+
} | {
|
|
1569
|
+
kind: 'disabled';
|
|
1570
|
+
reason: string;
|
|
1571
|
+
} | {
|
|
1572
|
+
kind: 'shadowed';
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
/** Options for {@link Dispatcher.resolveAll}. */
|
|
1576
|
+
interface ResolveAllOptions {
|
|
1577
|
+
/**
|
|
1578
|
+
* Evaluate eligibility and `enabled()` for candidates below the winner
|
|
1579
|
+
* instead of short-circuiting them to `shadowed`.
|
|
1580
|
+
*
|
|
1581
|
+
* Off by default, and deliberately so: the default walk's early exit is what
|
|
1582
|
+
* keeps `resolveOnly`'s `enabled()` call count identical to a real dispatch,
|
|
1583
|
+
* and `enabled()` predicates are only contractually pure — not free.
|
|
1584
|
+
*
|
|
1585
|
+
* Turn it on for diagnostics, where "this one was outranked" is a less
|
|
1586
|
+
* useful answer than "this one was outranked AND would have been disabled
|
|
1587
|
+
* anyway". With it on, `shadowed` narrows to its precise meaning: this
|
|
1588
|
+
* candidate would have fired, but something above it did.
|
|
1589
|
+
*/
|
|
1590
|
+
evaluateShadowed?: boolean;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Routes input events to actions.
|
|
1594
|
+
*
|
|
1595
|
+
* For each event it assembles the bindings in scope (hotkey, then active tool,
|
|
1596
|
+
* then ambient), matches them in specificity order, filters by eligibility and
|
|
1597
|
+
* each action's `enabled` gate, and invokes the first survivor. Ongoing
|
|
1598
|
+
* actions — anything that runs across a drag — are kept in flight here and
|
|
1599
|
+
* pumped with subsequent moves until the gesture ends.
|
|
1600
|
+
*/
|
|
1601
|
+
interface Dispatcher {
|
|
1602
|
+
/**
|
|
1603
|
+
* Route an input event through the binding pipeline. Returns `'handled'`
|
|
1604
|
+
* when a binding matched and the action invoked successfully (whether it
|
|
1605
|
+
* returned ops or not). Returns `'unhandled'` when no binding matched or
|
|
1606
|
+
* the matched action's `enabled()` returned a disabled reason.
|
|
1607
|
+
*/
|
|
1608
|
+
handleInput(event: InputEvent, ctx: DispatcherContext): 'handled' | 'unhandled';
|
|
1609
|
+
/**
|
|
1610
|
+
* Predict which action `event` would route to WITHOUT invoking it. Replays
|
|
1611
|
+
* the same walk as `handleInput` — scope assembly, specificity-sorted
|
|
1612
|
+
* match, eligibility filter, per-candidate `enabled()` gate — and returns
|
|
1613
|
+
* the first candidate that would fire, or `null` when the event would go
|
|
1614
|
+
* unhandled. Pure query: no invoker runs, no in-flight state changes, no
|
|
1615
|
+
* trace-log entry.
|
|
1616
|
+
*
|
|
1617
|
+
* Known divergence from a real dispatch: an ongoing invoker that matches
|
|
1618
|
+
* but returns an empty handle at `start()` (runtime bail) makes the real
|
|
1619
|
+
* dispatch fall through to the next candidate; prediction cannot see that
|
|
1620
|
+
* and reports the bailing action. Keep `enabled()` accurate on actions
|
|
1621
|
+
* that rely on prediction (hover cursors).
|
|
1622
|
+
*/
|
|
1623
|
+
resolveOnly(event: InputEvent, ctx: DispatcherContext): ResolveOnlyResult | null;
|
|
1624
|
+
/**
|
|
1625
|
+
* Every binding that matches `event`, in dispatch precedence order, each
|
|
1626
|
+
* with a verdict explaining whether it would fire. Same walk as
|
|
1627
|
+
* `resolveOnly` — scope assembly, specificity-sorted match, eligibility
|
|
1628
|
+
* check, per-candidate `enabled()` gate — without stopping at the winner
|
|
1629
|
+
* and without invoking anything. Nothing is dropped: candidates that
|
|
1630
|
+
* `resolveOnly`'s walk would filter out are kept here and labelled
|
|
1631
|
+
* `ineligible` instead. Pure query: no invoker runs, no in-flight state
|
|
1632
|
+
* changes, no trace-log entry.
|
|
1633
|
+
*
|
|
1634
|
+
* `resolveOnly` is the first `would-fire` entry of this list.
|
|
1635
|
+
*
|
|
1636
|
+
* Shares `resolveOnly`'s known divergence from a real dispatch: an ongoing
|
|
1637
|
+
* invoker that matches but returns an empty handle at `start()` makes the
|
|
1638
|
+
* real dispatch fall through, and this cannot see that.
|
|
1639
|
+
*
|
|
1640
|
+
* By default everything below the winner is `shadowed` without being asked,
|
|
1641
|
+
* which is what keeps this walk as cheap as the dispatch it replays. Pass
|
|
1642
|
+
* `{ evaluateShadowed: true }` to keep evaluating past the winner, so a
|
|
1643
|
+
* lower candidate that is ALSO ineligible or disabled says so — see
|
|
1644
|
+
* {@link ResolveAllOptions.evaluateShadowed}.
|
|
1645
|
+
*/
|
|
1646
|
+
resolveAll(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): ResolvedCandidate[];
|
|
1647
|
+
/**
|
|
1648
|
+
* Synthesize an end-of-gesture for every in-flight ongoing handle.
|
|
1649
|
+
* Used by tool-switch cancellation (Q2 decision).
|
|
1650
|
+
*/
|
|
1651
|
+
cancelAll(reason: 'commit' | 'cancel'): void;
|
|
1652
|
+
/**
|
|
1653
|
+
* Read-only view of currently in-flight ongoing handles, keyed by gestureId.
|
|
1654
|
+
* For debug/testing.
|
|
1655
|
+
*/
|
|
1656
|
+
inFlight(): ReadonlyMap<string, OngoingHandle>;
|
|
1657
|
+
/**
|
|
1658
|
+
* CSS cursor for the gesture currently in flight, or `null` when nothing
|
|
1659
|
+
* is. Reads `Action.activeCursor` (falling back to `Action.cursor`) off the
|
|
1660
|
+
* action whose handle is open — the hover pump applies this instead of its
|
|
1661
|
+
* prediction once a gesture starts, which is how `grab` becomes `grabbing`.
|
|
1662
|
+
*/
|
|
1663
|
+
inFlightCursor(): string | null;
|
|
1664
|
+
/**
|
|
1665
|
+
* Read-only iterator over currently in-flight `OngoingHandle` instances.
|
|
1666
|
+
*
|
|
1667
|
+
* Surface for the canvas's preview-ghost layer (`usePreviewGhostLayer`)
|
|
1668
|
+
* to walk each handle's `previewIds()` / `previewPose(id)` and render
|
|
1669
|
+
* dispatcher-driven gesture previews. Read-only by design:
|
|
1670
|
+
* external consumers must not mutate the in-flight map.
|
|
1671
|
+
*/
|
|
1672
|
+
getInFlightHandles(): Iterable<OngoingHandle>;
|
|
1673
|
+
/**
|
|
1674
|
+
* Subscribe to in-flight state changes. The callback fires after every
|
|
1675
|
+
* mutation that affects what the preview-ghost / dispatcher-overlay
|
|
1676
|
+
* layers read — handle start, every `onMove` pump, end, cancel,
|
|
1677
|
+
* cancel-all. Consumers re-read `getInFlightHandles()` and re-render.
|
|
1678
|
+
*
|
|
1679
|
+
* Returns an unsubscribe function.
|
|
1680
|
+
*/
|
|
1681
|
+
subscribe(fn: () => void): () => void;
|
|
1682
|
+
/**
|
|
1683
|
+
* Monotonic counter bumped on exactly the events {@link subscribe} fires
|
|
1684
|
+
* on. The snapshot half of the `useSyncExternalStore` contract: pair it
|
|
1685
|
+
* with `subscribe` to drive a render off in-flight gesture state without
|
|
1686
|
+
* a `useReducer` force-rerender.
|
|
1687
|
+
*
|
|
1688
|
+
* Starts at 0 and only ever increases. Two reads returning the same number
|
|
1689
|
+
* mean nothing pumped in between; it does **not** guarantee that a bump
|
|
1690
|
+
* changed anything observable (a pump that matched no binding still
|
|
1691
|
+
* counts — see `subscribe`).
|
|
1692
|
+
*/
|
|
1693
|
+
getVersion(): number;
|
|
1694
|
+
/**
|
|
1695
|
+
* Snapshot of the currently active action, for surfaces (chrome-caps
|
|
1696
|
+
* visibility rules, debug HUDs) that need to react to "what action
|
|
1697
|
+
* is in flight right now."
|
|
1698
|
+
*
|
|
1699
|
+
* - `kind` — the `OngoingHandle.kind` reported by the in-flight
|
|
1700
|
+
* handle (e.g. `'marquee'`, `'move'`). `null` when no action is
|
|
1701
|
+
* in flight OR the handle didn't declare a kind.
|
|
1702
|
+
* - `id` — the dispatcher's internal `gestureId` (`pointer-1`,
|
|
1703
|
+
* `key-held-Space`, …) — the pointer/key channel the action rode
|
|
1704
|
+
* in on. `null` when no action is in flight.
|
|
1705
|
+
*
|
|
1706
|
+
* When multiple handles are in flight simultaneously (e.g. a key-held
|
|
1707
|
+
* action overlapping a pointer action), the most-recently-started
|
|
1708
|
+
* handle wins. This matches user intent: the latest interaction is
|
|
1709
|
+
* the one consumers care about.
|
|
1710
|
+
*
|
|
1711
|
+
* That rule used to be near-vacuous on the pointer side, because every
|
|
1712
|
+
* pointer shared one handle slot and two pointer drags could not coexist.
|
|
1713
|
+
* With per-pointer keying they can — but only via paths that bypass the
|
|
1714
|
+
* multi-pointer policy in `useGestureDispatcher` (which stops a second
|
|
1715
|
+
* finger from opening a drag while a pinch is live), such as a mouse and
|
|
1716
|
+
* a pen used together. Latest-start remains the right answer there.
|
|
1717
|
+
*/
|
|
1718
|
+
getActiveAction(): {
|
|
1719
|
+
kind: string | null;
|
|
1720
|
+
id: string | null;
|
|
1721
|
+
};
|
|
1722
|
+
/**
|
|
1723
|
+
* Start an ongoing action driven by UI (not a gesture). Builds an
|
|
1724
|
+
* `InvocationCtx` with the given `deps` and `params`, calls
|
|
1725
|
+
* `action.invoker.start(ctx, { params })`, and registers the returned
|
|
1726
|
+
* handle in the in-flight map so `getInFlightHandles()` reports it —
|
|
1727
|
+
* enabling preview rendering via `SceneCanvas`.
|
|
1728
|
+
*
|
|
1729
|
+
* Returns `null` if `actionId` is unknown, the action's invoker is not
|
|
1730
|
+
* ongoing, or `start` returned an empty handle.
|
|
1731
|
+
*
|
|
1732
|
+
* If a UI-driven handle for the same `actionId` is already in flight,
|
|
1733
|
+
* it is committed (`end('commit')`) before the new one starts.
|
|
1734
|
+
*/
|
|
1735
|
+
beginUiOngoing(actionId: string, deps: ActionDeps, params?: Record<string, unknown>): UiOngoingControl | null;
|
|
1736
|
+
}
|
|
1737
|
+
/** Build a dispatcher. `getAction` overrides how action ids are resolved;
|
|
1738
|
+
* by default the `DispatcherContext`'s registry is used. */
|
|
1739
|
+
declare function createDispatcher(opts?: {
|
|
1740
|
+
getAction?: (id: string) => Action | undefined;
|
|
1741
|
+
}): Dispatcher;
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* @experimental
|
|
1745
|
+
* Partial override or full descriptor passed via `<SceneCanvas actions={...}>`.
|
|
1746
|
+
* `null` disables a default at this id.
|
|
1747
|
+
*/
|
|
1748
|
+
type ActionEntry = null | Partial<Action> | Action;
|
|
1749
|
+
/**
|
|
1750
|
+
* @experimental
|
|
1751
|
+
* Shape of the `actions` prop on `<SceneCanvas>`. `null` disables all defaults.
|
|
1752
|
+
*/
|
|
1753
|
+
type ActionsProp = null | Record<string, ActionEntry>;
|
|
1754
|
+
/**
|
|
1755
|
+
* @experimental
|
|
1756
|
+
* Imperative API exposed by `useActionsRegistry()`.
|
|
1757
|
+
*/
|
|
1758
|
+
interface ActionsRegistry {
|
|
1759
|
+
/** Add an action and return its release. Registrants for one id stack,
|
|
1760
|
+
* newest live, so releasing yours uncovers whoever you displaced. Call it
|
|
1761
|
+
* from an effect and release it in that effect's cleanup — registering from
|
|
1762
|
+
* a render body pushes an entry per render and releases none. */
|
|
1763
|
+
register(action: Action): () => void;
|
|
1764
|
+
/** Drop every registrant of `id`. This is the "this action should not exist"
|
|
1765
|
+
* door, not a release — for that, call what `register` returned. */
|
|
1766
|
+
unregister(id: string): void;
|
|
1767
|
+
/** Declare `id` not for this scope: it stays registered, and every other
|
|
1768
|
+
* scope over the same store still resolves it, but here it lists as absent
|
|
1769
|
+
* and neither `trigger` nor `begin` will fire it. Returns a release; an
|
|
1770
|
+
* `<ActionsScope>` drops what it muted when it unmounts. This is the
|
|
1771
|
+
* "not for me" door — `unregister` is the "should not exist" one. */
|
|
1772
|
+
mute(id: string): () => void;
|
|
1773
|
+
list(): readonly Action[];
|
|
1774
|
+
/** Fire an immediate-invoker action by id. The optional `params` arg is
|
|
1775
|
+
* forwarded to `ImmediateInvoker.run` as its second argument — use it for
|
|
1776
|
+
* parametric actions (e.g. `trigger('tool.activate', { toolId: 'rect' })`).
|
|
1777
|
+
* Ongoing-invoker actions are not reachable from `trigger`. */
|
|
1778
|
+
trigger(id: string, params?: Record<string, unknown>): boolean;
|
|
1779
|
+
/**
|
|
1780
|
+
* Subscribe to registry mutations. The callback fires after any
|
|
1781
|
+
* `register`/`unregister` that changes the version. Returns an
|
|
1782
|
+
* unsubscribe function. Designed for `useSyncExternalStore`-driven
|
|
1783
|
+
* surfaces (e.g. `<ActionBar>` in `@weasel-js/ui`) that need to
|
|
1784
|
+
* re-render when the action set changes.
|
|
1785
|
+
*/
|
|
1786
|
+
subscribe(listener: () => void): () => void;
|
|
1787
|
+
/**
|
|
1788
|
+
* Start an ongoing action driven by UI (color picker, opacity slider).
|
|
1789
|
+
* Returns a control object with `update(params)` and `end(reason)`.
|
|
1790
|
+
*
|
|
1791
|
+
* Returns `null` if no dispatcher is wired into this registry, the
|
|
1792
|
+
* action is unknown, or its invoker is not ongoing.
|
|
1793
|
+
*
|
|
1794
|
+
* See `Dispatcher.beginUiOngoing` for full semantics including
|
|
1795
|
+
* auto-commit when a prior UI handle for the same action is in flight.
|
|
1796
|
+
*/
|
|
1797
|
+
begin(id: string, params?: Record<string, unknown>): UiOngoingControl | null;
|
|
1798
|
+
/** Wire a dispatcher into the registry so `begin()` can delegate to it.
|
|
1799
|
+
* Returns a release that clears the slot only while this dispatcher still
|
|
1800
|
+
* holds it: a canvas displaced by a later one must not take input away from
|
|
1801
|
+
* the canvas now on screen. Call with `null` to detach unconditionally. */
|
|
1802
|
+
setDispatcher(d: Dispatcher | null): () => void;
|
|
1803
|
+
/** Wire a `DepRegistry` into the registry so `trigger()` / `begin()` can
|
|
1804
|
+
* resolve action deps even when this provider is mounted ABOVE the dep
|
|
1805
|
+
* registry (e.g. a consumer's root `<ActionsProvider>` reused by
|
|
1806
|
+
* SceneCanvas's `ActionsProviderIfRoot`). Takes precedence over the dep
|
|
1807
|
+
* registry read from context at the provider's own level. Call with
|
|
1808
|
+
* `null` to detach unconditionally; the returned release clears the slot
|
|
1809
|
+
* only while this registry still holds it. */
|
|
1810
|
+
setDepRegistry(r: DepRegistry | null): () => void;
|
|
1811
|
+
}
|
|
1812
|
+
/** Validate that `id` is usable as an action id in the route grammar.
|
|
1813
|
+
* Rejects ids that start with a reserved sigil (would shadow future
|
|
1814
|
+
* grammar extensions) and ids that collide with phase keywords (would
|
|
1815
|
+
* parse as the bare-phase shorthand). Mirrors `defineTool`'s tool-id
|
|
1816
|
+
* validation. */
|
|
1817
|
+
declare function validateActionId(id: string): void;
|
|
1818
|
+
/** Reject any '&'-channel phase atom in an Action's defaultBinding —
|
|
1819
|
+
* actions have no owning tool, so '&' can't resolve. Tool-side bindings
|
|
1820
|
+
* (Tool.bindings) are the right place for '&' atoms. */
|
|
1821
|
+
declare function validateActionDefaultBinding(action: Action): void;
|
|
1822
|
+
|
|
1823
|
+
/** Canonical id of the consolidated tool-offhand action. One descriptor
|
|
1824
|
+
* serves every tool; the matched binding supplies `params.toolId` to select
|
|
1825
|
+
* which tool to engage while the key is held. */
|
|
1826
|
+
declare const TOOL_OFFHAND_ID = "tool.offhand";
|
|
1827
|
+
/** Per-tool spec consumed by `buildToolOffhandBindings`. The key is the
|
|
1828
|
+
* held-key trigger (e.g. ' ' for Space-for-hand). */
|
|
1829
|
+
interface ToolOffhandBindingSpec {
|
|
1830
|
+
toolId: string;
|
|
1831
|
+
key: string | string[];
|
|
1832
|
+
}
|
|
1833
|
+
/** `KeyboardEvent.key` for a declared `Eligibility.offhand` trigger. Only
|
|
1834
|
+
* `space` is exercised today; the modifier arms follow the DOM's names. */
|
|
1835
|
+
declare function offhandKeyFor(trigger: HotkeyTrigger): string;
|
|
1836
|
+
/** Build the per-tool `BoundGesture` list for the consolidated action's
|
|
1837
|
+
* `defaultBinding`. Each entry pairs a `key-held` spec with
|
|
1838
|
+
* `opts.params.toolId` so the invoker's `start` can pick out which tool
|
|
1839
|
+
* to engage. */
|
|
1840
|
+
declare function buildToolOffhandBindings(specs: readonly ToolOffhandBindingSpec[]): BoundGesture[];
|
|
1841
|
+
/** Build the consolidated `tool.offhand` action — "hold a key to engage a
|
|
1842
|
+
* secondary tool" (e.g. Space-for-hand). `bindings` enumerates one
|
|
1843
|
+
* `{ spec: keyHeld, opts: { params: { toolId } } }` per tool; the invoker
|
|
1844
|
+
* reads `params.toolId` from the matched binding, pushes the tool id onto
|
|
1845
|
+
* the active-tool context's hotkey stack on `start`, and pops it on `onEnd`.
|
|
1846
|
+
* Mirrors the parametric shape of `tool.activate`. */
|
|
1847
|
+
declare function makeToolOffhandAction(bindings: BoundGesture[]): Action;
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* Named predicates for `defaultBinding.target.kindOf`. Each matches the
|
|
1851
|
+
* existing `kindOf` signature `(target: unknown, bodyTarget?: string) => boolean`
|
|
1852
|
+
* so they drop into action descriptors without a wrapper:
|
|
1853
|
+
*
|
|
1854
|
+
* target: { kindOf: isAnchor }
|
|
1855
|
+
*
|
|
1856
|
+
* Single source of truth for "what does this affordance kind mean" — adding
|
|
1857
|
+
* a new predicate here is what enables every action that needs the same
|
|
1858
|
+
* shape to read it. Drift prevention.
|
|
1859
|
+
*/
|
|
1860
|
+
/** A predicate that resolves from `bodyTarget` alone. The exclusive-claim
|
|
1861
|
+
* filter reads `readsAffordance`: without it a body predicate looks like it
|
|
1862
|
+
* consults the hit, and survives a claim that should have barred it. */
|
|
1863
|
+
type BodyPredicate = ((target: unknown, bodyTarget?: string) => boolean) & {
|
|
1864
|
+
readsAffordance: false;
|
|
1865
|
+
};
|
|
1866
|
+
/** Matches any body hit — selected or unselected node body. */
|
|
1867
|
+
declare const isBody: BodyPredicate;
|
|
1868
|
+
/** Body hit specifically already in the selection. */
|
|
1869
|
+
declare const isSelectedBody: BodyPredicate;
|
|
1870
|
+
/** Body hit specifically NOT in the selection. */
|
|
1871
|
+
declare const isUnselectedBody: BodyPredicate;
|
|
1872
|
+
/** Empty-canvas hit — no node beneath cursor. */
|
|
1873
|
+
declare const isEmpty: BodyPredicate;
|
|
1874
|
+
/** Any corner resize handle — top-left/top-right/bottom-left/bottom-right. */
|
|
1875
|
+
declare const isResizeHandle: (target: unknown) => boolean;
|
|
1876
|
+
/** The rotate-around-center handle above the bounding box. */
|
|
1877
|
+
declare const isRotateHandle: (target: unknown) => boolean;
|
|
1878
|
+
/** Any path anchor (the on-curve dot). */
|
|
1879
|
+
declare const isAnchor: (target: unknown) => boolean;
|
|
1880
|
+
/** Either control handle (controlIn or controlOut). */
|
|
1881
|
+
declare const isControlHandle: (target: unknown) => boolean;
|
|
1882
|
+
/** Any of the three anchor-editing affordances (anchor + both controls). */
|
|
1883
|
+
declare const isAnchorOrControl: (target: unknown) => boolean;
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* The DOM half of a drag: one pointer, held down, from press to release.
|
|
1887
|
+
*
|
|
1888
|
+
* Every pointerdown-to-pointerup lifecycle in the kit used to answer capture,
|
|
1889
|
+
* pointer identity, teardown and lost-pointer recovery for itself, and each
|
|
1890
|
+
* answered them differently. This is the one place those are decided; anything
|
|
1891
|
+
* above it — thresholds, coordinate spaces, what the drag *means* — belongs to
|
|
1892
|
+
* its caller.
|
|
1893
|
+
*/
|
|
1894
|
+
/** Why a session ended without the pointer being released. */
|
|
1895
|
+
type PointerSessionCancelReason =
|
|
1896
|
+
/** The browser cancelled the pointer (touch interrupted, palm rejection). */
|
|
1897
|
+
'pointercancel'
|
|
1898
|
+
/** The origin left the document mid-gesture, taking capture with it. */
|
|
1899
|
+
| 'lostcapture'
|
|
1900
|
+
/** `cancel()` — an unmount, a window blur, Escape, a consumer's own rule. */
|
|
1901
|
+
| 'aborted'
|
|
1902
|
+
/** A new press arrived on this pointer, so the tracked one had ended. */
|
|
1903
|
+
| 'superseded';
|
|
1904
|
+
interface PointerSessionCallbacks {
|
|
1905
|
+
/** Every move belonging to this pointer, while it is still held. */
|
|
1906
|
+
onMove?: (e: PointerEvent) => void;
|
|
1907
|
+
/** The pointer was released. Fires once, before teardown completes. */
|
|
1908
|
+
onEnd?: (e: PointerEvent) => void;
|
|
1909
|
+
/** The session ended without a release. Fires once. */
|
|
1910
|
+
onCancel?: (reason: PointerSessionCancelReason) => void;
|
|
1911
|
+
}
|
|
1912
|
+
interface PointerSessionOptions {
|
|
1913
|
+
/** Ask the origin element to capture the pointer. Default `true`. Capture
|
|
1914
|
+
* keeps hover and click off everything the drag passes over; the session
|
|
1915
|
+
* does not depend on it to keep receiving events. */
|
|
1916
|
+
capture?: boolean;
|
|
1917
|
+
}
|
|
1918
|
+
interface PointerSession {
|
|
1919
|
+
readonly pointerId: number;
|
|
1920
|
+
/** False once the session has ended, by any route. */
|
|
1921
|
+
readonly active: boolean;
|
|
1922
|
+
/** End the session now, reporting `'aborted'`. Idempotent. */
|
|
1923
|
+
cancel: () => void;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Open a session for the pointer that `down` belongs to.
|
|
1927
|
+
*
|
|
1928
|
+
* Listens on the origin's document rather than on the element: a captured
|
|
1929
|
+
* element that is removed mid-drag stops receiving events, and every listener
|
|
1930
|
+
* hung on it goes with it. Three recovery rules close the gaps a plain
|
|
1931
|
+
* pointerup/pointercancel pair leaves — losing capture cancels only once the
|
|
1932
|
+
* origin has left the document, a move reporting no held button is read as
|
|
1933
|
+
* the release that never arrived, and a fresh press on the same pointer says
|
|
1934
|
+
* the tracked one had already ended.
|
|
1935
|
+
*/
|
|
1936
|
+
declare function openPointerSession(origin: Element, down: PointerEvent | React.PointerEvent, cb: PointerSessionCallbacks, opts?: PointerSessionOptions): PointerSession;
|
|
1937
|
+
|
|
1938
|
+
/** What the registry knows at dispatch time. */
|
|
1939
|
+
interface EligibilityState {
|
|
1940
|
+
focusedId: string | null;
|
|
1941
|
+
heldTriggers: ReadonlySet<string>;
|
|
1942
|
+
/** Ids the host reports as hotkey-engaged. A declared `offhand` registers
|
|
1943
|
+
* the binding, but `tool.offhand`'s invoker still reports engagement by
|
|
1944
|
+
* pushing an id — this retires only if that contract changes. */
|
|
1945
|
+
engagedIds?: ReadonlySet<string>;
|
|
1946
|
+
/** Whether the active mode allows these capability tags. Omitted → allow. */
|
|
1947
|
+
allows?: (tags: readonly CapabilityTag[]) => boolean;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* The scope tier an entry's bindings are live at, or null when none are.
|
|
1951
|
+
* Ordered hotkey > active > ambient to match the dispatcher's own walk.
|
|
1952
|
+
*/
|
|
1953
|
+
declare function liveScope(id: string, eligibility: Eligibility, state: EligibilityState): BindingScope | null;
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* Every binding the entry set contributes right now, each tiered by the
|
|
1957
|
+
* entry's own declared eligibility. Entries with no live tier are omitted.
|
|
1958
|
+
*
|
|
1959
|
+
* Order is preserved: `matchSorted` sorts by specificity within a scope and
|
|
1960
|
+
* breaks ties by declaration order, so the caller's entry order decides which
|
|
1961
|
+
* of two same-specificity bindings in one tier wins.
|
|
1962
|
+
*/
|
|
1963
|
+
declare function scopeBindings(entries: Iterable<Contribution>, state: EligibilityState): ScopedBinding[];
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Concatenate contribution bundles into one registry, preserving order —
|
|
1967
|
+
* entry order decides which of two same-specificity bindings in one scope
|
|
1968
|
+
* tier wins, so it is part of the result, not an accident of it.
|
|
1969
|
+
*
|
|
1970
|
+
* Throws on a duplicate id rather than dropping one: a feature silently
|
|
1971
|
+
* losing its bindings is the failure this registry exists to make loud.
|
|
1972
|
+
*/
|
|
1973
|
+
declare function mergeContributions(...bundles: readonly Contribution[][]): Contribution[];
|
|
1974
|
+
|
|
1975
|
+
/**
|
|
1976
|
+
* What a tool declares.
|
|
1977
|
+
*
|
|
1978
|
+
* A tool is a declarative shell, not an event handler: it names the gestures
|
|
1979
|
+
* it responds to and, for each, the action to invoke. The dispatcher owns the
|
|
1980
|
+
* gesture, and the action owns the preview and the commit — so a tool
|
|
1981
|
+
* definition is mostly `bindings`, plus presentation and any actions the tool
|
|
1982
|
+
* itself introduces.
|
|
1983
|
+
*/
|
|
1984
|
+
interface ToolDef<TScratch = void, TOverlay = unknown> {
|
|
1985
|
+
id: string;
|
|
1986
|
+
/** Capability tags for modality eligibility. Forwarded onto `Tool.capabilities`. */
|
|
1987
|
+
capabilities?: CapabilityTag[];
|
|
1988
|
+
/**
|
|
1989
|
+
* Actions this tool owns and needs registered while it is in the tools
|
|
1990
|
+
* registry — e.g. polygon's `polygon.adjustSides`, which its own bindings
|
|
1991
|
+
* reference by id.
|
|
1992
|
+
*
|
|
1993
|
+
* Declared here rather than registered by the hook with `useAction`,
|
|
1994
|
+
* because tool hooks run wherever the consumer calls them — for
|
|
1995
|
+
* `<SceneCanvas>` that is ABOVE `<ActionsProviderIfRoot>`, where
|
|
1996
|
+
* `useActionsRegistry()` returns null and `useAction` silently no-ops. The
|
|
1997
|
+
* result was a binding pointing at an action id nothing had registered, so
|
|
1998
|
+
* the gesture fell through to whatever matched next (polygon's
|
|
1999
|
+
* wheel/arrow-key side adjustment did nothing and `nudge.*` moved the
|
|
2000
|
+
* selection instead). `<ToolActionsMounter>` registers these from inside
|
|
2001
|
+
* the provider.
|
|
2002
|
+
*/
|
|
2003
|
+
actions?: Action[];
|
|
2004
|
+
/** Hook name as exported from the kit barrel (e.g. `'useHandTool'`).
|
|
2005
|
+
* Set by built-in hooks for inspector / debugging. Consumer-authored
|
|
2006
|
+
* tools may set this to surface their hook name; omitted is fine.
|
|
2007
|
+
* Introspection-only — do not make this load-bearing in production.
|
|
2008
|
+
* Read off the def via `Tool.def` (the reflection escape hatch). */
|
|
2009
|
+
hookName?: string;
|
|
2010
|
+
presentation?: ToolPresentation<TScratch>;
|
|
2011
|
+
/** Optional caller-supplied activation key. Most built-in tools have their
|
|
2012
|
+
* activation key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`;
|
|
2013
|
+
* this field is for tools that want their activation key to be
|
|
2014
|
+
* configurable by the host (currently Lasso and Eyedropper). The dynamic
|
|
2015
|
+
* loop in `useKeybindings.ts` picks this up and appends a binding entry
|
|
2016
|
+
* to the consolidated `tool.activate` action (with `opts.params.toolId`
|
|
2017
|
+
* set so the invoker knows which tool to switch to). */
|
|
2018
|
+
keybinding?: ToolKeybinding;
|
|
2019
|
+
/** Held-key trigger: this tool engages while the key is down and
|
|
2020
|
+
* disengages on release. Carried onto `Tool.eligibility.offhand`, which
|
|
2021
|
+
* assembly reads to register the consolidated `tool.offhand` action — the
|
|
2022
|
+
* declaration is the wiring, with nothing for the host to do. */
|
|
2023
|
+
hotkey?: HotkeyTrigger;
|
|
2024
|
+
onActivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
2025
|
+
onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
2026
|
+
cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
|
|
2027
|
+
/** Override the default scratch initializer. Default is `() => null`
|
|
2028
|
+
* cast to `TScratch`, which works for tools whose scratch is fresh
|
|
2029
|
+
* every gesture. Tools that need scratch identity to survive across
|
|
2030
|
+
* gesture boundaries (e.g. the pen tool's multi-click subpath state)
|
|
2031
|
+
* pass a stable-ref-returning thunk here. The factory forwards this
|
|
2032
|
+
* onto the returned `Tool.initScratch`. */
|
|
2033
|
+
initScratch?: () => TScratch;
|
|
2034
|
+
/** Declarative gesture bindings, forwarded onto `Tool.bindings`. The
|
|
2035
|
+
* gesture dispatcher consults these at active scope while this tool is the
|
|
2036
|
+
* active one, and at hotkey scope while it is held. This is the tool's
|
|
2037
|
+
* entire input surface — the `initial` / `engaged` phase tables that used
|
|
2038
|
+
* to sit beside it are gone, along with the second dispatcher that read
|
|
2039
|
+
* them. */
|
|
2040
|
+
bindings?: GestureBinding[];
|
|
2041
|
+
/** Optional overlay layer rendered while the tool occupies any slot
|
|
2042
|
+
* (active, hotkey, or ambient), surfaced on `Tool.overlay`.
|
|
2043
|
+
*
|
|
2044
|
+
* The layer's `draw` closure should read dynamic state through refs or
|
|
2045
|
+
* closures captured in the enclosing render scope, and gate on it there —
|
|
2046
|
+
* `if (!scratch.something) return []` — rather than expecting the kit to
|
|
2047
|
+
* swap layers as the gesture progresses.
|
|
2048
|
+
*
|
|
2049
|
+
* Pass an array to contribute several layers; they render in order. */
|
|
2050
|
+
overlay?: TOverlay | TOverlay[];
|
|
2051
|
+
/** Where `overlay` sits relative to the selection chrome. Defaults to
|
|
2052
|
+
* `'top'`, above everything. */
|
|
2053
|
+
overlayPosition?: OverlayPosition;
|
|
2054
|
+
}
|
|
2055
|
+
/** Viewport-tool spec. Once phase tables went away this stopped differing
|
|
2056
|
+
* from `ToolDef` in any structural way; `defineViewportTool` survives as the
|
|
2057
|
+
* authoring signal that a tool pans/zooms the view rather than the scene. */
|
|
2058
|
+
type ViewportToolDef<TScratch = void, TOverlay = unknown> = ToolDef<TScratch, TOverlay>;
|
|
2059
|
+
|
|
2060
|
+
/**
|
|
2061
|
+
* Build a `Tool<TScratch>` from a declarative `ToolDef<TScratch>`.
|
|
2062
|
+
*
|
|
2063
|
+
* This used to be a translator: `ToolDef` carried `initial` / `engaged` phase
|
|
2064
|
+
* tables of hit-keyed route handlers, and `defineTool` compiled them into the
|
|
2065
|
+
* imperative `pointer` / `drag` / `keyboard` / `wheel` handlers the
|
|
2066
|
+
* tool-routing dispatcher called. Both ends of that are gone — tools declare
|
|
2067
|
+
* `bindings` and the gesture dispatcher routes them — so what remains is
|
|
2068
|
+
* identity plumbing plus the id check and the cursor resolver.
|
|
2069
|
+
*
|
|
2070
|
+
* It stays a function rather than becoming a spread because the id validation
|
|
2071
|
+
* and the `initScratch` / `cursor` defaults are worth applying uniformly, and
|
|
2072
|
+
* because `Tool.def` gives reflection a handle on the authored form.
|
|
2073
|
+
*/
|
|
2074
|
+
declare function defineTool<TScratch = void, TOverlay = unknown>(def: ToolDef<TScratch, TOverlay>): Tool<TScratch, TOverlay>;
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Define a tool that acts on the viewport rather than the scene.
|
|
2078
|
+
*
|
|
2079
|
+
* This used to do real work: `ViewportPhaseDef` was a narrowed `PhaseDef`
|
|
2080
|
+
* (no click routes, drag restricted to the function form), and the factory
|
|
2081
|
+
* lifted it back to the permissive shape before handing it to `defineTool`.
|
|
2082
|
+
* With phase tables gone there is no shape left to narrow — a viewport tool
|
|
2083
|
+
* declares `bindings` like any other, pointing at `viewport.*` actions.
|
|
2084
|
+
*
|
|
2085
|
+
* It survives as an authoring signal. `defineViewportTool` at the top of a
|
|
2086
|
+
* hook says "this tool moves the camera, not the drawing", which is worth
|
|
2087
|
+
* more than the type gymnastics it replaced.
|
|
2088
|
+
*/
|
|
2089
|
+
declare function defineViewportTool<TScratch = void, TOverlay = unknown>(def: ViewportToolDef<TScratch, TOverlay>): Tool<TScratch, TOverlay>;
|
|
2090
|
+
|
|
2091
|
+
/**
|
|
2092
|
+
* One row in the route registry — a single `GestureBinding` on one tool,
|
|
2093
|
+
* flattened into the route grammar's vocabulary so the inspector, the
|
|
2094
|
+
* conflict checker, and `describeRoute` can all read the same shape.
|
|
2095
|
+
*
|
|
2096
|
+
* Multiple rows can share (gesture, arg, target, modifiers) if different tools
|
|
2097
|
+
* declare them; consumers walk the list and group client-side.
|
|
2098
|
+
*/
|
|
2099
|
+
interface RegistryEntry {
|
|
2100
|
+
toolId: string;
|
|
2101
|
+
/** Which phase the binding's `phase` spec restricts it to. `'any'` when it
|
|
2102
|
+
* declares none, declares `'*'`, or declares an atom list whose atoms
|
|
2103
|
+
* don't agree on one phase — such a binding fires in either phase, and
|
|
2104
|
+
* reporting it as `'initial'` made it collide with genuinely-initial
|
|
2105
|
+
* bindings in `findConflicts`' bucket key. */
|
|
2106
|
+
phase: 'initial' | 'engaged' | 'any';
|
|
2107
|
+
/** Structured v3 modifier requirements. Empty object = "no modifiers
|
|
2108
|
+
* held" (the strict default). */
|
|
2109
|
+
modifiers: ParsedModifiers;
|
|
2110
|
+
gesture: GestureName;
|
|
2111
|
+
/** Resolved arg value for arg-bearing gestures (wheel direction,
|
|
2112
|
+
* key name, multiTouchTap fingers). Undefined for gestures whose
|
|
2113
|
+
* descriptor has no `arg`. */
|
|
2114
|
+
arg: string | undefined;
|
|
2115
|
+
/** Every arg value this binding actually answers to, when the spec names
|
|
2116
|
+
* more than one — `key: ['Delete','Backspace']` answers to both, and
|
|
2117
|
+
* `matchKey` matches any member case-insensitively. `arg` collapses them
|
|
2118
|
+
* into one display token, which is right for a route string and wrong for
|
|
2119
|
+
* deciding whether two bindings collide. Absent when there is only one. */
|
|
2120
|
+
argAlternatives?: readonly string[];
|
|
2121
|
+
/** Target class for hit-testing gestures. `undefined` when the descriptor
|
|
2122
|
+
* has `hasTarget: false` or the spec declares no target;
|
|
2123
|
+
* {@link PREDICATE_TARGET} when the spec uses a `kindOf` predicate, which
|
|
2124
|
+
* the route grammar has no notation for. */
|
|
2125
|
+
target: string | undefined;
|
|
2126
|
+
/** The action this binding fires. */
|
|
2127
|
+
actionId: string;
|
|
2128
|
+
/** The `GestureSpec` this row was flattened from, by reference. Reflection
|
|
2129
|
+
* consumers that need something the grammar doesn't capture — the
|
|
2130
|
+
* specificity tuple, a `kindOf` predicate identity — read it here rather
|
|
2131
|
+
* than re-walking `Tool.bindings`. */
|
|
2132
|
+
spec: GestureSpec;
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Stand-in target for a `{ kindOf }` predicate spec.
|
|
2136
|
+
*
|
|
2137
|
+
* The route grammar can name target *classes* (`empty`, `selected-body`,
|
|
2138
|
+
* `kind:rect`) but not arbitrary predicates, so three distinct select-tool
|
|
2139
|
+
* bindings all render as this one token. Narrowing it would mean giving the
|
|
2140
|
+
* grammar a way to describe a function, which is a real design question and
|
|
2141
|
+
* not one this reflection layer should answer by inventing syntax.
|
|
2142
|
+
*/
|
|
2143
|
+
declare const PREDICATE_TARGET = "predicate";
|
|
2144
|
+
/**
|
|
2145
|
+
* Flatten every tool's `bindings` into route-registry rows.
|
|
2146
|
+
*
|
|
2147
|
+
* This was `buildActionRegistry`, and it walked `ToolDef.initial` /
|
|
2148
|
+
* `.engaged` phase tables — the grammar that no longer exists. It was also
|
|
2149
|
+
* blind to `Tool.bindings` the entire time the two lived side by side, which
|
|
2150
|
+
* is why the inspector under-reported select by more than half. The rename
|
|
2151
|
+
* fixes a second thing: it never had anything to do with the Actions
|
|
2152
|
+
* Registry, and reading `buildActionRegistry` next to `ActionsRegistry`
|
|
2153
|
+
* suggested otherwise.
|
|
2154
|
+
*/
|
|
2155
|
+
declare function buildRouteRegistry(tools: readonly Tool<unknown>[]): RegistryEntry[];
|
|
2156
|
+
/** The route-grammar gesture a `GestureSpec.kind` routes as, or `undefined`
|
|
2157
|
+
* for kinds the grammar has no name for. */
|
|
2158
|
+
declare function routeGestureForSpecKind(kind: GestureSpec['kind']): GestureName | undefined;
|
|
2159
|
+
/**
|
|
2160
|
+
* Every route string one `GestureSpec` declares, in route-grammar notation.
|
|
2161
|
+
*
|
|
2162
|
+
* One string per arg alternative — a spec bound to `['ArrowUp','ArrowDown']`
|
|
2163
|
+
* yields two routes, because each is separately conflictable and separately
|
|
2164
|
+
* readable. Returns `[]` for spec kinds the grammar can't name.
|
|
2165
|
+
*
|
|
2166
|
+
* This is the display counterpart to {@link buildRouteRegistry}: same
|
|
2167
|
+
* projection of a spec onto the grammar, rendered as text instead of as a
|
|
2168
|
+
* conflict-checkable row.
|
|
2169
|
+
*/
|
|
2170
|
+
declare function routesForSpec(spec: GestureSpec): readonly string[];
|
|
2171
|
+
|
|
2172
|
+
/** Two or more tools declare the same exact (phase, gesture, arg, target,
|
|
2173
|
+
* modifiers) tuple — the dispatcher's slot precedence picks one
|
|
2174
|
+
* arbitrarily (well, deterministically by slot order, but the author
|
|
2175
|
+
* probably didn't intend the duplication). */
|
|
2176
|
+
interface Conflict {
|
|
2177
|
+
phase: 'initial' | 'engaged' | 'any';
|
|
2178
|
+
gesture: GestureName;
|
|
2179
|
+
arg: string | undefined;
|
|
2180
|
+
target: string | undefined;
|
|
2181
|
+
modifiers: ParsedModifiers;
|
|
2182
|
+
/** All tool ids that registered the same tuple. At least 2 by
|
|
2183
|
+
* construction. Order matches the input tools[] order. */
|
|
2184
|
+
toolIds: string[];
|
|
2185
|
+
}
|
|
2186
|
+
/** Detect exact-tuple overlaps across a tool registration set.
|
|
2187
|
+
*
|
|
2188
|
+
* Intentionally NOT flagged:
|
|
2189
|
+
* - Broad vs. narrow targets (e.g. an untargeted `click` alongside
|
|
2190
|
+
* `click` on `empty`) — the dispatcher's specificity ordering resolves
|
|
2191
|
+
* those cleanly, and the broad one is usually the intended fallback.
|
|
2192
|
+
* - Different modifier requirements on the same target — they fire on
|
|
2193
|
+
* different inputs.
|
|
2194
|
+
* - A binding whose action declines via `enabled()` so a lower-priority
|
|
2195
|
+
* one can take the gesture. Detecting that intent would mean evaluating
|
|
2196
|
+
* the action; consumers can suppress known-intentional compositions in
|
|
2197
|
+
* their UI layer.
|
|
2198
|
+
*
|
|
2199
|
+
* - Two `{ kindOf }` predicate targets. The route grammar renders every
|
|
2200
|
+
* predicate as the single token {@link PREDICATE_TARGET}, so bucketing on
|
|
2201
|
+
* the rendered target alone reported select's `resize` / `rotate` / `move`
|
|
2202
|
+
* drags — three genuinely different predicates — as a three-way conflict.
|
|
2203
|
+
* Predicate entries bucket by function identity instead, which means two
|
|
2204
|
+
* *separately written but equivalent* predicates go unflagged. That's the
|
|
2205
|
+
* right way to be wrong here: this check has to be silent when nothing is
|
|
2206
|
+
* wrong or nobody will keep it on.
|
|
2207
|
+
*
|
|
2208
|
+
* Note that two bindings sharing a tuple on the SAME tool are now possible
|
|
2209
|
+
* (bindings are an array, where phase tables were objects with unique
|
|
2210
|
+
* keys) — so a conflict may name one tool twice.
|
|
2211
|
+
*
|
|
2212
|
+
* This is the raw same-tuple detector. Feeding it a whole tool *registry*
|
|
2213
|
+
* over-reports, because registry tools take turns in the active slot and
|
|
2214
|
+
* can't collide with each other — see {@link findScopedConflicts}.
|
|
2215
|
+
*/
|
|
2216
|
+
declare function findConflicts(tools: readonly Tool<unknown>[]): Conflict[];
|
|
2217
|
+
/**
|
|
2218
|
+
* The tool scopes as the dispatcher sees them — which is what decides whether
|
|
2219
|
+
* two same-tuple bindings can actually collide.
|
|
2220
|
+
*/
|
|
2221
|
+
interface ToolScopes {
|
|
2222
|
+
/** Tools eligible for the active slot, keyed by id or as a flat list. */
|
|
2223
|
+
registry: readonly Tool<unknown>[] | Readonly<Record<string, Tool<unknown>>>;
|
|
2224
|
+
/** Always-on tools. Every one of these is live at once. */
|
|
2225
|
+
ambient?: readonly Tool<unknown>[];
|
|
2226
|
+
/** Registered actions. Their `defaultBinding`s assemble at ambient scope
|
|
2227
|
+
* (hotkey scope when `Action.scope` says so), alongside the tools above. */
|
|
2228
|
+
actions?: readonly Action[];
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Detect reachable route conflicts in an assembled tool set and report each one.
|
|
2232
|
+
*
|
|
2233
|
+
* This is the wiring `findConflicts` spent its first life without: the kit
|
|
2234
|
+
* could detect the one class of genuine routing ambiguity it has and never
|
|
2235
|
+
* looked. Call it once wherever a tool set is assembled (`useTools` does),
|
|
2236
|
+
* behind a `process.env.NODE_ENV !== 'production'` guard.
|
|
2237
|
+
*
|
|
2238
|
+
* **Warn, never throw.** A conflict between a consumer's tool and a kit tool
|
|
2239
|
+
* is a design question — sometimes deliberate, since the loser can still take
|
|
2240
|
+
* the gesture by declining through `enabled()` — and exploding in a running
|
|
2241
|
+
* app is the wrong way to raise it. A conflict between two *kit* tools is
|
|
2242
|
+
* always a bug, but the place to fail on that is the kit's own test suite
|
|
2243
|
+
* (`canvas/SceneCanvas.routeConflicts.test.tsx`), not a consumer's console.
|
|
2244
|
+
*
|
|
2245
|
+
* @returns The conflicts found, so callers can dedupe repeat reports.
|
|
2246
|
+
*/
|
|
2247
|
+
declare function reportRouteConflicts(scopes: ToolScopes, warn?: (message: string) => void): Conflict[];
|
|
2248
|
+
|
|
2249
|
+
/**
|
|
2250
|
+
* Pure keyboard-binding helpers shared between the tool dispatcher
|
|
2251
|
+
* (`useKeybindings`), the gesture dispatcher (`useGestureDispatcher`), and
|
|
2252
|
+
* individual Tool declarations.
|
|
2253
|
+
*
|
|
2254
|
+
* Extracted from the deleted singular `useKeybinding` hook so the remaining
|
|
2255
|
+
* consumers don't depend on its disappearance.
|
|
2256
|
+
*/
|
|
2257
|
+
/**
|
|
2258
|
+
* @internal — runtime shape consumed by `matchesKeyBinding`. Public tool authors
|
|
2259
|
+
* should use `ToolKeybinding` from `@weasel-js/core` for declaring
|
|
2260
|
+
* configurable activation keys.
|
|
2261
|
+
*/
|
|
2262
|
+
interface KeyBinding {
|
|
2263
|
+
/**
|
|
2264
|
+
* Key or list of keys to match. Compared case-insensitively against
|
|
2265
|
+
* `event.key`, so `'a'` matches both `'a'` and `'A'`.
|
|
2266
|
+
*/
|
|
2267
|
+
key: string | readonly string[];
|
|
2268
|
+
/**
|
|
2269
|
+
* Require Cmd (mac) or Ctrl (others). When `true`, exactly one of
|
|
2270
|
+
* `metaKey`/`ctrlKey` must be held. Default `false` (both forbidden).
|
|
2271
|
+
*/
|
|
2272
|
+
mod?: boolean;
|
|
2273
|
+
/** Require Alt. Default `false` (forbidden). */
|
|
2274
|
+
alt?: boolean;
|
|
2275
|
+
/**
|
|
2276
|
+
* Shift policy. `undefined`/`false` forbids shift, `true` requires
|
|
2277
|
+
* shift, `'optional'` allows either. Default `undefined`.
|
|
2278
|
+
*/
|
|
2279
|
+
shift?: boolean | 'optional';
|
|
2280
|
+
/** Skip when focus is in an editable element. Default `true`. */
|
|
2281
|
+
skipInEditable?: boolean;
|
|
2282
|
+
/** When `false`, the listener is not attached. Default `true`. */
|
|
2283
|
+
enabled?: boolean;
|
|
2284
|
+
/** Call `preventDefault` before the handler. Default `true`. */
|
|
2285
|
+
preventDefault?: boolean;
|
|
2286
|
+
}
|
|
2287
|
+
/** Returns true when the target is an input, textarea, or contenteditable element. */
|
|
2288
|
+
declare function isEditableTarget(target: EventTarget | null): boolean;
|
|
2289
|
+
/**
|
|
2290
|
+
* Pure matcher for `KeyBinding` against a `KeyboardEvent`. Only checks key +
|
|
2291
|
+
* modifier policy — does NOT apply the editable-target skip or
|
|
2292
|
+
* `preventDefault`. Shared by the tool dispatcher (`useKeybindings`) and the
|
|
2293
|
+
* gesture dispatcher so the rules can't drift.
|
|
2294
|
+
*/
|
|
2295
|
+
declare function matchesKeyBinding(e: KeyboardEvent, b: KeyBinding): boolean;
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* Typed keys for the gesture/behavior scratch store.
|
|
2299
|
+
*
|
|
2300
|
+
* Gestures expose `ctx.scratch: Record<string, unknown>` as a per-gesture
|
|
2301
|
+
* mutable bag that behaviors use to stash state across `onDown` / `onMove`
|
|
2302
|
+
* / `onEnd` calls. The store is `unknown`-valued because behaviors are
|
|
2303
|
+
* tool-agnostic — a single behavior may run under different tools with
|
|
2304
|
+
* different scratch shapes. Without a typing convention every read site
|
|
2305
|
+
* has to `(ctx.scratch as <some shape>)[KEY]`, and a writer changing the
|
|
2306
|
+
* shape can silently break readers with no compile-time signal.
|
|
2307
|
+
*
|
|
2308
|
+
* `ScratchKey<T>` is a phantom-typed string that carries its payload type
|
|
2309
|
+
* via a branded interface. `getScratch` and `setScratch` use the brand to
|
|
2310
|
+
* narrow / constrain at the typing layer; at runtime the key is just its
|
|
2311
|
+
* underlying string and the store remains a plain `Record`.
|
|
2312
|
+
*
|
|
2313
|
+
* @example
|
|
2314
|
+
* ```ts
|
|
2315
|
+
* // Module-level (or behavior-level) constant, shared by writer + reader:
|
|
2316
|
+
* const LASSO_VERTICES = scratchKey<readonly { x: number; y: number }[]>('lasso.vertices');
|
|
2317
|
+
*
|
|
2318
|
+
* // Writer (a tool's onMove):
|
|
2319
|
+
* setScratch(ctx.scratch, LASSO_VERTICES, vertices);
|
|
2320
|
+
*
|
|
2321
|
+
* // Reader (a behavior's onEnd):
|
|
2322
|
+
* const vertices = getScratch(ctx.scratch, LASSO_VERTICES) ?? [];
|
|
2323
|
+
* // ^? readonly { x: number; y: number }[] | undefined
|
|
2324
|
+
* ```
|
|
2325
|
+
*/
|
|
2326
|
+
/** Phantom-typed key for a scratch slot. The `__scratchKeyPayload` phantom
|
|
2327
|
+
* field carries the value type without occupying any runtime memory. */
|
|
2328
|
+
interface ScratchKey<T> {
|
|
2329
|
+
readonly name: string;
|
|
2330
|
+
/** Phantom — never read at runtime. Only present so TypeScript can
|
|
2331
|
+
* recover `T` at use sites. */
|
|
2332
|
+
readonly __scratchKeyPayload?: (_: T) => T;
|
|
2333
|
+
}
|
|
2334
|
+
/** Compatible scratch store shape. The kit's gesture context's `scratch`
|
|
2335
|
+
* field already satisfies this; consumers don't need to construct one. */
|
|
2336
|
+
type ScratchStore = Record<string, unknown>;
|
|
2337
|
+
/**
|
|
2338
|
+
* Make a typed scratch key. Use module-level constants so the same key
|
|
2339
|
+
* is shared by writer and reader.
|
|
2340
|
+
*
|
|
2341
|
+
* Namespace the name by behavior or feature to avoid collisions
|
|
2342
|
+
* (`'behavior.field'` is a good convention). Two keys with the same
|
|
2343
|
+
* `name` refer to the same slot at runtime — the type parameter is a
|
|
2344
|
+
* compile-time contract, not a uniqueness guarantee.
|
|
2345
|
+
*/
|
|
2346
|
+
declare function scratchKey<T>(name: string): ScratchKey<T>;
|
|
2347
|
+
/** Read a typed slot from the scratch store. Returns `undefined` if
|
|
2348
|
+
* nothing has been written under the key. */
|
|
2349
|
+
declare function getScratch<T>(store: ScratchStore, key: ScratchKey<T>): T | undefined;
|
|
2350
|
+
/** Write a value to a typed slot. Overwrites whatever was there. */
|
|
2351
|
+
declare function setScratch<T>(store: ScratchStore, key: ScratchKey<T>, value: T): void;
|
|
2352
|
+
/** Remove a slot from the scratch store. Returns `true` if it was set,
|
|
2353
|
+
* `false` if it wasn't. */
|
|
2354
|
+
declare function deleteScratch<T>(store: ScratchStore, key: ScratchKey<T>): boolean;
|
|
2355
|
+
|
|
2356
|
+
/**
|
|
2357
|
+
* IngestItem materialization — turns a live `DataTransfer` (drop) or
|
|
2358
|
+
* clipboard `DataTransfer` (paste) into fully-owned `IngestItem[]`.
|
|
2359
|
+
*
|
|
2360
|
+
* `DataTransfer` items are only readable while the originating DOM event is
|
|
2361
|
+
* on the stack, so BOTH functions must be called synchronously from the event
|
|
2362
|
+
* handler. `itemsFromDataTransfer` kicks off every `getAsString` read
|
|
2363
|
+
* immediately and resolves once all strings are collected;
|
|
2364
|
+
* `itemsFromClipboardData` is fully synchronous (`getData` + `files`).
|
|
2365
|
+
*
|
|
2366
|
+
* MIME strings are normalized to bare lowercase `type/subtype` (parameters
|
|
2367
|
+
* like `;charset=utf-8` stripped) — downstream exact matching (binding
|
|
2368
|
+
* `types`, content-handler `match`) assumes the bare form.
|
|
2369
|
+
*
|
|
2370
|
+
* Output shape: **files first (source order), then strings (item order)**.
|
|
2371
|
+
* For paste (`itemsFromClipboardData`) string flavors are emitted in
|
|
2372
|
+
* `INGEST_STRING_MIMES` order: text/plain, text/html, text/uri-list.
|
|
2373
|
+
*/
|
|
2374
|
+
|
|
2375
|
+
/** The string flavors the kit normalizes. Everything else (browser-internal
|
|
2376
|
+
* custom flavors, etc.) is dropped at materialization. */
|
|
2377
|
+
declare const INGEST_STRING_MIMES: ReadonlySet<string>;
|
|
2378
|
+
/** Wrap raw `File`s (file picker, `CanvasExtensionApi.ingest`) as
|
|
2379
|
+
* `IngestItem`s, normalizing each MIME the same way the drop/paste paths
|
|
2380
|
+
* do. Synchronous — `File` objects are already fully owned. */
|
|
2381
|
+
declare function itemsFromFiles(files: File[]): IngestItem[];
|
|
2382
|
+
/** Materialize a drop's `DataTransfer`. MUST be called during the `drop`
|
|
2383
|
+
* event dispatch (see module doc). */
|
|
2384
|
+
declare function itemsFromDataTransfer(dt: DataTransfer): Promise<IngestItem[]>;
|
|
2385
|
+
/** Materialize a paste's `clipboardData`. Fully synchronous. */
|
|
2386
|
+
declare function itemsFromClipboardData(cd: DataTransfer): IngestItem[];
|
|
2387
|
+
|
|
2388
|
+
/**
|
|
2389
|
+
* Convert client coords to element CSS-pixel coords (relative to the element's
|
|
2390
|
+
* top-left). Apps drawing in CSS-pixel space feed the result directly into
|
|
2391
|
+
* world math when pan/zoom is identity. With pan/zoom, compose this with
|
|
2392
|
+
* your inverse-viewport transform.
|
|
2393
|
+
*/
|
|
2394
|
+
declare function clientToCanvas(canvas: HTMLElement, clientX: number, clientY: number): [number, number];
|
|
2395
|
+
/**
|
|
2396
|
+
* Rect-taking variant of `clientToCanvas` for call sites that already hold a
|
|
2397
|
+
* `DOMRect` (or any `{ left, top }` object) and don't have a canvas element
|
|
2398
|
+
* in scope.
|
|
2399
|
+
*/
|
|
2400
|
+
declare function clientToCanvasRect(rect: {
|
|
2401
|
+
left: number;
|
|
2402
|
+
top: number;
|
|
2403
|
+
}, clientX: number, clientY: number): [number, number];
|
|
2404
|
+
|
|
2405
|
+
/** Conditional `console.debug` — no-op unless `namespace` (or `*`) is enabled.
|
|
2406
|
+
* Each call is prefixed with `[namespace]` so the source is searchable. */
|
|
2407
|
+
declare function dlog(namespace: string, ...args: unknown[]): void;
|
|
2408
|
+
|
|
2409
|
+
/**
|
|
2410
|
+
* @weasel-js/routing — binding-to-action routing.
|
|
2411
|
+
*
|
|
2412
|
+
* The pure half: no React, no DOM. The React seam that pumps browser events
|
|
2413
|
+
* into the dispatcher, and the providers that feed it, live behind
|
|
2414
|
+
* `@weasel-js/routing/react`.
|
|
2415
|
+
*/
|
|
2416
|
+
|
|
2417
|
+
/**
|
|
2418
|
+
* The dependency schema — the map of dep name → value type that
|
|
2419
|
+
* `Action.requires`, `DepRegistry.register`/`get` and `useDepSource` are all
|
|
2420
|
+
* keyed on.
|
|
2421
|
+
*
|
|
2422
|
+
* It is declared **empty** here on purpose. Routing knows that an action names
|
|
2423
|
+
* its dependencies and that the registry resolves them at invocation time; it
|
|
2424
|
+
* does not know what a selection, a scene or a viewport is. Whoever supplies
|
|
2425
|
+
* those merges them in:
|
|
2426
|
+
*
|
|
2427
|
+
* ```ts
|
|
2428
|
+
* declare module '@weasel-js/routing' {
|
|
2429
|
+
* interface DepSchema { selection: SelectionApi }
|
|
2430
|
+
* }
|
|
2431
|
+
* ```
|
|
2432
|
+
*
|
|
2433
|
+
* `@weasel-js/core` merges the kit's own 24 deps this way and re-exports
|
|
2434
|
+
* `DepSchema`, so a consumer adds its own against `'@weasel-js/core'` and
|
|
2435
|
+
* never has to know this module exists. The merge carries through the
|
|
2436
|
+
* re-export in both directions — `scripts/smoke-consumer-bundle.mjs` asserts
|
|
2437
|
+
* it against the published `.d.ts`, which is where it once broke: a
|
|
2438
|
+
* cross-module augmentation stops merging when `rollup-plugin-dts` flattens
|
|
2439
|
+
* both files into one chunk.
|
|
2440
|
+
*/
|
|
2441
|
+
interface DepSchema {
|
|
2442
|
+
}
|
|
2443
|
+
/** Every dep name declared by anything that has merged into {@link DepSchema}. */
|
|
2444
|
+
type DepName = keyof DepSchema;
|
|
2445
|
+
|
|
2446
|
+
export { INGEST_STRING_MIMES as $, type ActionsRegistry as A, type BindingOpts as B, type Contribution as C, type Dispatcher as D, type Condition as E, type Conflict as F, type ContributionChrome as G, type ContributionRouting as H, DEFAULT_ALLOWED_CAPABILITIES as I, DEFAULT_DEVICE_PROFILE as J, type DebugSink as K, type DepName as L, type DetectedDeviceFacts as M, type DeviceProfile as N, type OverlayPosition as O, type DispatchLogEntry as P, type DispatcherContext as Q, type RuleCtx as R, type ScopedBinding as S, type Tool as T, type DragSample as U, type Eligibility as V, type EligibilityState as W, type GestureBinding as X, type HandleKind as Y, type HitShape as Z, type HotkeyTrigger as _, type AffordanceHit as a, isAnchorOrControl as a$, type ImmediateInvoker as a0, type InvocationCtx as a1, type Invoker as a2, type KeyBinding as a3, type MatchResult as a4, type ModeSwitchLogEntry as a5, type ModifierState as a6, NEVER as a7, type NodeId as a8, type OngoingHandle as a9, type ToolModifiers as aA, type ToolOffhandBindingSpec as aB, type ToolPresentation as aC, type ToolSlot as aD, type TraceLogEntry as aE, type UiOngoingControl as aF, type View as aG, type ViewportToolDef as aH, actionBindings as aI, buildDepsFromRequires as aJ, buildRouteRegistry as aK, buildRuleCtx as aL, buildToolOffhandBindings as aM, clientToCanvas as aN, clientToCanvasRect as aO, createDispatcher as aP, defineTool as aQ, defineViewportTool as aR, deleteScratch as aS, describeRule as aT, dlog as aU, evaluate as aV, evaluateEnabled as aW, filterEligible as aX, findConflicts as aY, getScratch as aZ, isAnchor as a_, type OngoingInvoker as aa, type OngoingOverlay as ab, type OverlayRole as ac, PREDICATE_TARGET as ad, type Point2 as ae, type PointerSession as af, type PointerSessionCallbacks as ag, type PointerSessionCancelReason as ah, type PointerSessionOptions as ai, type RegistryEntry as aj, type ResizeAnchor as ak, type ResolveAllOptions as al, type ResolveOnlyResult as am, type ResolvedCandidate as an, type Rule as ao, type ScratchKey as ap, type ScratchStore as aq, type SelectionApi as ar, type SelectionExtendKey as as, type SelectionMode as at, type Selector as au, TOOL_OFFHAND_ID as av, type ToolBounds as aw, type ToolCtx as ax, type ToolDef as ay, type ToolKeybinding as az, type DepSchema as b, isBody as b0, isControlHandle as b1, isEditableTarget as b2, isEmpty as b3, isResizeHandle as b4, isRotateHandle as b5, isSelectedBody as b6, isUnselectedBody as b7, itemsFromClipboardData as b8, itemsFromDataTransfer as b9, itemsFromFiles as ba, liveScope as bb, makeToolOffhandAction as bc, matchBest as bd, matchSorted as be, matchesKeyBinding as bf, mergeContributions as bg, offhandKeyFor as bh, openPointerSession as bi, pointerGestureId as bj, recordModeSwitch as bk, reportRouteConflicts as bl, resolveDeviceProfile as bm, resolveParams as bn, routeGestureForSpecKind as bo, routesForSpec as bp, scopeBindings as bq, scratchKey as br, setScratch as bs, specificity as bt, targetConsultsAffordance as bu, validateActionDefaultBinding as bv, validateActionId as bw, type Action as c, type AnyToolOf as d, type DepRegistry as e, DepRegistryProvider as f, useDepSource as g, useOptionalDepRegistry as h, ALWAYS as i, type ActionDeps as j, ActionDisabledReason as k, type ActionDispatch as l, type ActionEnabledResult as m, type ActionEntry as n, type ActionPresentation as o, type ActionSource as p, type ActionsProp as q, type AnyTool as r, type BindingScope as s, type BindingSource as t, useDepRegistry as u, type BodyPredicate as v, type BoundGesture as w, type Bounds as x, type BuildRuleCtxArgs as y, COARSE_TARGET_SCALE as z };
|