@weasel-js/core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +139 -0
- package/dist/{DrawCommand-Dl0bXNfS.d.ts → DrawCommand-CJtqqt8H.d.ts} +26 -2
- package/dist/{chunk-7V6JEOXE.js → chunk-7F3SDUJ4.js} +7884 -9167
- package/dist/chunk-7F3SDUJ4.js.map +1 -0
- package/dist/chunk-FSZEXVCR.js +171 -0
- package/dist/chunk-FSZEXVCR.js.map +1 -0
- package/dist/clone.d.ts +1 -1
- package/dist/geometry-D9BDMiQi.d.ts +114 -0
- package/dist/{grid-Cf87knjU.d.ts → grid-CaSK9bHV.d.ts} +1 -1
- package/dist/index-D31EADQG.d.ts +2847 -0
- package/dist/index.css +0 -35
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +913 -2546
- package/dist/index.js +2 -2
- package/dist/insert.d.ts +2 -2
- package/dist/move.d.ts +3 -3
- package/dist/{options-BPPBWMa7.d.ts → options-DMWeTELe.d.ts} +1 -1
- package/dist/{pointSnapToGrid-D7s7QmOF.d.ts → pointSnapToGrid-C3EruUwt.d.ts} +3 -54
- package/dist/renderer.css +0 -35
- package/dist/renderer.css.map +1 -1
- package/dist/renderer.d.ts +5 -4
- package/dist/renderer.js +2 -2
- package/dist/resize.d.ts +3 -3
- package/dist/routing.d.ts +10 -8
- package/dist/routing.js +1 -1
- package/dist/{types-BjUi2vA-.d.ts → types-Dcaa0tPq.d.ts} +1 -25
- package/dist/{registerFont-CP-wCsrz.d.ts → viewToMat3-D4lrBigW.d.ts} +2 -44
- package/package.json +6 -5
- package/dist/chunk-7V6JEOXE.js.map +0 -1
- package/dist/chunk-AM6ARSPN.js +0 -517
- package/dist/chunk-AM6ARSPN.js.map +0 -1
- package/dist/fitViewToBounds-evGsnR8Q.d.ts +0 -62
- package/dist/index-DZBYMsHI.d.ts +0 -1555
- package/dist/routing.css +0 -35
- package/dist/routing.css.map +0 -1
|
@@ -0,0 +1,2847 @@
|
|
|
1
|
+
import { N as NodeId, S as Scene, P as PoseComposition } from './types-Cpb4hii1.js';
|
|
2
|
+
import { M as ModifierState, A as ActionBehavior, c as ResizeAnchor, R as ResizePose, B as BoundsConstraint, P as PointSnapBehavior } from './types-Dcaa0tPq.js';
|
|
3
|
+
import { V as View } from './view-DSQgxBJB.js';
|
|
4
|
+
import { CapabilityTag } from '@weasel-js/modes';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { MutableRefObject, ReactNode, ReactElement } from 'react';
|
|
7
|
+
import { IngestItem, GestureSpec, InputEvent, ParsedModifiers, GestureName, ChannelRef, DescribeRouteOptions, GESTURE_DESCRIPTORS, GestureArgSpec, GestureDescriptor, ModRequirement, ModifierKey, ParsedRoute, PhaseAtom, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, RouteDescriptionPart, RouteFieldName, RouteTermLabel, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, parseRoute } from '@weasel-js/gestures';
|
|
8
|
+
import { F as FillStyle, a as Stroke, D as DrawCommand } from './DrawCommand-CJtqqt8H.js';
|
|
9
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
10
|
+
import { P as Path } from './types-B6MMiodD.js';
|
|
11
|
+
import { Op, History } from '@weasel-js/history';
|
|
12
|
+
import { L as LayoutStrategy, I as InsertAdapter } from './types-B_-khFM0.js';
|
|
13
|
+
import { B as Bounds, P as PoseProjection } from './geometry-D9BDMiQi.js';
|
|
14
|
+
import { Mat3 } from '@weasel-js/geom';
|
|
15
|
+
import { D as DebugSink } from './types-BJ8_cyT7.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read-only state that affordances consult on every render and hit-test
|
|
19
|
+
* call. Built once per Canvas render via `buildChromeState`; affordances
|
|
20
|
+
* must not cache it across calls.
|
|
21
|
+
*/
|
|
22
|
+
interface ChromeState {
|
|
23
|
+
/** Currently selected ids. Live; reflects useSelection's React state. */
|
|
24
|
+
readonly selection: readonly NodeId[];
|
|
25
|
+
/** True when the canvas is in multi-mode AND >= 2 ids are selected. */
|
|
26
|
+
readonly multiActive: boolean;
|
|
27
|
+
/** Bounds for any selection member id. Honors active-tool overlay state
|
|
28
|
+
* (move/resize/rotate ghosts → ghost bounds; otherwise → committed
|
|
29
|
+
* pose bounds). Returns null for unknown ids or ids whose bounds aren't
|
|
30
|
+
* computable. */
|
|
31
|
+
boundsOf(id: string): Bounds | null;
|
|
32
|
+
/** Multi-union AABB when `multiActive`. Computed lazily from `boundsOf`
|
|
33
|
+
* over every selected id; null otherwise. */
|
|
34
|
+
readonly unionBounds: Bounds | null;
|
|
35
|
+
/** Active modifier state at the moment of the call. */
|
|
36
|
+
readonly modifiers: ModifierState;
|
|
37
|
+
/** True iff the node's pose-descriptor declares it can carry a rotation.
|
|
38
|
+
* Affordances consult this to decide whether to expose rotate cursors /
|
|
39
|
+
* drag-bands. Defaults to `true` when the descriptor doesn't declare
|
|
40
|
+
* (back-compat) or when the id is unknown — the rotation gesture will
|
|
41
|
+
* no-op visually for poses without AABB fields, but the affordance
|
|
42
|
+
* doesn't lie about the cursor. Optional on the interface so unit-test
|
|
43
|
+
* call sites that construct `ChromeState` by hand keep compiling;
|
|
44
|
+
* affordances should treat an absent predicate as "true". */
|
|
45
|
+
canRotate?(id: string): boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @experimental
|
|
50
|
+
* A single interactive piece of chrome. Pure functions; the kit composes
|
|
51
|
+
* multiple affordances into a single RenderLayer per tool via
|
|
52
|
+
* `composeAffordanceLayer`.
|
|
53
|
+
*
|
|
54
|
+
* Affordances declare interactive regions in a target's *local* frame.
|
|
55
|
+
* The framework (`composeAffordanceLayer`) composes the target's bounds
|
|
56
|
+
* transform (rotation around the AABB center, when present) for both paint
|
|
57
|
+
* and hit-test, so the affordance never touches rotation, view.scale, or
|
|
58
|
+
* world↔screen math.
|
|
59
|
+
*/
|
|
60
|
+
interface Affordance {
|
|
61
|
+
/** Stable id for debug overlays + visibility maps. */
|
|
62
|
+
id: string;
|
|
63
|
+
/** Enumerate this affordance's interactive regions. Each region lives in
|
|
64
|
+
* some target id's local frame (or in the world frame when `targetId`
|
|
65
|
+
* is `null`). Returning `[]` means "no chrome for this state" (no
|
|
66
|
+
* selection, multi-mode disabled, etc.). */
|
|
67
|
+
regions(state: ChromeState): readonly AffordanceRegion[];
|
|
68
|
+
/** Optional non-interactive decoration (e.g., a leader line drawn from
|
|
69
|
+
* a bounds edge to a handle — visual only, not draggable). Receives
|
|
70
|
+
* raw state + view because the decoration may live outside any single
|
|
71
|
+
* target's local frame. Most affordances leave this undefined. */
|
|
72
|
+
decorate?(state: ChromeState, view: View): DrawCommand[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* @experimental
|
|
76
|
+
* One interactive region produced by an affordance. The framework owns
|
|
77
|
+
* the local↔world transform for `targetId` (when non-null), so `shape`,
|
|
78
|
+
* `paint.sizePx`, and `hitRadiusPx` are always specified in coordinates
|
|
79
|
+
* the affordance can reason about directly.
|
|
80
|
+
*/
|
|
81
|
+
interface AffordanceRegion<TScratch = unknown> {
|
|
82
|
+
/** Stable id, e.g. `corner-min-min`. Used for debug overlays + a11y. */
|
|
83
|
+
id: string;
|
|
84
|
+
/** Target id whose `state.boundsOf(targetId)` defines this region's
|
|
85
|
+
* local frame. `bounds.rotation` (if present) is the only transform
|
|
86
|
+
* applied — translation is the AABB origin; scale is identity. Pass
|
|
87
|
+
* `null` for affordances anchored to the viewport / world frame
|
|
88
|
+
* (identity transform). */
|
|
89
|
+
targetId: string | null;
|
|
90
|
+
/** Region geometry, expressed in the target's local frame.
|
|
91
|
+
*
|
|
92
|
+
* - `point` — circular hit (`hitRadiusPx` is screen-space).
|
|
93
|
+
* - `rect` — axis-aligned rect (target rotation applies).
|
|
94
|
+
* - `annulus` — outer ellipse minus inner rect cutout. Used for
|
|
95
|
+
* invisible zones that sit *around* the AABB (e.g. rotate-on-
|
|
96
|
+
* hover band). The outer ellipse is defined by world-space
|
|
97
|
+
* semi-axes `rx` / `ry` around `(cx, cy)`; the inner rect is the
|
|
98
|
+
* same target-local rect that defines the AABB. Hit-test:
|
|
99
|
+
* inside outer ellipse AND outside inner rect. */
|
|
100
|
+
shape: {
|
|
101
|
+
kind: 'point';
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
hitRadiusPx: number;
|
|
105
|
+
} | {
|
|
106
|
+
kind: 'rect';
|
|
107
|
+
x: number;
|
|
108
|
+
y: number;
|
|
109
|
+
width: number;
|
|
110
|
+
height: number;
|
|
111
|
+
} | {
|
|
112
|
+
kind: 'annulus';
|
|
113
|
+
/** Outer-ellipse center (target-local). */
|
|
114
|
+
cx: number;
|
|
115
|
+
cy: number;
|
|
116
|
+
/** Outer-ellipse semi-axes (target-local). */
|
|
117
|
+
rx: number;
|
|
118
|
+
ry: number;
|
|
119
|
+
/** Inner rect (target-local) — the cutout. Typically the
|
|
120
|
+
* selection's AABB. */
|
|
121
|
+
innerX: number;
|
|
122
|
+
innerY: number;
|
|
123
|
+
innerWidth: number;
|
|
124
|
+
innerHeight: number;
|
|
125
|
+
};
|
|
126
|
+
/** Optional paint. World position is derived from `shape` + target
|
|
127
|
+
* transform; visual size stays in screen pixels (so handles don't
|
|
128
|
+
* warp under zoom or non-uniform scale). Omit for hit-only regions.
|
|
129
|
+
*
|
|
130
|
+
* - `square` — small fixed-size square (only valid over `point` shapes).
|
|
131
|
+
* - `annulus` — fill + stroke the annulus ring (only valid over
|
|
132
|
+
* `annulus` shapes). Uses even-odd fill rule to punch the inner-rect
|
|
133
|
+
* cutout.
|
|
134
|
+
* - `custom` — emit arbitrary draw commands; receives a {@link CustomPaintContext}. */
|
|
135
|
+
paint?: {
|
|
136
|
+
kind: 'square';
|
|
137
|
+
sizePx: number;
|
|
138
|
+
fill?: FillStyle;
|
|
139
|
+
stroke?: Stroke;
|
|
140
|
+
} | {
|
|
141
|
+
kind: 'annulus';
|
|
142
|
+
fill?: FillStyle;
|
|
143
|
+
stroke?: Stroke;
|
|
144
|
+
insetPx?: number;
|
|
145
|
+
} | {
|
|
146
|
+
kind: 'custom';
|
|
147
|
+
draw: (ctx: CustomPaintContext) => DrawCommand[];
|
|
148
|
+
};
|
|
149
|
+
/** Declared CSS cursor for this region. NOTE: nothing consumes this
|
|
150
|
+
* field yet — the hover-cursor pump lives on the dispatcher pipeline
|
|
151
|
+
* (`useGestureDispatcher`) and reads `AffordanceHit.cursor` from
|
|
152
|
+
* `buildAffordanceAt`'s synthesized hits, which do not go through
|
|
153
|
+
* region objects. Honoring this field is part of the pending
|
|
154
|
+
* affordance-layer unification (see the selection-overlay note in
|
|
155
|
+
* `docs/taxonomy.md`). */
|
|
156
|
+
cursor?: string;
|
|
157
|
+
/** Drag binding produced when this region is hit. Lazily called so
|
|
158
|
+
* affordances don't pay binding-construction cost on every paint frame —
|
|
159
|
+
* state snapshots (e.g., capturing per-leaf poses at click time) belong
|
|
160
|
+
* inside `bind()`, not inside `regions()`. */
|
|
161
|
+
bind(): AffordanceBinding<TScratch>;
|
|
162
|
+
}
|
|
163
|
+
/** Context passed to a region's `paint.kind === 'custom'` draw callback.
|
|
164
|
+
* Provides both the world-space anchor (already transformed) and the
|
|
165
|
+
* original local shape, for affordances that want to do additional
|
|
166
|
+
* geometry themselves. */
|
|
167
|
+
interface CustomPaintContext {
|
|
168
|
+
/** World-space mapping of `shape`. For `point`, only `x`/`y` are set.
|
|
169
|
+
* For `rect`, all four fields are set. */
|
|
170
|
+
world: {
|
|
171
|
+
x: number;
|
|
172
|
+
y: number;
|
|
173
|
+
width?: number;
|
|
174
|
+
height?: number;
|
|
175
|
+
};
|
|
176
|
+
/** The original local shape (same object identity as `region.shape`). */
|
|
177
|
+
local: AffordanceRegion['shape'];
|
|
178
|
+
view: View;
|
|
179
|
+
state: ChromeState;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* @experimental
|
|
183
|
+
* Result of an affordance hit — what the region computed about itself.
|
|
184
|
+
*
|
|
185
|
+
* `initialScratch` is the payload: what the region already knows (which
|
|
186
|
+
* corner, which target id) so the action that picks up the drag doesn't
|
|
187
|
+
* re-derive it. `<SceneCanvas>` reads it out of the layer hit-test and packs
|
|
188
|
+
* it into `AffordanceHit`, which flows to the matching action through
|
|
189
|
+
* `InvocationCtx.drag.affordance`.
|
|
190
|
+
*
|
|
191
|
+
* This used to also carry a `drag: DragChannel` naming the handlers the
|
|
192
|
+
* tool-routing dispatcher should wire up. Every implementation supplied a
|
|
193
|
+
* no-op stub that claimed, because the real routing had already moved to
|
|
194
|
+
* bindings; the field went with that dispatcher.
|
|
195
|
+
*/
|
|
196
|
+
interface AffordanceBinding<TScratch = unknown> {
|
|
197
|
+
initialScratch?: TScratch;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Canvas size in CSS pixels — passed to `draw` for layers that anchor to
|
|
202
|
+
* canvas edges (e.g. the debug overlay's layer-list panel). The GL backend
|
|
203
|
+
* supplies it explicitly so layers don't have to know about DPR.
|
|
204
|
+
*/
|
|
205
|
+
interface Dims {
|
|
206
|
+
width: number;
|
|
207
|
+
height: number;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A single named render sub-layer within a canvas renderer.
|
|
211
|
+
*
|
|
212
|
+
* @template TData - The data object passed to each draw call.
|
|
213
|
+
*/
|
|
214
|
+
interface RenderLayer<TData> {
|
|
215
|
+
/** Unique identifier used in visibility maps and ordering arrays. */
|
|
216
|
+
id: string;
|
|
217
|
+
/** Human-readable name for UI toggles. */
|
|
218
|
+
label: string;
|
|
219
|
+
/**
|
|
220
|
+
* Emit a DrawCommand tree for the GL backend to dispatch.
|
|
221
|
+
*
|
|
222
|
+
* For world-space layers (the default), emit commands in WORLD COORDS —
|
|
223
|
+
* `drawLayers` automatically wraps them in `{ kind: 'group', transform:
|
|
224
|
+
* viewToMat3(view), ... }` before handing them to the renderer. Do NOT
|
|
225
|
+
* apply the view transform yourself.
|
|
226
|
+
*
|
|
227
|
+
* For screen-space layers (`space: 'screen'`), emit commands in CSS-pixel
|
|
228
|
+
* coords directly; `drawLayers` passes them through unchanged. If part
|
|
229
|
+
* of a screen-space layer's output needs to track the view, wrap that
|
|
230
|
+
* subset manually with `viewToMat3(view)`.
|
|
231
|
+
*/
|
|
232
|
+
draw: (data: TData, view: View, dims: Dims) => DrawCommand[];
|
|
233
|
+
/**
|
|
234
|
+
* Whether the layer is shown when no explicit visibility entry exists.
|
|
235
|
+
* Defaults to `true` when absent.
|
|
236
|
+
*/
|
|
237
|
+
defaultVisible?: boolean;
|
|
238
|
+
/**
|
|
239
|
+
* When true, the layer is always drawn regardless of the visibility map.
|
|
240
|
+
* Useful for layers that must never be hidden (e.g. base grid).
|
|
241
|
+
*/
|
|
242
|
+
alwaysOn?: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Coordinate space the layer draws in.
|
|
245
|
+
*
|
|
246
|
+
* - `'world'` (default): the layer's `draw` returns world-space commands;
|
|
247
|
+
* `drawLayers` wraps them in a `kind: 'group'` with `viewToMat3(view)`
|
|
248
|
+
* automatically.
|
|
249
|
+
* - `'screen'`: the layer's `draw` returns screen-space (CSS-pixel)
|
|
250
|
+
* commands; `drawLayers` passes them through unchanged. World-anchored
|
|
251
|
+
* chrome inside a screen-space layer must call `worldToScreen` or wrap
|
|
252
|
+
* the relevant subset with `viewToMat3(view)` manually.
|
|
253
|
+
*/
|
|
254
|
+
space?: 'world' | 'screen';
|
|
255
|
+
/**
|
|
256
|
+
* Optional hit-test. When defined, the dispatcher consults this on
|
|
257
|
+
* pointerdown (top-down layer order) before falling through to the
|
|
258
|
+
* active tool's slot walk. First non-null result wins; null means
|
|
259
|
+
* "I don't claim this hit, try the next layer."
|
|
260
|
+
*
|
|
261
|
+
* Coordinates are world-space. The `data` arg is the layer's
|
|
262
|
+
* configured data slot (same as `draw`); `view` and `dims` mirror
|
|
263
|
+
* `draw`'s arguments.
|
|
264
|
+
*/
|
|
265
|
+
hitTest?: (worldX: number, worldY: number, data: TData, view: View, dims: Dims,
|
|
266
|
+
/** Chrome-caps visibility predicate. When supplied, the layer must
|
|
267
|
+
* not return a hit from any chrome element whose id reports
|
|
268
|
+
* `false`. Absent → every element is hittable. */
|
|
269
|
+
isVisible?: (id: string) => boolean) => AffordanceBinding | null;
|
|
270
|
+
/**
|
|
271
|
+
* Called on every pointermove when no gesture is currently captured.
|
|
272
|
+
* Lets layers (e.g. HUD widgets) track hover state without participating
|
|
273
|
+
* in the drag pipeline. Coords are world-space; the layer is responsible
|
|
274
|
+
* for any further conversion (e.g. world→screen for screen-space layers)
|
|
275
|
+
* and for its own throttling.
|
|
276
|
+
*/
|
|
277
|
+
onUncapturedMove?: (worldX: number, worldY: number, evt: PointerEvent, view: View, dims: Dims) => void;
|
|
278
|
+
/**
|
|
279
|
+
* Called when the cursor leaves the canvas element. Lets layers clear
|
|
280
|
+
* any hover state they're holding.
|
|
281
|
+
*/
|
|
282
|
+
onUncapturedLeave?: () => void;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Walk visible layers and concatenate their emitted DrawCommand arrays into
|
|
286
|
+
* one flat list, ready to feed to `WeaselRenderer.render(commands)`.
|
|
287
|
+
*
|
|
288
|
+
* Visibility resolution order:
|
|
289
|
+
* 1. `alwaysOn` — always drawn, ignores visibility map.
|
|
290
|
+
* 2. Explicit entry in `visibility` map — overrides default.
|
|
291
|
+
* 3. `layer.defaultVisible` — falls back to `true` when absent.
|
|
292
|
+
*
|
|
293
|
+
* Transform composition: world-space layers (the default; `space` unset or
|
|
294
|
+
* `'world'`) have their commands wrapped in a `kind: 'group'` with
|
|
295
|
+
* `viewToMat3(view)` before they reach the renderer. Screen-space layers
|
|
296
|
+
* (`space: 'screen'`) pass through unchanged.
|
|
297
|
+
*/
|
|
298
|
+
declare function drawLayers<TData>(layers: RenderLayer<TData>[], data: TData, visibility: Record<string, boolean>, order: string[] | undefined, view: View | undefined, dims: Dims): DrawCommand[];
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Live state read by rule evaluation. Built once per frame on the consuming
|
|
302
|
+
* surface — chrome-caps, the affordance pipeline, the dispatcher's
|
|
303
|
+
* eligibility filter — and discarded.
|
|
304
|
+
*
|
|
305
|
+
* Adding a new field is additive: existing rules don't change, new
|
|
306
|
+
* selector atoms can read it.
|
|
307
|
+
*/
|
|
308
|
+
interface RuleCtx {
|
|
309
|
+
readonly focused: boolean;
|
|
310
|
+
readonly selection: readonly NodeId[];
|
|
311
|
+
readonly multiActive: boolean;
|
|
312
|
+
readonly modifiers: ModifierState;
|
|
313
|
+
readonly action: {
|
|
314
|
+
readonly kind: string | null;
|
|
315
|
+
readonly id: string | null;
|
|
316
|
+
};
|
|
317
|
+
readonly hover: NodeId | null;
|
|
318
|
+
readonly view: View;
|
|
319
|
+
/** Active mode id. `'normal'` when no non-default mode is engaged. */
|
|
320
|
+
readonly mode: string;
|
|
321
|
+
/** Capability tags allowed by the active mode (the union of
|
|
322
|
+
* `ModeDefinition.allows` plus implicit tags). The `capability:`
|
|
323
|
+
* selector reads this to determine whether a tag is permitted. */
|
|
324
|
+
readonly allowedCapabilities: ReadonlySet<CapabilityTag>;
|
|
325
|
+
/** Whether the current selection may be resized. `<SceneCanvas>` folds
|
|
326
|
+
* `selectTool.resize.resizable` over the selection (true only when every
|
|
327
|
+
* selected node is resizable). Read by the `resizable:` selector to gate
|
|
328
|
+
* `selection.resize-handles`. Absent (legacy ctx builders) is treated as
|
|
329
|
+
* resizable — back-compat: handles show unless a consumer opts a node out. */
|
|
330
|
+
readonly selectionResizable?: boolean;
|
|
331
|
+
/** Whether a path is currently in anchor-edit mode. Read by the
|
|
332
|
+
* `editingAnchors:` selector, which gates the path-edit chrome.
|
|
333
|
+
*
|
|
334
|
+
* This is deliberately a fact about state, not about permission: the
|
|
335
|
+
* anchor overlay and the anchor hit-test must agree, and the thing they
|
|
336
|
+
* must agree on is "is there an edited path right now", which no
|
|
337
|
+
* capability or mode id answers. A mode that allows `edits-anchors`
|
|
338
|
+
* with nothing being edited should draw no anchors. Absent is treated
|
|
339
|
+
* as false. */
|
|
340
|
+
readonly editingAnchors?: boolean;
|
|
341
|
+
}
|
|
342
|
+
interface BuildRuleCtxArgs {
|
|
343
|
+
focused: boolean;
|
|
344
|
+
selection: readonly NodeId[];
|
|
345
|
+
multiActive: boolean;
|
|
346
|
+
modifiers: ModifierState;
|
|
347
|
+
action: {
|
|
348
|
+
kind: string | null;
|
|
349
|
+
id: string | null;
|
|
350
|
+
};
|
|
351
|
+
hover: NodeId | null;
|
|
352
|
+
view: View;
|
|
353
|
+
mode: string;
|
|
354
|
+
allowedCapabilities: ReadonlySet<CapabilityTag>;
|
|
355
|
+
/** Optional — omitted means "resizable" (handles show). See {@link RuleCtx}. */
|
|
356
|
+
selectionResizable?: boolean;
|
|
357
|
+
/** Optional — omitted means "no path is being anchor-edited". */
|
|
358
|
+
editingAnchors?: boolean;
|
|
359
|
+
}
|
|
360
|
+
declare function buildRuleCtx(args: BuildRuleCtxArgs): RuleCtx;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* A selector is a conjunction of key/value tests. Multiple keys at the same
|
|
364
|
+
* level AND together. Each key maps to a selector primitive in the evaluator.
|
|
365
|
+
*/
|
|
366
|
+
interface Selector {
|
|
367
|
+
selection?: {
|
|
368
|
+
is?: number;
|
|
369
|
+
atLeast?: number;
|
|
370
|
+
empty?: boolean;
|
|
371
|
+
};
|
|
372
|
+
mode?: string | {
|
|
373
|
+
not: string;
|
|
374
|
+
} | {
|
|
375
|
+
in: readonly string[];
|
|
376
|
+
};
|
|
377
|
+
capability?: CapabilityTag | readonly CapabilityTag[] | {
|
|
378
|
+
in: readonly CapabilityTag[];
|
|
379
|
+
} | {
|
|
380
|
+
not: CapabilityTag;
|
|
381
|
+
};
|
|
382
|
+
gesturing?: boolean;
|
|
383
|
+
actionIs?: string;
|
|
384
|
+
modifierHeld?: keyof ModifierState;
|
|
385
|
+
focused?: boolean;
|
|
386
|
+
hovering?: boolean;
|
|
387
|
+
hoveringSelected?: boolean;
|
|
388
|
+
zoomAtLeast?: number;
|
|
389
|
+
/** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
|
|
390
|
+
* mode. Absent flag is treated as `false`. */
|
|
391
|
+
editingAnchors?: boolean;
|
|
392
|
+
/** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
|
|
393
|
+
* (resizable), so `{ resizable: true }` passes for legacy ctx builders
|
|
394
|
+
* that don't compute it. */
|
|
395
|
+
resizable?: boolean;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
|
|
399
|
+
* over `Selector` leaves. `when` is the escape hatch — its closure is
|
|
400
|
+
* opaque to introspection and should be avoided when a declarative form
|
|
401
|
+
* exists. Empty `all` is true; empty `any` is false.
|
|
402
|
+
*/
|
|
403
|
+
type Rule = Selector | {
|
|
404
|
+
all: readonly Rule[];
|
|
405
|
+
} | {
|
|
406
|
+
any: readonly Rule[];
|
|
407
|
+
} | {
|
|
408
|
+
not: Rule;
|
|
409
|
+
} | {
|
|
410
|
+
when: (ctx: RuleCtx) => boolean;
|
|
411
|
+
};
|
|
412
|
+
/** Constant rules. Kept here so they have a single source. */
|
|
413
|
+
declare const ALWAYS: Rule;
|
|
414
|
+
declare const NEVER: Rule;
|
|
415
|
+
/**
|
|
416
|
+
* Render a {@link Rule} as a short human-readable string, for inspectors and
|
|
417
|
+
* diagnostics that need to say WHICH rule decided something.
|
|
418
|
+
*
|
|
419
|
+
* ```
|
|
420
|
+
* { capability: 'edits-page' } → capability:edits-page
|
|
421
|
+
* { all: [{ mode: 'normal' }, { focused: true }] } → all(mode:normal, focused:true)
|
|
422
|
+
* { not: { selection: { empty: true } } } → not(selection:empty=true)
|
|
423
|
+
* { when: function hasPath() { … } } → when(hasPath)
|
|
424
|
+
* ```
|
|
425
|
+
*
|
|
426
|
+
* Never throws and always terminates — unlike `JSON.stringify`, which throws
|
|
427
|
+
* on a cyclic value and renders the `when` arm as a useless `{}`.
|
|
428
|
+
*/
|
|
429
|
+
declare function describeRule(rule: Rule): string;
|
|
430
|
+
declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Live state read by chrome-visibility {@link Condition}s. Backward-compat
|
|
434
|
+
* alias for the legacy ChromeCtx shape — kept for consumers that still
|
|
435
|
+
* import `ChromeCtx`. Subset of `RuleCtx`: legacy ChromeCtx didn't carry
|
|
436
|
+
* mode/capability info. The resolver builds a `RuleCtx` for evaluation;
|
|
437
|
+
* surfaces that still operate in `ChromeCtx` shape supply defaults
|
|
438
|
+
* (mode='normal', empty allowedCapabilities) at the construction site.
|
|
439
|
+
*/
|
|
440
|
+
interface ChromeCtx {
|
|
441
|
+
readonly focused: boolean;
|
|
442
|
+
readonly selection: readonly NodeId[];
|
|
443
|
+
readonly multiActive: boolean;
|
|
444
|
+
readonly modifiers: ModifierState;
|
|
445
|
+
readonly action: {
|
|
446
|
+
readonly kind: string | null;
|
|
447
|
+
readonly id: string | null;
|
|
448
|
+
};
|
|
449
|
+
readonly hover: NodeId | null;
|
|
450
|
+
readonly view: View;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Composable visibility predicate with fluent surface. Carries its underlying
|
|
454
|
+
* `Rule` tree at `.rule` so the resolver can introspect / share trees with
|
|
455
|
+
* the affordance pipeline and the dispatcher's eligibility filter.
|
|
456
|
+
*
|
|
457
|
+
* Callable form `cond(ctx)` evaluates the tree against ctx. The fluent
|
|
458
|
+
* methods return new Conditions wrapping new trees.
|
|
459
|
+
*
|
|
460
|
+
* **Chain semantics: strict left-to-right, no precedence.**
|
|
461
|
+
* `a.and(b).or(c)` is `(a && b) || c`; `a.or(b).and(c)` is
|
|
462
|
+
* `(a || b) && c`. Mix `.and` and `.or` only when you mean
|
|
463
|
+
* left-to-right evaluation. For grouped disjunction, name the
|
|
464
|
+
* subexpression or use the top-level `or(...)`.
|
|
465
|
+
*/
|
|
466
|
+
interface Condition {
|
|
467
|
+
(ctx: RuleCtx): boolean;
|
|
468
|
+
readonly rule: Rule;
|
|
469
|
+
/** `this && other` */
|
|
470
|
+
and(other: Condition | Rule): Condition;
|
|
471
|
+
/** `this || other` */
|
|
472
|
+
or(other: Condition | Rule): Condition;
|
|
473
|
+
/** `this && !other` */
|
|
474
|
+
andNot(other: Condition | Rule): Condition;
|
|
475
|
+
/** `this || !other` */
|
|
476
|
+
orNot(other: Condition | Rule): Condition;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Stable identifier for one user-visible chrome element. The same id
|
|
480
|
+
* gates both paint and hit-test — there is no separate
|
|
481
|
+
* `affordance.X` / `selection.X` split — so toggling a rule cannot
|
|
482
|
+
* leave a visually-present but un-hittable handle (or vice versa).
|
|
483
|
+
*
|
|
484
|
+
* Naming convention by lifecycle:
|
|
485
|
+
*
|
|
486
|
+
* - `selection.*` — chrome reflecting committed selection state
|
|
487
|
+
* (persists between actions).
|
|
488
|
+
* - `action.*` — chrome that only exists during an in-flight
|
|
489
|
+
* action (vanishes on commit / cancel).
|
|
490
|
+
* - `snap.*` — snapping system chrome (guides, target highlights).
|
|
491
|
+
* - `grid`, `debug.*` — environment chrome.
|
|
492
|
+
*
|
|
493
|
+
* The intersection `(string & {})` keeps the union open so consumers
|
|
494
|
+
* can register their own ids; the kit's built-ins are listed
|
|
495
|
+
* explicitly for autocomplete.
|
|
496
|
+
*/
|
|
497
|
+
type ChromeId = 'selection.outline' | 'selection.resize-handles' | 'selection.rotation-handle' | 'action.marquee' | 'action.lasso' | 'action.move-ghosts' | 'action.insert-preview' | 'action.commands' | 'snap.guides' | 'snap.targets' | 'grid' | (string & {});
|
|
498
|
+
/**
|
|
499
|
+
* Consumer override map. Merged on top of the kit's
|
|
500
|
+
* `defaultVisibilityRules`; absent keys fall through to defaults,
|
|
501
|
+
* absent ids fall through to `always`. Entries may be either fluent
|
|
502
|
+
* `Condition` instances OR raw `Rule` trees — the resolver normalizes.
|
|
503
|
+
*/
|
|
504
|
+
type VisibilityRules = Partial<Record<ChromeId, Condition | Rule>>;
|
|
505
|
+
|
|
506
|
+
/** A 2D point in either world or screen coordinates. */
|
|
507
|
+
interface Point2 {
|
|
508
|
+
x: number;
|
|
509
|
+
y: number;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Information about which UI affordance was hit at pointerdown.
|
|
513
|
+
*
|
|
514
|
+
* Populated by the dispatcher when the `affordanceAt` thunk is provided to
|
|
515
|
+
* `useGestureDispatcher`. Tools / action invokers that only fire on a specific
|
|
516
|
+
* affordance (e.g. a resize handle) use this field as a guard — if the
|
|
517
|
+
* affordance is absent or is the wrong kind, they return `{}` and let other
|
|
518
|
+
* bindings handle the drag.
|
|
519
|
+
*
|
|
520
|
+
* `kind` is a discriminator string:
|
|
521
|
+
* - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
|
|
522
|
+
* `'handle:bottom-right'` — corner resize handles.
|
|
523
|
+
* - `'rotate-handle'` — the rotation affordance.
|
|
524
|
+
* - `'anchor:N'` — a path anchor at index N.
|
|
525
|
+
*
|
|
526
|
+
* `fixedPoint` is the world-space point that should remain stationary during
|
|
527
|
+
* the gesture. For resize handles this is the opposite (diagonally fixed)
|
|
528
|
+
* corner; for rotate it is the pivot.
|
|
529
|
+
*
|
|
530
|
+
* `targetIds` are the node ids this affordance belongs to.
|
|
531
|
+
*/
|
|
532
|
+
interface AffordanceHit {
|
|
533
|
+
/** Discriminator string, e.g. `'handle:bottom-right'`. */
|
|
534
|
+
kind: string;
|
|
535
|
+
/** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
|
|
536
|
+
fixedPoint?: {
|
|
537
|
+
x: number;
|
|
538
|
+
y: number;
|
|
539
|
+
};
|
|
540
|
+
/** Which nodes this affordance belongs to. */
|
|
541
|
+
targetIds?: string[];
|
|
542
|
+
/** Set when `kind` matches `'handle:*'`. Identifies which corner stays
|
|
543
|
+
* fixed during a resize so consumers (resizeAction) don't re-parse `kind`.
|
|
544
|
+
* Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
|
|
545
|
+
* leave this undefined. */
|
|
546
|
+
anchor?: ResizeAnchor;
|
|
547
|
+
/** CSS cursor to show while the pointer hovers this affordance (no
|
|
548
|
+
* gesture in flight). Consumed by the hover-cursor pump in
|
|
549
|
+
* `useGestureDispatcher`; unset = the pump falls through to
|
|
550
|
+
* action-cursor prediction, then to the active tool's cursor. */
|
|
551
|
+
cursor?: string;
|
|
552
|
+
/**
|
|
553
|
+
* Free-form payload from whatever produced the hit, carried through to the
|
|
554
|
+
* matching action untouched.
|
|
555
|
+
*
|
|
556
|
+
* Kit affordances describe themselves fully in the fields above and leave
|
|
557
|
+
* this unset. It exists for affordances the kit doesn't know the shape of —
|
|
558
|
+
* a registered layer's own chrome, where the hit-test already resolved
|
|
559
|
+
* *which* of its pieces was hit and the action would otherwise have to
|
|
560
|
+
* redo that work. `@weasel-js/hud` passes the hit widget here.
|
|
561
|
+
*/
|
|
562
|
+
payload?: unknown;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* One accumulated point on a drag trail: world-space position plus whatever
|
|
566
|
+
* stylus state the originating `PointerEvent` carried.
|
|
567
|
+
*
|
|
568
|
+
* The stylus fields are absent for mouse/touch on browsers that don't report
|
|
569
|
+
* them, and for synthetic events. Consumers that want pressure-driven output
|
|
570
|
+
* (e.g. `Stroke.vertexWidths` from a pencil stroke) read them off the samples
|
|
571
|
+
* their `insert` dep receives — see `apps/site/demos/VertexWidthsDemo.tsx`.
|
|
572
|
+
*/
|
|
573
|
+
interface DragSample extends Point2 {
|
|
574
|
+
/** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
|
|
575
|
+
pressure?: number;
|
|
576
|
+
/** Degrees, ±90. Zero for mouse/touch. */
|
|
577
|
+
tiltX?: number;
|
|
578
|
+
/** Degrees, ±90. Zero for mouse/touch. */
|
|
579
|
+
tiltY?: number;
|
|
580
|
+
}
|
|
581
|
+
/** Per-invocation runtime context the dispatcher hands to an Invoker.
|
|
582
|
+
* Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
|
|
583
|
+
* populated only for matching gesture kinds. */
|
|
584
|
+
interface InvocationCtx {
|
|
585
|
+
world: Point2;
|
|
586
|
+
screen: Point2;
|
|
587
|
+
modifiers: ModifierState;
|
|
588
|
+
deps: ActionDeps;
|
|
589
|
+
drag?: {
|
|
590
|
+
start: Point2;
|
|
591
|
+
current: Point2;
|
|
592
|
+
delta: Point2;
|
|
593
|
+
/**
|
|
594
|
+
* Drag delta in client/screen coordinates (CSS pixels from the drag
|
|
595
|
+
* origin). Use this — never `delta` — for any action whose effect
|
|
596
|
+
* mutates the viewport itself (pan, view-zoom), because world-space
|
|
597
|
+
* deltas become self-referential as the view shifts mid-drag.
|
|
598
|
+
*
|
|
599
|
+
* Populated when the dispatcher received `clientX`/`clientY` on the
|
|
600
|
+
* underlying pointer events. Absent for legacy callers that don't
|
|
601
|
+
* provide them.
|
|
602
|
+
*/
|
|
603
|
+
screenDelta?: Point2;
|
|
604
|
+
affordance?: AffordanceHit;
|
|
605
|
+
/**
|
|
606
|
+
* Full pointermove history for the current drag, in world space, with
|
|
607
|
+
* per-sample stylus state when the browser reported it.
|
|
608
|
+
* Accumulated by the dispatcher on every `pointermove` pump event.
|
|
609
|
+
* Available only during `onMove` and `onEnd` calls (not on `start`).
|
|
610
|
+
* Used by `lassoSelectAction` to build its polygon vertex list and by
|
|
611
|
+
* `insertAction`'s pencil kind to carry the freehand stroke.
|
|
612
|
+
*/
|
|
613
|
+
points?: DragSample[];
|
|
614
|
+
};
|
|
615
|
+
wheel?: {
|
|
616
|
+
deltaX: number;
|
|
617
|
+
deltaY: number;
|
|
618
|
+
deltaZ: number;
|
|
619
|
+
};
|
|
620
|
+
multiTouch?: {
|
|
621
|
+
centroid: Point2;
|
|
622
|
+
spread: number;
|
|
623
|
+
rotation: number;
|
|
624
|
+
/**
|
|
625
|
+
* Pinch-zoom geometry. Populated by the dispatcher when a multitouch
|
|
626
|
+
* handle is in flight and a pointermove-pump fires.
|
|
627
|
+
* `startSpread` is the spread at the moment the gesture began.
|
|
628
|
+
* `currentSpread` is the spread at the current frame.
|
|
629
|
+
*/
|
|
630
|
+
pinch?: {
|
|
631
|
+
startSpread: number;
|
|
632
|
+
currentSpread: number;
|
|
633
|
+
centroid: Point2;
|
|
634
|
+
};
|
|
635
|
+
};
|
|
636
|
+
key?: {
|
|
637
|
+
key: string;
|
|
638
|
+
repeat: boolean;
|
|
639
|
+
};
|
|
640
|
+
/**
|
|
641
|
+
* Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
|
|
642
|
+
* UI-driven ongoing actions (color picker, opacity slider) so handles can
|
|
643
|
+
* read the current value on `start` and updated values on `onMove`. The
|
|
644
|
+
* gesture dispatcher does not populate this field; gesture-driven actions
|
|
645
|
+
* receive params via `BindingOpts.params` on `start` (the `opts` arg).
|
|
646
|
+
*/
|
|
647
|
+
params?: Record<string, unknown>;
|
|
648
|
+
}
|
|
649
|
+
/** Per-invocation options the dispatcher reads from a `GestureBinding`'s
|
|
650
|
+
* `opts` field and passes to `OngoingInvoker.start`. Today carries
|
|
651
|
+
* behaviors; extensible. */
|
|
652
|
+
interface BindingOpts {
|
|
653
|
+
behaviors?: ActionBehavior<unknown, unknown, unknown>[];
|
|
654
|
+
/** Per-binding action parameters. The action's invoker reads
|
|
655
|
+
* these via the second arg to `run` (or via InvocationCtx for ongoing
|
|
656
|
+
* invokers, when needed). Loose typing (Record<string, unknown>) for
|
|
657
|
+
* now; consider per-action typing later via BindingOpts<A>.
|
|
658
|
+
*
|
|
659
|
+
* params may also be a thunk evaluated each time the
|
|
660
|
+
* dispatcher (or invoker) needs the value. Thunks let tools close over
|
|
661
|
+
* refs that mutate during a gesture (e.g. polygon `sides` adjusted
|
|
662
|
+
* mid-drag via ArrowUp). For ongoing invokers that want the latest
|
|
663
|
+
* values at commit, the invoker can re-call the thunk inside `onEnd`
|
|
664
|
+
* via `resolveParams(opts?.params)`. */
|
|
665
|
+
params?: Record<string, unknown> | (() => Record<string, unknown>);
|
|
666
|
+
}
|
|
667
|
+
/** Convention-shaped action dependencies bag. Actions declare which
|
|
668
|
+
* contexts they consume; the dispatcher composes them per call.
|
|
669
|
+
* Consumer-side contexts (e.g. ColorContext) plug in by extending. */
|
|
670
|
+
interface ActionDeps {
|
|
671
|
+
selection?: unknown;
|
|
672
|
+
view?: unknown;
|
|
673
|
+
scene?: unknown;
|
|
674
|
+
pointer?: unknown;
|
|
675
|
+
activeTool?: unknown;
|
|
676
|
+
[k: string]: unknown;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Discriminated overlay shape returned by `OngoingHandle.overlay()`.
|
|
680
|
+
* Dispatcher-side chrome surface for in-flight
|
|
681
|
+
* gestures that paint non-ghost visuals. The canvas's
|
|
682
|
+
* `useDispatcherOverlayLayer` walks every in-flight handle, calls
|
|
683
|
+
* `overlay()`, and dispatches on `kind` to draw the appropriate shape.
|
|
684
|
+
*
|
|
685
|
+
* `marquee` mirrors `AreaSelectOverlay`; `lasso` mirrors `LassoSelectOverlay`.
|
|
686
|
+
* `commands` is the generic escape hatch — actions emit arbitrary
|
|
687
|
+
* `DrawCommand[]` for previews the typed variants can't express (insert
|
|
688
|
+
* shape outlines, paste ghosts of synthetic nodes, custom chrome). World-
|
|
689
|
+
* space is the default; the layer wraps in `viewToMat3` so commands track
|
|
690
|
+
* the camera. Set `space: 'screen'` for projections you've already done
|
|
691
|
+
* yourself (rare).
|
|
692
|
+
*/
|
|
693
|
+
type OngoingOverlay = {
|
|
694
|
+
kind: 'marquee';
|
|
695
|
+
start: {
|
|
696
|
+
x: number;
|
|
697
|
+
y: number;
|
|
698
|
+
};
|
|
699
|
+
current: {
|
|
700
|
+
x: number;
|
|
701
|
+
y: number;
|
|
702
|
+
};
|
|
703
|
+
shiftHeld: boolean;
|
|
704
|
+
} | {
|
|
705
|
+
kind: 'lasso';
|
|
706
|
+
vertices: ReadonlyArray<{
|
|
707
|
+
x: number;
|
|
708
|
+
y: number;
|
|
709
|
+
}>;
|
|
710
|
+
current: {
|
|
711
|
+
x: number;
|
|
712
|
+
y: number;
|
|
713
|
+
};
|
|
714
|
+
shiftHeld: boolean;
|
|
715
|
+
} | {
|
|
716
|
+
kind: 'commands';
|
|
717
|
+
commands: readonly DrawCommand[];
|
|
718
|
+
/** Coordinate space the commands are authored in. Default `'world'`
|
|
719
|
+
* — the layer wraps them in `viewToMat3(view)` so they track the
|
|
720
|
+
* camera. `'screen'` emits them as-is (CSS pixels). */
|
|
721
|
+
space?: 'world' | 'screen';
|
|
722
|
+
} | {
|
|
723
|
+
/**
|
|
724
|
+
* Live insert-drag preview — dispatched by `insertAction` while the
|
|
725
|
+
* user is dragging out a new shape. Pre-commit there is no scene node
|
|
726
|
+
* to ghost via `previewIds()`/`previewPose()`, so insert paints its
|
|
727
|
+
* preview through the dispatcher overlay layer instead.
|
|
728
|
+
*
|
|
729
|
+
* `shape` is the kit's built-in insert kind. `bounds` is the AABB of
|
|
730
|
+
* the current drag (start/current normalized). `extras` is the
|
|
731
|
+
* per-kind extras the action already collected — the overlay
|
|
732
|
+
* renderer rebuilds the shape using the same path builders the
|
|
733
|
+
* commit factory uses, so the preview matches the eventual node.
|
|
734
|
+
*
|
|
735
|
+
* `extras` is opaque (`unknown`) at the union level; the overlay
|
|
736
|
+
* renderer narrows on `shape` and casts the field shape it expects.
|
|
737
|
+
*/
|
|
738
|
+
kind: 'insertPreview';
|
|
739
|
+
shape: 'rect' | 'ellipse' | 'line' | 'polygon' | 'star' | 'pencil';
|
|
740
|
+
bounds: {
|
|
741
|
+
x: number;
|
|
742
|
+
y: number;
|
|
743
|
+
width: number;
|
|
744
|
+
height: number;
|
|
745
|
+
};
|
|
746
|
+
extras: unknown;
|
|
747
|
+
/** World-space point to paint a small "anchor" dot at. Sells the
|
|
748
|
+
* click point as the drag's anchor — particularly useful for
|
|
749
|
+
* radial shapes (polygon/star) where no vertex sits on the
|
|
750
|
+
* click point, and for any shape in center mode where the dot
|
|
751
|
+
* marks the center the shape grows around. */
|
|
752
|
+
anchorPoint?: {
|
|
753
|
+
x: number;
|
|
754
|
+
y: number;
|
|
755
|
+
};
|
|
756
|
+
};
|
|
757
|
+
/** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
|
|
758
|
+
* `onMove` on subsequent input events of the same gesture and calls
|
|
759
|
+
* `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
|
|
760
|
+
* on pointercancel / blur / escape). */
|
|
761
|
+
interface OngoingHandle {
|
|
762
|
+
/**
|
|
763
|
+
* Optional logical action kind — a stable, human-readable tag the
|
|
764
|
+
* dispatcher exposes via `getActiveAction()` for chrome-visibility
|
|
765
|
+
* rules and any other surface that wants to react to "what action
|
|
766
|
+
* is currently in flight" without inspecting handles directly.
|
|
767
|
+
*
|
|
768
|
+
* Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
|
|
769
|
+
* `'pan'`, `'pinch'`.
|
|
770
|
+
*
|
|
771
|
+
* Distinct from the dispatcher's internal `gestureId` (`pointer-mouse`,
|
|
772
|
+
* `key-held-Space`, etc.) which keys per-pointer state and is not
|
|
773
|
+
* meaningful to consumers.
|
|
774
|
+
*
|
|
775
|
+
* When omitted, the action is "anonymous" — `getActiveAction().kind`
|
|
776
|
+
* reports `null` even though a handle is in flight. This is fine for
|
|
777
|
+
* actions that don't have visible chrome of their own.
|
|
778
|
+
*/
|
|
779
|
+
kind?: string;
|
|
780
|
+
onMove?(ctx: InvocationCtx): void;
|
|
781
|
+
onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
|
|
782
|
+
/**
|
|
783
|
+
* Optional preview surface — dispatcher-side ghost overlay.
|
|
784
|
+
*
|
|
785
|
+
* An ongoing-action implementation may populate `previewIds()` +
|
|
786
|
+
* `previewPose(id)` to expose its in-flight preview state for the
|
|
787
|
+
* canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
|
|
788
|
+
* top of the committed scene during the gesture.
|
|
789
|
+
*
|
|
790
|
+
* Returning `null` (or omitting the method entirely) means "no preview
|
|
791
|
+
* this gesture" — the canvas will skip this handle as a source.
|
|
792
|
+
*
|
|
793
|
+
* Semantics mirror the tool-side `Tool.previewIds` / `Tool.previewPose`
|
|
794
|
+
* pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
|
|
795
|
+
* returns the interim pose for one of those ids (shape opaque — the
|
|
796
|
+
* canvas casts to its `TPose` parameter). The preview-ghost layer
|
|
797
|
+
* merges all sources via first-non-null semantics, with tool-side
|
|
798
|
+
* previews taking precedence over dispatcher-side (preserves
|
|
799
|
+
* backwards-compat during the registry-unification migration).
|
|
800
|
+
*/
|
|
801
|
+
previewIds?(): Iterable<string> | null;
|
|
802
|
+
previewPose?(id: string): unknown | null;
|
|
803
|
+
/**
|
|
804
|
+
* When `false`, the preview-ghost layer paints the ghost AND the
|
|
805
|
+
* source node stays visible at its committed pose. Defaults to
|
|
806
|
+
* `true` (move/resize/rotate semantics: ghost replaces the source
|
|
807
|
+
* during the gesture). Clone overrides to `false` so the original
|
|
808
|
+
* stays put and the ghost appears at the drag target.
|
|
809
|
+
*/
|
|
810
|
+
previewHidesSource?: boolean;
|
|
811
|
+
/**
|
|
812
|
+
* Optional per-id preview *data*. Falls back to the committed
|
|
813
|
+
* `node.data` when null/absent. Use when the gesture mutates
|
|
814
|
+
* `node.data` (e.g. anchor-edit on nodes that store the polygon on
|
|
815
|
+
* `data.path`) rather than (or in addition to) the pose. The preview-
|
|
816
|
+
* ghost layer assembles a synthetic node from `{ ...node, pose:
|
|
817
|
+
* previewPose ?? node.pose, data: previewData ?? node.data }` before
|
|
818
|
+
* calling the scene slot's `drawOne`.
|
|
819
|
+
*
|
|
820
|
+
* Sources compose first-non-null per axis: an action can emit only
|
|
821
|
+
* `previewPose` (translation), only `previewData` (data-only edit),
|
|
822
|
+
* or both (pose + data both change, e.g. anchor drag on a data.path
|
|
823
|
+
* node where the bounds shift).
|
|
824
|
+
*/
|
|
825
|
+
previewData?(id: string): unknown | null;
|
|
826
|
+
/**
|
|
827
|
+
* Optional chrome surface — dispatcher-side overlay layer.
|
|
828
|
+
*
|
|
829
|
+
* An ongoing-action implementation may populate `overlay()` to expose a
|
|
830
|
+
* non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
|
|
831
|
+
* `useDispatcherOverlayLayer` to paint while the gesture is in flight.
|
|
832
|
+
* Returning `null` (or omitting the method) means "no overlay this
|
|
833
|
+
* gesture" — the canvas will skip this handle as a chrome source.
|
|
834
|
+
*
|
|
835
|
+
* Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
|
|
836
|
+
* which paints displaced scene-node silhouettes. Marquee and lasso
|
|
837
|
+
* gestures don't displace any node, but still need on-screen feedback.
|
|
838
|
+
*/
|
|
839
|
+
overlay?(): OngoingOverlay | null;
|
|
840
|
+
}
|
|
841
|
+
/** Fire-once invocation. Runs to completion synchronously (or fires off an
|
|
842
|
+
* async side-effect; the registry doesn't wait). */
|
|
843
|
+
interface ImmediateInvoker {
|
|
844
|
+
timing: 'immediate';
|
|
845
|
+
/** `params` carries the matched binding's opts.params. When
|
|
846
|
+
* invoked via the legacy `Action.run` bridge or from the command palette
|
|
847
|
+
* with no per-binding context, `params` is undefined; descriptors should
|
|
848
|
+
* default to a sensible variant. */
|
|
849
|
+
run(deps: ActionDeps, params?: Record<string, unknown>): void;
|
|
850
|
+
}
|
|
851
|
+
/** Phase-machine invocation. `start` opens the phase and returns the handle
|
|
852
|
+
* the dispatcher pumps. */
|
|
853
|
+
interface OngoingInvoker {
|
|
854
|
+
timing: 'ongoing';
|
|
855
|
+
start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
|
|
856
|
+
}
|
|
857
|
+
/** Pluggable invocation strategy for an Action. Future variants
|
|
858
|
+
* (`longPress`, `twoStage`, `modal`) extend this union without touching
|
|
859
|
+
* the `Action` type. */
|
|
860
|
+
type Invoker = ImmediateInvoker | OngoingInvoker;
|
|
861
|
+
|
|
862
|
+
/** Boolean op identifiers — five Pathfinder primaries plus Crop. */
|
|
863
|
+
type BooleanOp = 'union' | 'intersect' | 'subtract' | 'exclude' | 'divide' | 'crop';
|
|
864
|
+
/**
|
|
865
|
+
* z-position descriptor for a path node. `parentId` is the direct parent
|
|
866
|
+
* (or `null` for a top-level node); `index` is the position within that
|
|
867
|
+
* parent's child order. Used by the optional `getZOrder` hook below to
|
|
868
|
+
* reposition the result of a boolean op at the topmost source's slot.
|
|
869
|
+
*/
|
|
870
|
+
/** @internal */
|
|
871
|
+
interface BooleanZOrder {
|
|
872
|
+
parentId: string | null;
|
|
873
|
+
index: number;
|
|
874
|
+
}
|
|
875
|
+
/** Adapter the hook and the pure core both consume. */
|
|
876
|
+
interface BooleansAdapter {
|
|
877
|
+
getSelection(): NodeId[];
|
|
878
|
+
getWorldPath(id: NodeId): Path | undefined;
|
|
879
|
+
compareZ(a: NodeId, b: NodeId): number;
|
|
880
|
+
/**
|
|
881
|
+
* Mint a new node from a boolean-op result `Path`. `producedBy` names the
|
|
882
|
+
* op that synthesized it — adapters that store provenance (e.g. for a
|
|
883
|
+
* layer-panel icon) record it; others ignore the arg.
|
|
884
|
+
*/
|
|
885
|
+
createPathNode(path: Path, producedBy: BooleanOp): {
|
|
886
|
+
id: string;
|
|
887
|
+
};
|
|
888
|
+
/**
|
|
889
|
+
* Optional: return the full object for an id, used by the delete ops so
|
|
890
|
+
* their `invert` (an insert) can restore the complete object on undo.
|
|
891
|
+
* If omitted, a `{ id }` stub is captured — undo will reinstate the id
|
|
892
|
+
* but consumers reading other fields (path, fill, etc.) will see them as
|
|
893
|
+
* undefined. Mirrors `DeleteAdapter.getNode`; should be provided whenever
|
|
894
|
+
* undo over boolean ops is expected to be lossless.
|
|
895
|
+
*/
|
|
896
|
+
getNode?(id: NodeId): {
|
|
897
|
+
id: string;
|
|
898
|
+
} | undefined | null;
|
|
899
|
+
/**
|
|
900
|
+
* Optional: return the parent + child-index of `id` so the result of a
|
|
901
|
+
* boolean op can be placed in the topmost source's z-slot. Adapters that
|
|
902
|
+
* also expose `getChildren`/`setChildOrder` (the `ReorderAdapter`
|
|
903
|
+
* contract) will have the kit emit a `createMoveToIndexOp` after the
|
|
904
|
+
* inserts. Adapters that omit this method get v1 behavior — the result
|
|
905
|
+
* lands wherever the adapter's plain `insertNode` defaults to.
|
|
906
|
+
*/
|
|
907
|
+
getZOrder?(id: NodeId): BooleanZOrder | undefined;
|
|
908
|
+
applyOps?(ops: Op[], label?: string): void;
|
|
909
|
+
setSelection?(ids: NodeId[]): void;
|
|
910
|
+
insertNode?(node: {
|
|
911
|
+
id: string;
|
|
912
|
+
}): void;
|
|
913
|
+
removeNode?(id: string): void;
|
|
914
|
+
}
|
|
915
|
+
/** Outcome reported back to callers (lets the hook surface no-op signals). */
|
|
916
|
+
type BooleanOpResult = {
|
|
917
|
+
kind: 'applied';
|
|
918
|
+
resultIds: string[];
|
|
919
|
+
} | {
|
|
920
|
+
kind: 'noop';
|
|
921
|
+
reason: 'no-paths' | 'too-few-for-subtract' | 'empty-result';
|
|
922
|
+
};
|
|
923
|
+
declare function applyBooleanOp(adapter: BooleansAdapter, op: BooleanOp): BooleanOpResult;
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Selection click policy. `single` always replaces; `multi` toggles when the
|
|
927
|
+
* configured extend key is held, otherwise replaces.
|
|
928
|
+
*/
|
|
929
|
+
type SelectionMode = 'single' | 'multi';
|
|
930
|
+
/** Modifier key used to extend the selection in `multi` mode. */
|
|
931
|
+
type SelectionExtendKey = 'shift' | 'meta' | 'ctrl';
|
|
932
|
+
/** API returned by {@link useSelection}. */
|
|
933
|
+
interface SelectionApi {
|
|
934
|
+
/** Current selection. Re-renders trigger when this reference changes. */
|
|
935
|
+
current: readonly NodeId[];
|
|
936
|
+
/** Imperative read for use inside event callbacks (avoids stale closures). */
|
|
937
|
+
get(): NodeId[];
|
|
938
|
+
/** Replace selection. */
|
|
939
|
+
set(ids: NodeId[]): void;
|
|
940
|
+
/** Add id (multi-mode appends; single-mode replaces). */
|
|
941
|
+
add(id: NodeId): void;
|
|
942
|
+
/** Remove id from selection. */
|
|
943
|
+
remove(id: NodeId): void;
|
|
944
|
+
/** Toggle id in/out of selection. */
|
|
945
|
+
toggle(id: NodeId): void;
|
|
946
|
+
/** Clear selection. */
|
|
947
|
+
clear(): void;
|
|
948
|
+
/** True if id is selected. */
|
|
949
|
+
contains(id: NodeId): boolean;
|
|
950
|
+
/**
|
|
951
|
+
* Apply a click to the selection per the configured mode/extend key.
|
|
952
|
+
* - `single`: replaces selection with `[id]`, regardless of modifiers.
|
|
953
|
+
* - `multi`: with the extend key held, toggles `id` in/out of the selection;
|
|
954
|
+
* otherwise replaces with `[id]`.
|
|
955
|
+
*/
|
|
956
|
+
applyClick(id: NodeId, modifiers: {
|
|
957
|
+
shift: boolean;
|
|
958
|
+
meta: boolean;
|
|
959
|
+
ctrl: boolean;
|
|
960
|
+
}): void;
|
|
961
|
+
/** Pre-built methods for spreading into an adapter that needs them. */
|
|
962
|
+
adapterMethods: {
|
|
963
|
+
getSelection: () => NodeId[];
|
|
964
|
+
setSelection: (ids: NodeId[]) => void;
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
/** Options for {@link useSelection}. */
|
|
968
|
+
interface UseSelectionOptions {
|
|
969
|
+
/** Default `'single'`. */
|
|
970
|
+
mode?: SelectionMode;
|
|
971
|
+
/** Default `'shift'`. Ignored in single-mode. */
|
|
972
|
+
extend?: SelectionExtendKey;
|
|
973
|
+
/** Default `[]`. */
|
|
974
|
+
initial?: readonly NodeId[];
|
|
975
|
+
/** When `true`, every mutator (`set`/`add`/`remove`/`toggle`/`clear`/
|
|
976
|
+
* `applyClick`) is a no-op — selection stays at whatever `initial`
|
|
977
|
+
* pinned it to. Useful for demos that exist to showcase a single
|
|
978
|
+
* pre-selected node (e.g. the bezier-edit curve) and don't want a
|
|
979
|
+
* stray click to deselect. */
|
|
980
|
+
lock?: boolean;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Default implementation of the `getSelection` / `setSelection` adapter
|
|
984
|
+
* contract every action hook (delete, duplicate, nudge, group, ...) requires.
|
|
985
|
+
*
|
|
986
|
+
* Owns selection state, exposes a click-policy helper (single vs multi with
|
|
987
|
+
* an extend key), and pre-builds the two adapter methods consumers otherwise
|
|
988
|
+
* hand-roll in every demo:
|
|
989
|
+
*
|
|
990
|
+
* ```tsx
|
|
991
|
+
* const selection = useSelection({ mode: 'multi' });
|
|
992
|
+
* const adapter = { ...arrayAdapter({...}), ...selection.adapterMethods };
|
|
993
|
+
* ```
|
|
994
|
+
*/
|
|
995
|
+
declare function useSelection(opts?: UseSelectionOptions): SelectionApi;
|
|
996
|
+
|
|
997
|
+
/** Context handed to every content handler for one ingest event. */
|
|
998
|
+
interface IngestCtx {
|
|
999
|
+
/** World-space arrival point (drop / pointed imperative ingest); `null`
|
|
1000
|
+
* for paste and point-less calls — handlers pick their own policy
|
|
1001
|
+
* (the kit image handler centers on the viewport). */
|
|
1002
|
+
point: {
|
|
1003
|
+
x: number;
|
|
1004
|
+
y: number;
|
|
1005
|
+
} | null;
|
|
1006
|
+
/** Visible canvas area in world coordinates. */
|
|
1007
|
+
viewportWorldRect(): {
|
|
1008
|
+
x: number;
|
|
1009
|
+
y: number;
|
|
1010
|
+
width: number;
|
|
1011
|
+
height: number;
|
|
1012
|
+
};
|
|
1013
|
+
/** The kit insert dep — id/layer/undoable-op supplied; the canonical way
|
|
1014
|
+
* for a handler to mint a node (`insert.commit(bounds, { kind, ... })`). */
|
|
1015
|
+
insert: InsertDep;
|
|
1016
|
+
/** Raw op commit for handlers that build their own ops. */
|
|
1017
|
+
applyOps(ops: Op[], label?: string): void;
|
|
1018
|
+
scene: Scene<unknown, string, unknown>;
|
|
1019
|
+
selection: SelectionApi;
|
|
1020
|
+
/** Consumer file→src resolver (SceneCanvas `ingestion.resolveSrc`).
|
|
1021
|
+
* When absent, the kit image handler embeds as a `data:` URI. */
|
|
1022
|
+
resolveSrc?: (file: File) => Promise<string>;
|
|
1023
|
+
/** Kit SVG-handler options (SceneCanvas `ingestion.svg`) — e.g.
|
|
1024
|
+
* `{ unpack: unpackSvgFiles }` (from `@weasel-js/svg`) to parse SVG files
|
|
1025
|
+
* into scene nodes. */
|
|
1026
|
+
svg?: SvgIngestOptions;
|
|
1027
|
+
/** Clipboard-paste seam — present when the hosting `SceneCanvas` supplied
|
|
1028
|
+
* an adapter with `commitPaste`. `reviver` comes from
|
|
1029
|
+
* `SceneCanvasProps.ingestion.clipboard`. Absent ⇒ the kit weasel-JSON
|
|
1030
|
+
* handler declines inert (dwarn, nothing ingested) — its matched items
|
|
1031
|
+
* were already consumed at match time, so they do NOT fall through;
|
|
1032
|
+
* only match-level misses flow on to other handlers. */
|
|
1033
|
+
clipboard?: ClipboardIngestCtx;
|
|
1034
|
+
/** Set to `true` by the kit weasel-JSON handler when it successfully
|
|
1035
|
+
* pastes a payload in this event. The `ctx` object is shared across all
|
|
1036
|
+
* handlers in one `runIngest` call, and higher-priority handlers' `handle`
|
|
1037
|
+
* bodies run (synchronously) before lower ones — so `kit:svg`'s
|
|
1038
|
+
* `text/plain` SVG fallback reads this to decline the SVG flavor of a copy
|
|
1039
|
+
* whose canonical weasel-JSON flavor already ingested (avoids a
|
|
1040
|
+
* double-paste when both flavors ride one clipboard event). */
|
|
1041
|
+
consumedWeaselPayload?: boolean;
|
|
1042
|
+
/** Full action-deps bag, for consumer handlers that need more. */
|
|
1043
|
+
deps: ActionDeps;
|
|
1044
|
+
}
|
|
1045
|
+
interface ContentHandlerEntry {
|
|
1046
|
+
/** Stable identifier — used for unregistration and debugging
|
|
1047
|
+
* (`'kit:image'`, `'app:csv'`). */
|
|
1048
|
+
id: string;
|
|
1049
|
+
/** MIME glob(s) (`'image/*'`, `'text/csv'`) or an item predicate. */
|
|
1050
|
+
match: string | string[] | ((item: IngestItem) => boolean);
|
|
1051
|
+
/** Higher runs earlier. Kit defaults register at -100 so any consumer
|
|
1052
|
+
* handler (default 0) beats them. */
|
|
1053
|
+
priority?: number;
|
|
1054
|
+
handle(items: IngestItem[], ctx: IngestCtx): void | Promise<void>;
|
|
1055
|
+
}
|
|
1056
|
+
/** Register a content handler. Returns a disposer that removes it. */
|
|
1057
|
+
declare function registerContentHandler(entry: ContentHandlerEntry): () => void;
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* @experimental
|
|
1061
|
+
* PointerContext — a tiny ambient context that publishes the world-space
|
|
1062
|
+
* position of the canvas pointer, refreshed on every `pointermove` over
|
|
1063
|
+
* the canvas. Cleared (set to `null`) on `pointerleave`.
|
|
1064
|
+
*
|
|
1065
|
+
* Why ref-based and not state-based: cursor moves fire dozens of times per
|
|
1066
|
+
* second; routing those through React state would re-render every consumer
|
|
1067
|
+
* in the tree. The context exposes a stable `pointerRef` whose `.current`
|
|
1068
|
+
* is mutated directly by the publisher, plus a thunk `getDropPoint()` that
|
|
1069
|
+
* reads it on demand. Consumers (e.g. `useClipboard`) pull via the thunk
|
|
1070
|
+
* inside their callbacks — no subscription, no re-render.
|
|
1071
|
+
*
|
|
1072
|
+
* `<SceneCanvas>` publishes automatically. `useClipboardOps` consumes when
|
|
1073
|
+
* the caller didn't pass an explicit `getDropPoint` option. Other future
|
|
1074
|
+
* hit-on-cursor consumers (drop-zone hover, context-menu anchor) can reuse
|
|
1075
|
+
* the same context.
|
|
1076
|
+
*/
|
|
1077
|
+
|
|
1078
|
+
/** @experimental World-space pointer position, or `null` when the pointer
|
|
1079
|
+
* isn't over the publishing canvas. */
|
|
1080
|
+
type PointerWorldPos = {
|
|
1081
|
+
worldX: number;
|
|
1082
|
+
worldY: number;
|
|
1083
|
+
} | null;
|
|
1084
|
+
/** @experimental */
|
|
1085
|
+
interface PointerContextValue {
|
|
1086
|
+
/** Live ref — mutate to publish, read for the latest snapshot. The
|
|
1087
|
+
* identity is stable for the lifetime of the provider. */
|
|
1088
|
+
readonly pointerRef: MutableRefObject<PointerWorldPos>;
|
|
1089
|
+
/** Convenience thunk equivalent to `() => pointerRef.current`. Stable
|
|
1090
|
+
* identity for the lifetime of the provider; safe to pass to hooks. */
|
|
1091
|
+
readonly getDropPoint: () => PointerWorldPos;
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* @experimental
|
|
1095
|
+
* Wrap the part of the React tree that should share a pointer-position
|
|
1096
|
+
* context. Usually placed at the demo / app root, alongside
|
|
1097
|
+
* `<ActionsProvider>` and `<SelectionContextProvider>`.
|
|
1098
|
+
*
|
|
1099
|
+
* Most consumers don't need to mount this directly — `<SceneCanvas>` mounts
|
|
1100
|
+
* an internal provider when no parent provider is in scope, so child hooks
|
|
1101
|
+
* (`useClipboard` without an explicit `getDropPoint`) read the canvas's
|
|
1102
|
+
* tracked pointer for free.
|
|
1103
|
+
*/
|
|
1104
|
+
declare function PointerContextProvider({ children }: {
|
|
1105
|
+
children: ReactNode;
|
|
1106
|
+
}): ReactNode;
|
|
1107
|
+
/** @experimental Read the surrounding pointer-context value, or `null` when
|
|
1108
|
+
* no provider is in scope. */
|
|
1109
|
+
declare function usePointerContext(): PointerContextValue | null;
|
|
1110
|
+
|
|
1111
|
+
interface ActiveToolContextValue {
|
|
1112
|
+
active: string;
|
|
1113
|
+
hotkeyStack: string[];
|
|
1114
|
+
setActive(id: string): void;
|
|
1115
|
+
pushHotkey(id: string): void;
|
|
1116
|
+
popHotkey(): void;
|
|
1117
|
+
}
|
|
1118
|
+
interface ActiveToolContextProviderProps {
|
|
1119
|
+
children: ReactNode;
|
|
1120
|
+
initialActive?: string;
|
|
1121
|
+
}
|
|
1122
|
+
declare function ActiveToolContextProvider({ children, initialActive, }: ActiveToolContextProviderProps): react_jsx_runtime.JSX.Element;
|
|
1123
|
+
declare function useActiveToolContext(): ActiveToolContextValue;
|
|
1124
|
+
/**
|
|
1125
|
+
* Like `useActiveToolContext`, but returns `null` when no
|
|
1126
|
+
* `<ActiveToolContextProvider>` is in scope instead of throwing. Used by
|
|
1127
|
+
* `useStandardActions` to preserve its silent-no-op contract when no provider
|
|
1128
|
+
* is present.
|
|
1129
|
+
*/
|
|
1130
|
+
declare function useOptionalActiveToolContext(): ActiveToolContextValue | null;
|
|
1131
|
+
/**
|
|
1132
|
+
* Conditional `<ActiveToolContextProvider>` wrapper. Mounts a provider only
|
|
1133
|
+
* when no parent provider is in scope — otherwise renders children unwrapped
|
|
1134
|
+
* so callers (e.g. `<WeaselProvider>`, `<SceneCanvas>`) defer to the host's
|
|
1135
|
+
* existing scope. Mirrors `ActionsProviderIfRoot` / `DepRegistryProviderIfRoot`.
|
|
1136
|
+
*/
|
|
1137
|
+
declare function ActiveToolContextProviderIfRoot({ children, }: {
|
|
1138
|
+
children: ReactNode;
|
|
1139
|
+
}): react_jsx_runtime.JSX.Element;
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* `enterTextEditAction` — immediate Action descriptor for entering in-place
|
|
1143
|
+
* text editing on a selected text node.
|
|
1144
|
+
*
|
|
1145
|
+
* ## Status: REAL
|
|
1146
|
+
*
|
|
1147
|
+
* Fires via `useTextTool.bindings` when the user clicks on a
|
|
1148
|
+
* selected text node. Calls `deps.textEdit.startEdit(id)` to activate the
|
|
1149
|
+
* contenteditable overlay managed by `useTextEdit` / `useSceneTextEdit`.
|
|
1150
|
+
*
|
|
1151
|
+
* ## No defaultBinding / defaultBinding
|
|
1152
|
+
*
|
|
1153
|
+
* This action has no ambient key or gesture binding — it fires ONLY via
|
|
1154
|
+
* `useTextTool`'s `Tool.bindings` entry:
|
|
1155
|
+
*
|
|
1156
|
+
* ```ts
|
|
1157
|
+
* bindings: [
|
|
1158
|
+
* { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' },
|
|
1159
|
+
* ]
|
|
1160
|
+
* ```
|
|
1161
|
+
*
|
|
1162
|
+
* Keeping it binding-free avoids ambient double-fire and scopes the action to
|
|
1163
|
+
* the text tool context where `classifyTarget` is already wired.
|
|
1164
|
+
*
|
|
1165
|
+
* ## Self-guard: only act on text nodes
|
|
1166
|
+
*
|
|
1167
|
+
* The `'selected-body'` target yields a match for any selected node kind. To
|
|
1168
|
+
* avoid entering text-edit mode when the text tool happens to have a non-text
|
|
1169
|
+
* node selected, the action self-guards via an optional `isTextNode` predicate
|
|
1170
|
+
* on `TextEditDep`:
|
|
1171
|
+
*
|
|
1172
|
+
* - When `isTextNode` is absent: action fires unconditionally (the binding
|
|
1173
|
+
* spec is the real gate — consumers should only bind this action from the
|
|
1174
|
+
* text tool).
|
|
1175
|
+
* - When `isTextNode(id)` returns `false`: action is a no-op for that node.
|
|
1176
|
+
*
|
|
1177
|
+
* ### Pre-filtering at dispatch time
|
|
1178
|
+
*
|
|
1179
|
+
* `classifyTarget` now surfaces node kind, so a binding can pre-filter instead
|
|
1180
|
+
* of relying on the self-guard:
|
|
1181
|
+
*
|
|
1182
|
+
* ```ts
|
|
1183
|
+
* { spec: { kind: 'click', target: 'kind:text:selected' }, actionId: 'enterTextEdit' }
|
|
1184
|
+
* ```
|
|
1185
|
+
*
|
|
1186
|
+
* That reads the *routing trait's* kind, so it matches whatever names the
|
|
1187
|
+
* consumer registered in `<SceneCanvas routing>` — `'text'` under the kit's
|
|
1188
|
+
* inferred default. `isTextNode` stays on `TextEditDep` because it also covers
|
|
1189
|
+
* consumers who bind the broader `'selected-body'` target, and because it is
|
|
1190
|
+
* the only guard for a consumer who opted out of routing entirely.
|
|
1191
|
+
*
|
|
1192
|
+
* ## Migration plan for useTextTool
|
|
1193
|
+
*
|
|
1194
|
+
* When wiring `useTextTool` to `Tool.bindings`:
|
|
1195
|
+
*
|
|
1196
|
+
* 1. Add to `useTextTool`'s `bindings`:
|
|
1197
|
+
* ```ts
|
|
1198
|
+
* { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' }
|
|
1199
|
+
* ```
|
|
1200
|
+
* 2. Register a `textEdit` dep sourced from the `useTextEdit` / `useSceneTextEdit`
|
|
1201
|
+
* return value, plus an `isTextNode` predicate that checks `data.kind === 'text'`
|
|
1202
|
+
* (or however the consumer identifies text nodes).
|
|
1203
|
+
* 3. The existing `hitExisting` gate in `useTextTool`'s click route becomes
|
|
1204
|
+
* redundant — remove it in the same pass.
|
|
1205
|
+
*/
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Dep for `enterTextEditAction`.
|
|
1209
|
+
*
|
|
1210
|
+
* Wrap the return value of `useTextEdit` / `useSceneTextEdit` to source this
|
|
1211
|
+
* dep. The `isTextNode` predicate is optional — when absent the action fires
|
|
1212
|
+
* unconditionally (the binding spec acts as the gate).
|
|
1213
|
+
*
|
|
1214
|
+
* @example
|
|
1215
|
+
* ```ts
|
|
1216
|
+
* const textEdit = useSceneTextEdit({ scene, container });
|
|
1217
|
+
* useDepSource('textEdit', () => ({
|
|
1218
|
+
* startEdit: textEdit.startEdit,
|
|
1219
|
+
* isTextNode: (id) => scene.get(id as NodeId)?.data?.kind === 'text',
|
|
1220
|
+
* }));
|
|
1221
|
+
* ```
|
|
1222
|
+
*/
|
|
1223
|
+
interface TextEditDep {
|
|
1224
|
+
/**
|
|
1225
|
+
* Begin editing the node with `id`. Activates the contenteditable overlay
|
|
1226
|
+
* managed by `useTextEdit` / `useSceneTextEdit`.
|
|
1227
|
+
*/
|
|
1228
|
+
startEdit(id: string, opts?: {
|
|
1229
|
+
caret?: number | 'all';
|
|
1230
|
+
}): void;
|
|
1231
|
+
/**
|
|
1232
|
+
* Optional predicate: returns `true` when the node with `id` is a text node.
|
|
1233
|
+
* When absent the action fires on any selected node (binding spec is the gate).
|
|
1234
|
+
* When present and returning `false`, the invocation is a no-op.
|
|
1235
|
+
*/
|
|
1236
|
+
isTextNode?(id: string): boolean;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* @experimental
|
|
1240
|
+
* Static descriptor for the `enterTextEdit` Action.
|
|
1241
|
+
*
|
|
1242
|
+
* Requires dep-schema entries: `textEdit`, `selection`.
|
|
1243
|
+
*
|
|
1244
|
+
* No `defaultBinding` / `defaultBinding` — fires only via `Tool.bindings`.
|
|
1245
|
+
* Self-guards via `TextEditDep.isTextNode` when provided.
|
|
1246
|
+
*/
|
|
1247
|
+
declare const enterTextEditAction: Action & {
|
|
1248
|
+
requires: string[];
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Consumer-supplied commit for the Slice action. `commit` receives the finite
|
|
1253
|
+
* slice segment (world coords); the consumer scans the scene, splits crossed
|
|
1254
|
+
* paths via `splitPathByLine`, and applies the result as one undoable batch.
|
|
1255
|
+
*/
|
|
1256
|
+
interface SliceDep {
|
|
1257
|
+
commit(a: Point2, b: Point2): void;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* @experimental
|
|
1261
|
+
* Static descriptor for the `slice` Action.
|
|
1262
|
+
*
|
|
1263
|
+
* Ongoing drag invoker: tracks a slice line from drag start to current
|
|
1264
|
+
* pointer, renders a live line overlay while the gesture is in flight,
|
|
1265
|
+
* and on commit calls `SliceDep.commit(a, b)`. No-ops gracefully when
|
|
1266
|
+
* the `slice` dep is absent.
|
|
1267
|
+
*/
|
|
1268
|
+
declare const sliceAction: Action & {
|
|
1269
|
+
requires: string[];
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
/** Optional consumer seam: given a node and the affine `m` that a pose-transform
|
|
1273
|
+
* action applied to the node's POSE, return updated `data` with the node's
|
|
1274
|
+
* data-held geometry transformed by `m`, or `null` if this node has no
|
|
1275
|
+
* data-held geometry (the kit leaves `data` alone). */
|
|
1276
|
+
interface GeometryProjection {
|
|
1277
|
+
transform(node: {
|
|
1278
|
+
id?: string;
|
|
1279
|
+
data: unknown;
|
|
1280
|
+
pose: unknown;
|
|
1281
|
+
}, m: Mat3): unknown | null;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/** Minimal view API the action layer consumes. May be refined later. */
|
|
1285
|
+
interface ViewApi {
|
|
1286
|
+
get(): View;
|
|
1287
|
+
set(v: View): void;
|
|
1288
|
+
/** Optional recenter callback. When wired, `viewportZoomAction`'s
|
|
1289
|
+
* `reset` branch (Cmd-0) calls this instead of resetting to identity —
|
|
1290
|
+
* letting consumers re-fit the page (or other reference bounds) into
|
|
1291
|
+
* the workspace. Receives no args; the consumer reads its own bounds
|
|
1292
|
+
* + host dims and dispatches `setView(...)`. */
|
|
1293
|
+
recenter?(): void;
|
|
1294
|
+
/** Optional canvas-local host dimensions (CSS px). When wired,
|
|
1295
|
+
* `viewportZoomAction`'s keyboard branches (Cmd+= / Cmd+-) anchor at the
|
|
1296
|
+
* host center instead of the top-left origin. Null when the host isn't
|
|
1297
|
+
* measurable (unmounted). */
|
|
1298
|
+
hostSize?(): {
|
|
1299
|
+
width: number;
|
|
1300
|
+
height: number;
|
|
1301
|
+
} | null;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Adapter dep for `areaSelectAction`.
|
|
1305
|
+
*
|
|
1306
|
+
* Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>` via AABB
|
|
1307
|
+
* overlap over scene nodes. Consumers with custom hit-testing override this
|
|
1308
|
+
* dep entry in their own registrar.
|
|
1309
|
+
*/
|
|
1310
|
+
/**
|
|
1311
|
+
* Topmost-node-at-world-point dep, consumed by `moveAction` for
|
|
1312
|
+
* reparent-on-drop and available to any action that needs a single-best
|
|
1313
|
+
* pick. Mirrors the same hit-test plumbing `<SceneCanvas>` feeds to the
|
|
1314
|
+
* tool dispatcher; consumers with custom hit-testing override here.
|
|
1315
|
+
*
|
|
1316
|
+
* `exclude` is iterated once per call and treated as a set membership
|
|
1317
|
+
* test — the dep walks hits front-to-back and returns the first id not
|
|
1318
|
+
* in the exclude set. Pass moving-node roots + their descendants when
|
|
1319
|
+
* the caller wants to ignore the nodes it's manipulating.
|
|
1320
|
+
*/
|
|
1321
|
+
type NodeAtPointDep = (point: {
|
|
1322
|
+
x: number;
|
|
1323
|
+
y: number;
|
|
1324
|
+
}, exclude?: Iterable<NodeId>) => NodeId | null;
|
|
1325
|
+
interface AreaSelectDep {
|
|
1326
|
+
/** Return ids of all scene nodes whose AABB overlaps `bounds`. */
|
|
1327
|
+
hitTestArea(bounds: {
|
|
1328
|
+
x: number;
|
|
1329
|
+
y: number;
|
|
1330
|
+
width: number;
|
|
1331
|
+
height: number;
|
|
1332
|
+
}): NodeId[];
|
|
1333
|
+
/** Return the current selection id list. */
|
|
1334
|
+
getSelection(): NodeId[];
|
|
1335
|
+
/** Replace the current selection. */
|
|
1336
|
+
setSelection(ids: NodeId[]): void;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Adapter dep for `editAnchorsAction`.
|
|
1340
|
+
*
|
|
1341
|
+
* Provides narrow read/write access to the editable polygon for a single
|
|
1342
|
+
* node. Consumers register this dep so anchor-edit actions can read/write
|
|
1343
|
+
* the polygon WITHOUT knowing whether it lives directly on the node's
|
|
1344
|
+
* pose (`pose.kind === 'polygon'`) or on `node.data.path` (the kit's
|
|
1345
|
+
* built-in pen-tool default, also WeaselDraw's shape).
|
|
1346
|
+
*
|
|
1347
|
+
* Note on live previews: in-flight edit state is surfaced through the
|
|
1348
|
+
* dispatcher's standard `OngoingHandle.previewIds/previewPose/previewData`
|
|
1349
|
+
* triple (not this dep), so chrome and preview-ghost stay in lock-step
|
|
1350
|
+
* via one source of truth.
|
|
1351
|
+
*/
|
|
1352
|
+
interface EditAnchorsDep {
|
|
1353
|
+
/** Id of the node currently being edited. Empty string means no node is
|
|
1354
|
+
* currently in edit mode — the chrome and gesture both opt out. */
|
|
1355
|
+
editingId: string;
|
|
1356
|
+
/** Enter/exit edit mode for a specific node. Pass `null` (or an empty
|
|
1357
|
+
* string) to exit. `enterPathEditAction` and `exitPathEditAction` call
|
|
1358
|
+
* this; consumers can call it directly to drive edit mode programmatically. */
|
|
1359
|
+
setEditingId(id: string | null): void;
|
|
1360
|
+
/** Returns the COMMITTED editable polygon in world coordinates, or
|
|
1361
|
+
* null if this node has no editable polygon. Does NOT consult in-
|
|
1362
|
+
* flight previews — callers that need live state read the dispatcher's
|
|
1363
|
+
* in-flight handles. */
|
|
1364
|
+
getEditablePath(id: string): unknown;
|
|
1365
|
+
/** Returns where the polygon is stored — `'pose'` when `node.pose`
|
|
1366
|
+
* IS the polygon, `'data'` when it lives on `node.data.path` with a
|
|
1367
|
+
* rect pose, or `null` when the node has no editable polygon. The
|
|
1368
|
+
* action uses this to know which preview-ghost axis to populate
|
|
1369
|
+
* (`previewPose` only / `previewData` + `previewPose` for data.path). */
|
|
1370
|
+
getStorageKind(id: string): 'pose' | 'data' | null;
|
|
1371
|
+
/** Returns the node's raw `pose` and `data` so storage-aware actions
|
|
1372
|
+
* can capture origin state at gesture-start and synthesize a matching
|
|
1373
|
+
* `previewPose` / `previewData` during `onMove`. Used by
|
|
1374
|
+
* `editAnchorsAction` for the data.path branch (rect pose + data
|
|
1375
|
+
* carrying extra fields like fill / stroke that must be preserved
|
|
1376
|
+
* through the preview). Returns null when the node is gone. */
|
|
1377
|
+
getNodeShape(id: string): {
|
|
1378
|
+
pose: unknown;
|
|
1379
|
+
data: unknown;
|
|
1380
|
+
} | null;
|
|
1381
|
+
/** Commit `worldPath` as the new value for `id`. Implementation routes
|
|
1382
|
+
* to setPose (when pose IS the polygon) or batched setPose+update
|
|
1383
|
+
* (when the polygon lives on data.path). Records one history entry
|
|
1384
|
+
* labelled `label`. */
|
|
1385
|
+
applyEdit(id: string, worldPath: unknown, label: string): void;
|
|
1386
|
+
/**
|
|
1387
|
+
* Anchors currently selected within the edited path, as **flat anchor
|
|
1388
|
+
* indices** — the same numbering `enumerateAnchors` produces and the
|
|
1389
|
+
* `anchor:N` affordance kinds carry.
|
|
1390
|
+
*
|
|
1391
|
+
* Selection is transient UI state, deliberately not part of the scene:
|
|
1392
|
+
* it is cleared whenever `editingId` changes, and any edit that
|
|
1393
|
+
* renumbers anchors (insert, delete) is responsible for leaving it
|
|
1394
|
+
* coherent. Empty means "no anchor selected" — the keyboard actions
|
|
1395
|
+
* (nudge, delete) no-op rather than acting on all anchors, matching
|
|
1396
|
+
* Illustrator.
|
|
1397
|
+
*/
|
|
1398
|
+
selectedAnchors: ReadonlySet<number>;
|
|
1399
|
+
/** Replace the anchor selection. Pass an empty iterable to clear. */
|
|
1400
|
+
setSelectedAnchors(next: Iterable<number>): void;
|
|
1401
|
+
/**
|
|
1402
|
+
* In-flight anchor-marquee rect in world coords, or null when no
|
|
1403
|
+
* marquee drag is active. Written by `marqueeAnchorsAction` and read by
|
|
1404
|
+
* the path-editing overlay — the same "ongoing action owns the preview,
|
|
1405
|
+
* chrome just draws it" split the move/resize ghosts use.
|
|
1406
|
+
*/
|
|
1407
|
+
marquee: {
|
|
1408
|
+
x: number;
|
|
1409
|
+
y: number;
|
|
1410
|
+
width: number;
|
|
1411
|
+
height: number;
|
|
1412
|
+
} | null;
|
|
1413
|
+
/** Set or clear the in-flight marquee rect. */
|
|
1414
|
+
setMarquee(rect: {
|
|
1415
|
+
x: number;
|
|
1416
|
+
y: number;
|
|
1417
|
+
width: number;
|
|
1418
|
+
height: number;
|
|
1419
|
+
} | null): void;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Adapter dep for `lassoSelectAction`.
|
|
1423
|
+
*
|
|
1424
|
+
* Provides polygon-lasso hit-testing + selection read/write.
|
|
1425
|
+
* Consumers that don't implement `hitTestLasso` can omit it; the action
|
|
1426
|
+
* falls back to a bounding-box AABB test via `hitTestArea`.
|
|
1427
|
+
*/
|
|
1428
|
+
interface LassoSelectDep {
|
|
1429
|
+
/**
|
|
1430
|
+
* Hit-test against a closed polygon (vertex order CW or CCW; last→first
|
|
1431
|
+
* closing edge is implicit). Returns matching node ids.
|
|
1432
|
+
* Optional — when absent, `lassoSelectAction` falls back to AABB via
|
|
1433
|
+
* `hitTestArea`.
|
|
1434
|
+
*/
|
|
1435
|
+
hitTestLasso?(polygon: ReadonlyArray<{
|
|
1436
|
+
x: number;
|
|
1437
|
+
y: number;
|
|
1438
|
+
}>, mode: 'centers' | 'intersect' | 'enclosed'): string[];
|
|
1439
|
+
/** Return ids of nodes whose AABB overlaps the given rect (fallback). */
|
|
1440
|
+
hitTestArea(bounds: {
|
|
1441
|
+
x: number;
|
|
1442
|
+
y: number;
|
|
1443
|
+
width: number;
|
|
1444
|
+
height: number;
|
|
1445
|
+
}): string[];
|
|
1446
|
+
/** Return the current selection id list. */
|
|
1447
|
+
getSelection(): string[];
|
|
1448
|
+
/** Replace the current selection. */
|
|
1449
|
+
setSelection(ids: string[]): void;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Options for the kit `image/svg+xml` content handler, threaded from
|
|
1453
|
+
* SceneCanvas's `ingestion={{ svg }}` prop.
|
|
1454
|
+
*/
|
|
1455
|
+
interface SvgIngestOptions {
|
|
1456
|
+
/** Parse dropped/pasted/picked SVG files into native scene nodes (path /
|
|
1457
|
+
* text leaves under containers mirroring the source `<g>` structure)
|
|
1458
|
+
* instead of the default single embedded-image node.
|
|
1459
|
+
*
|
|
1460
|
+
* Pass `unpackSvgFiles` from `@weasel-js/svg`:
|
|
1461
|
+
*
|
|
1462
|
+
* ```ts
|
|
1463
|
+
* import { unpackSvgFiles } from '@weasel-js/svg';
|
|
1464
|
+
* <SceneCanvas ingestion={{ svg: { unpack: unpackSvgFiles } }} />
|
|
1465
|
+
* ```
|
|
1466
|
+
*
|
|
1467
|
+
* It is injected rather than flagged on with `true` because the SVG parser
|
|
1468
|
+
* lives in `@weasel-js/svg`, which depends on this package — core importing
|
|
1469
|
+
* it back would make the two mutually dependent and unpublishable
|
|
1470
|
+
* separately. Passing the function keeps the parser out of core's bundle
|
|
1471
|
+
* for consumers who never unpack. */
|
|
1472
|
+
unpack?: SvgUnpacker;
|
|
1473
|
+
}
|
|
1474
|
+
/** Parses SVG files and inserts the resulting nodes into `ctx.scene`, as one
|
|
1475
|
+
* `applyOps` batch per file. Implemented by `unpackSvgFiles` in
|
|
1476
|
+
* `@weasel-js/svg`; see {@link SvgIngestOptions.unpack}. */
|
|
1477
|
+
type SvgUnpacker = (files: File[], ctx: IngestCtx) => Promise<void>;
|
|
1478
|
+
/**
|
|
1479
|
+
* Clipboard-paste seam consumed by the kit weasel-JSON content handler
|
|
1480
|
+
* (`IngestCtx.clipboard`). Built by `<SceneCanvas>` from its own synthesized
|
|
1481
|
+
* adapter + the `ingestion.clipboard` prop; absent when the consumer set
|
|
1482
|
+
* `ingestion.clipboard.enabled === false` or the adapter lacks `commitPaste`.
|
|
1483
|
+
* Absence makes the handler decline inert (dwarn, nothing ingested) — its
|
|
1484
|
+
* matched items were already consumed at match time and do not fall through
|
|
1485
|
+
* to other handlers.
|
|
1486
|
+
*/
|
|
1487
|
+
interface ClipboardIngestCtx {
|
|
1488
|
+
/** The hosting canvas's adapter — `commitPaste` materializes the pasted
|
|
1489
|
+
* nodes (fresh ids, offset applied); insertion still goes through ops. */
|
|
1490
|
+
adapter: InsertAdapter<{
|
|
1491
|
+
id: string;
|
|
1492
|
+
}>;
|
|
1493
|
+
/** JSON reviver for the weasel wire payload (typed arrays etc.) — from
|
|
1494
|
+
* `SceneCanvasProps.ingestion.clipboard.reviver`. */
|
|
1495
|
+
reviver?: (key: string, value: unknown) => unknown;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Dep for the `ingest` action (external-content ingestion).
|
|
1499
|
+
* Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
|
|
1500
|
+
* `useIngestionDepSource` — canvas rect + current view.
|
|
1501
|
+
*/
|
|
1502
|
+
interface IngestionDep {
|
|
1503
|
+
/** Visible canvas area in world coordinates. */
|
|
1504
|
+
viewportWorldRect(): {
|
|
1505
|
+
x: number;
|
|
1506
|
+
y: number;
|
|
1507
|
+
width: number;
|
|
1508
|
+
height: number;
|
|
1509
|
+
};
|
|
1510
|
+
/** Consumer file→src resolver (from SceneCanvas's `ingestion` prop).
|
|
1511
|
+
* Live accessor — read it at use time. Destructuring (or copying the
|
|
1512
|
+
* property early) snapshots the current value and won't track later
|
|
1513
|
+
* prop changes across an `await`. */
|
|
1514
|
+
resolveSrc?: (file: File) => Promise<string>;
|
|
1515
|
+
/** Kit SVG-handler options (from SceneCanvas's `ingestion` prop).
|
|
1516
|
+
* Live accessor, same caveat as `resolveSrc`. */
|
|
1517
|
+
svg?: SvgIngestOptions;
|
|
1518
|
+
/** Clipboard-paste seam for the kit weasel-JSON handler.
|
|
1519
|
+
* Live accessor, same caveat as `resolveSrc`. */
|
|
1520
|
+
clipboard?: ClipboardIngestCtx;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Per-kind extra geometry passed to `InsertDep.commit`.
|
|
1524
|
+
*
|
|
1525
|
+
* Built-in tools populate a typed variant so the kit's default factory can
|
|
1526
|
+
* render the true tool params (line endpoints, polygon side count, star
|
|
1527
|
+
* geometry, pencil sample list). Consumer-defined tools may pass any
|
|
1528
|
+
* `{ kind: string; ... }` payload; the kit's factory falls back to AABB
|
|
1529
|
+
* inscription for unknown kinds.
|
|
1530
|
+
*
|
|
1531
|
+
* `bounds` is still passed alongside as a useful AABB pose hint — factories
|
|
1532
|
+
* may use it as the node's pose even when richer geometry is available.
|
|
1533
|
+
*/
|
|
1534
|
+
type InsertExtras = {
|
|
1535
|
+
kind: 'rect';
|
|
1536
|
+
} | {
|
|
1537
|
+
kind: 'ellipse';
|
|
1538
|
+
} | {
|
|
1539
|
+
kind: 'line';
|
|
1540
|
+
a: {
|
|
1541
|
+
x: number;
|
|
1542
|
+
y: number;
|
|
1543
|
+
};
|
|
1544
|
+
b: {
|
|
1545
|
+
x: number;
|
|
1546
|
+
y: number;
|
|
1547
|
+
};
|
|
1548
|
+
} | {
|
|
1549
|
+
kind: 'polygon';
|
|
1550
|
+
sides: number;
|
|
1551
|
+
rotation: number;
|
|
1552
|
+
center?: {
|
|
1553
|
+
x: number;
|
|
1554
|
+
y: number;
|
|
1555
|
+
};
|
|
1556
|
+
radius?: number;
|
|
1557
|
+
} | {
|
|
1558
|
+
kind: 'star';
|
|
1559
|
+
points: number;
|
|
1560
|
+
innerRadiusRatio: number;
|
|
1561
|
+
rotation: number;
|
|
1562
|
+
center?: {
|
|
1563
|
+
x: number;
|
|
1564
|
+
y: number;
|
|
1565
|
+
};
|
|
1566
|
+
outerRadius?: number;
|
|
1567
|
+
} | {
|
|
1568
|
+
kind: 'pencil';
|
|
1569
|
+
samples: ReadonlyArray<DragSample>;
|
|
1570
|
+
} | {
|
|
1571
|
+
kind: 'text';
|
|
1572
|
+
text?: string;
|
|
1573
|
+
} | {
|
|
1574
|
+
kind: string;
|
|
1575
|
+
[extra: string]: unknown;
|
|
1576
|
+
};
|
|
1577
|
+
/**
|
|
1578
|
+
* World-space point snapping — grid, guides, or any consumer rule.
|
|
1579
|
+
*
|
|
1580
|
+
* Sourced by `<SceneCanvas>` from its `toolOptions.snapPoint`. Actions apply
|
|
1581
|
+
* it to the coords they ingest so the live preview and the committed
|
|
1582
|
+
* geometry agree; `insertAction` snaps the drag's start and current point.
|
|
1583
|
+
*
|
|
1584
|
+
* Optional: when the dep is absent, actions treat it as identity.
|
|
1585
|
+
*/
|
|
1586
|
+
interface SnapDep {
|
|
1587
|
+
/** Snap a world-space point. Return `p` unchanged to opt out. */
|
|
1588
|
+
point(p: {
|
|
1589
|
+
x: number;
|
|
1590
|
+
y: number;
|
|
1591
|
+
}): {
|
|
1592
|
+
x: number;
|
|
1593
|
+
y: number;
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Adapter dep for `insertAction`.
|
|
1598
|
+
*
|
|
1599
|
+
* Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>`. The `extras`
|
|
1600
|
+
* carry the active tool's kind + per-kind geometry. Callers
|
|
1601
|
+
* that need typed data must supply a richer `insert` dep.
|
|
1602
|
+
*/
|
|
1603
|
+
interface InsertDep {
|
|
1604
|
+
/**
|
|
1605
|
+
* Materialise a new node from the given drag-rect bounds and typed
|
|
1606
|
+
* per-kind extras. Returns the new node's id, or `null` if the consumer
|
|
1607
|
+
* rejected the insert (e.g. sub-threshold bounds, unknown kind).
|
|
1608
|
+
*/
|
|
1609
|
+
commit(bounds: {
|
|
1610
|
+
x: number;
|
|
1611
|
+
y: number;
|
|
1612
|
+
width: number;
|
|
1613
|
+
height: number;
|
|
1614
|
+
}, extras: InsertExtras): NodeId | null;
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Adapter dep for `resizeAction`.
|
|
1618
|
+
*
|
|
1619
|
+
* Carries the four behavior-shaping options the legacy `useResize` hook
|
|
1620
|
+
* exposed through `UseResizeOptions`: bounds-frame behaviors (e.g.
|
|
1621
|
+
* `lockAspectWithModifier`), world-space anchor-point snap behaviors (e.g.
|
|
1622
|
+
* `pointSnapToGrid`), group-expansion (`expandIds`), and pose↔bounds
|
|
1623
|
+
* projection (`geometry`).
|
|
1624
|
+
*
|
|
1625
|
+
* Optional in `DepSchema`: when absent, `resizeAction` falls back to
|
|
1626
|
+
* identity defaults (no behaviors, identity expandIds, `RECT_POSE_DESCRIPTOR`
|
|
1627
|
+
* geometry). Consumers wire the dep via `useDepSource('resizePolicy', ...)`
|
|
1628
|
+
* from any descendant of `<DepRegistryProvider>` / `<SceneCanvas>`.
|
|
1629
|
+
*
|
|
1630
|
+
* The generic is erased to `unknown` at the schema entry; consumers cast at
|
|
1631
|
+
* the call site (mirrors the `scene` entry's convention).
|
|
1632
|
+
*/
|
|
1633
|
+
interface ResizePolicy<TPose> {
|
|
1634
|
+
/** Bounds-frame constraints. Constrained to `TPose extends ResizePose` since
|
|
1635
|
+
* constraints read/write `{x,y,width,height}`. For non-rect TPose pass `[]`. */
|
|
1636
|
+
constraints: TPose extends ResizePose ? BoundsConstraint<TPose>[] : never[];
|
|
1637
|
+
/** World-space anchor-point snap behaviors. Same TPose constraint as
|
|
1638
|
+
* `constraints`. */
|
|
1639
|
+
pointSnap: TPose extends ResizePose ? PointSnapBehavior<TPose>[] : never[];
|
|
1640
|
+
/** Group-expansion at gesture start. Identity (`ids => ids`) when group
|
|
1641
|
+
* resize isn't wanted. */
|
|
1642
|
+
expandIds: (ids: string[]) => string[];
|
|
1643
|
+
/** Projection from `TPose` to bounds and back. Use `RECT_POSE_DESCRIPTOR`
|
|
1644
|
+
* for plain rect poses. */
|
|
1645
|
+
projection: PoseProjection<TPose>;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Layout-strategy lookup by container id, consumed by `moveAction` to run
|
|
1649
|
+
* the drag-time reflow pass. Sourced by `<SceneCanvas>` from its `layouts`
|
|
1650
|
+
* prop. Optional: `getLayout` returns null for any container when no layout
|
|
1651
|
+
* is configured, so the reflow pass is a no-op then.
|
|
1652
|
+
*/
|
|
1653
|
+
interface LayoutDep {
|
|
1654
|
+
getLayout(containerId: string): LayoutStrategy<unknown> | null;
|
|
1655
|
+
}
|
|
1656
|
+
interface DepSchema {
|
|
1657
|
+
/** Kit selection state — ids of currently selected nodes. */
|
|
1658
|
+
selection: SelectionApi;
|
|
1659
|
+
/** Current viewport — camera position + scale. */
|
|
1660
|
+
view: ViewApi;
|
|
1661
|
+
/**
|
|
1662
|
+
* Scene tree — structural reads + undoable mutations.
|
|
1663
|
+
*
|
|
1664
|
+
* The entry uses the fully-erased form `Scene<unknown, string, unknown>`
|
|
1665
|
+
* because `DepSchema` must be concrete. Actions that need a typed scene
|
|
1666
|
+
* should cast: `deps.scene as Scene<MyData, MyLayer, MyPose>`.
|
|
1667
|
+
*/
|
|
1668
|
+
scene: Scene<unknown, string, unknown>;
|
|
1669
|
+
/** Undo/redo history bound to the current scene. */
|
|
1670
|
+
history: History;
|
|
1671
|
+
/**
|
|
1672
|
+
* Canvas pointer position in world space.
|
|
1673
|
+
*
|
|
1674
|
+
* Exposes `pointerRef` (mutable live ref) and `getDropPoint()` thunk.
|
|
1675
|
+
* Marked `@experimental` in the source.
|
|
1676
|
+
*/
|
|
1677
|
+
pointer: PointerContextValue;
|
|
1678
|
+
/** Currently active tool id + hotkey-hold stack. */
|
|
1679
|
+
activeTool: ActiveToolContextValue;
|
|
1680
|
+
/**
|
|
1681
|
+
* Area-select dep — AABB hit-test + selection read/write.
|
|
1682
|
+
*
|
|
1683
|
+
* Sourced from `<SceneCanvas>` via AABB overlap over all scene
|
|
1684
|
+
* nodes. Override per-consumer for custom hit-testing (e.g. contain-mode,
|
|
1685
|
+
* lock-aware filtering).
|
|
1686
|
+
*/
|
|
1687
|
+
areaSelect: AreaSelectDep;
|
|
1688
|
+
/**
|
|
1689
|
+
* Topmost node at a world-space point. Sourced by `<SceneCanvas>` from
|
|
1690
|
+
* the same picker that feeds the tool dispatcher's `getNodeAtPoint`.
|
|
1691
|
+
* Optional: actions that read this (e.g. `moveAction` reparent-on-drop)
|
|
1692
|
+
* fall back to a no-op when the dep isn't registered.
|
|
1693
|
+
*/
|
|
1694
|
+
nodeAtPoint?: NodeAtPointDep;
|
|
1695
|
+
/**
|
|
1696
|
+
* Insert dep — node factory for drag-to-insert.
|
|
1697
|
+
*
|
|
1698
|
+
* Sourced from `<SceneCanvas>`. The `kind` param comes from
|
|
1699
|
+
* the active binding's `opts.params.kind`. Override per-consumer to
|
|
1700
|
+
* provide a typed node factory (e.g. with custom data payloads).
|
|
1701
|
+
*/
|
|
1702
|
+
insert: InsertDep;
|
|
1703
|
+
/**
|
|
1704
|
+
* Snap dep — world-space point snapping (grid / guides).
|
|
1705
|
+
*
|
|
1706
|
+
* Sourced by `<SceneCanvas>` from `toolOptions.snapPoint`. Optional:
|
|
1707
|
+
* absent means no snapping (identity).
|
|
1708
|
+
*/
|
|
1709
|
+
snap?: SnapDep;
|
|
1710
|
+
/**
|
|
1711
|
+
* Lasso-select dep — polygon hit-test + selection read/write.
|
|
1712
|
+
*
|
|
1713
|
+
* Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>`.
|
|
1714
|
+
* Falls back to AABB hit-test when `hitTestLasso` is absent.
|
|
1715
|
+
*/
|
|
1716
|
+
lassoSelect: LassoSelectDep;
|
|
1717
|
+
/**
|
|
1718
|
+
* Edit-anchors dep — narrow read/write of one polygon's path pose.
|
|
1719
|
+
*
|
|
1720
|
+
* Sourced from consumer. Wraps `getPose`/`setPose`/`applyOps`
|
|
1721
|
+
* for the currently-being-edited polygon node.
|
|
1722
|
+
*
|
|
1723
|
+
* The `editAnchorsAction` requires this dep to be registered when anchor
|
|
1724
|
+
* editing is active. If absent, `start` returns an empty handle (no-op).
|
|
1725
|
+
*/
|
|
1726
|
+
editAnchors: EditAnchorsDep;
|
|
1727
|
+
/**
|
|
1728
|
+
* Text-edit dep — activates the in-place text editing overlay.
|
|
1729
|
+
*
|
|
1730
|
+
* Sourced from consumer via `useTextEdit` / `useSceneTextEdit`.
|
|
1731
|
+
* The `enterTextEditAction` requires this dep to be registered by the text
|
|
1732
|
+
* tool when text editing is available.
|
|
1733
|
+
*
|
|
1734
|
+
* The optional `isTextNode` predicate guards against entering edit mode on
|
|
1735
|
+
* non-text nodes. A binding can pre-filter instead with a
|
|
1736
|
+
* `target: 'kind:text:selected'` spec; the guard remains for consumers who
|
|
1737
|
+
* bind the broader `'selected-body'` target or opted out of routing.
|
|
1738
|
+
*/
|
|
1739
|
+
textEdit: TextEditDep;
|
|
1740
|
+
/**
|
|
1741
|
+
* Resize-policy dep — bounds constraints, point-snap behaviors,
|
|
1742
|
+
* group expansion, and pose↔bounds projection for `resizeAction`.
|
|
1743
|
+
*
|
|
1744
|
+
* Optional: when omitted, `resizeAction` falls back to identity defaults
|
|
1745
|
+
* (no constraints, no snap, identity expandIds, `RECT_POSE_DESCRIPTOR`).
|
|
1746
|
+
* Consumers wire via `useDepSource('resizePolicy', ...)` or the
|
|
1747
|
+
* `useResizePolicy` helper.
|
|
1748
|
+
*/
|
|
1749
|
+
resizePolicy?: ResizePolicy<unknown>;
|
|
1750
|
+
/**
|
|
1751
|
+
* Booleans adapter — read selection ids, fetch world-space `Path`s,
|
|
1752
|
+
* compare z-order, and mint result nodes for Pathfinder ops.
|
|
1753
|
+
*
|
|
1754
|
+
* Consumers wire via `useBooleansAdapter(adapter)` (a thin wrapper
|
|
1755
|
+
* around `useDepSource('booleansAdapter', ...)`). The descriptor's
|
|
1756
|
+
* `enabled` predicate reads `deps.selection` for the count check; the
|
|
1757
|
+
* invoker reads `deps.booleansAdapter` to execute the op.
|
|
1758
|
+
*/
|
|
1759
|
+
booleansAdapter?: BooleansAdapter;
|
|
1760
|
+
/**
|
|
1761
|
+
* Gesture dispatcher control surface — exposes `cancelAll(reason)` so
|
|
1762
|
+
* actions that need to abort an in-flight handle (Escape cancels a
|
|
1763
|
+
* drag, etc.) can do so. Sourced by `<SceneCanvas>` from the
|
|
1764
|
+
* dispatcher instance it already owns.
|
|
1765
|
+
*/
|
|
1766
|
+
dispatcher?: {
|
|
1767
|
+
cancelAll(reason: 'commit' | 'cancel'): void;
|
|
1768
|
+
};
|
|
1769
|
+
/**
|
|
1770
|
+
* Layout-strategy lookup. Sourced by `<SceneCanvas>` from `layouts`.
|
|
1771
|
+
* Optional: absent (or all-null) → `moveAction` skips reflow.
|
|
1772
|
+
*/
|
|
1773
|
+
layout?: LayoutDep;
|
|
1774
|
+
/**
|
|
1775
|
+
* Slice dep — consumer-supplied commit for the Slice action.
|
|
1776
|
+
*
|
|
1777
|
+
* Receives the finite slice segment in world coordinates; the consumer
|
|
1778
|
+
* scans the scene, splits crossed paths via `splitPathByLine`, and
|
|
1779
|
+
* applies the result as one undoable batch.
|
|
1780
|
+
*
|
|
1781
|
+
* Optional: when absent, `sliceAction` is a no-op.
|
|
1782
|
+
*/
|
|
1783
|
+
slice?: SliceDep;
|
|
1784
|
+
/**
|
|
1785
|
+
* Optional consumer commit hook. When present, `moveAction` (and other
|
|
1786
|
+
* default actions) submit their committed ops through it instead of
|
|
1787
|
+
* `scene.applyBatch`, so apps with their own history integration
|
|
1788
|
+
* (checkpoint + push entry) capture the gesture as one undo entry.
|
|
1789
|
+
* When absent, commits fall back to `scene.applyBatch`.
|
|
1790
|
+
*/
|
|
1791
|
+
applyOps?: (ops: Op[], label: string) => void;
|
|
1792
|
+
/** Optional pose-composition strategy for hierarchical (local-pose) scenes.
|
|
1793
|
+
* When absent, defaults to IDENTITY (absolute-pose: nodes store world
|
|
1794
|
+
* coords). Local-pose consumers supply { compose: composeRectPose,
|
|
1795
|
+
* decompose: decomposeRectPose } (or their pose shape's equivalent). */
|
|
1796
|
+
poseComposition?: PoseComposition<unknown>;
|
|
1797
|
+
/**
|
|
1798
|
+
* Ingestion dep — canvas viewport rect + consumer file→src resolver.
|
|
1799
|
+
*
|
|
1800
|
+
* Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
|
|
1801
|
+
* `useIngestionDepSource`. Feeds `ingestAction` with the world-space
|
|
1802
|
+
* viewport rect for paste-placement and image fit-clamping, and forwards
|
|
1803
|
+
* the consumer's optional `resolveSrc` seam.
|
|
1804
|
+
*
|
|
1805
|
+
* Optional: when absent, the `ingest` action no-ops (there is no
|
|
1806
|
+
* placement geometry to work with).
|
|
1807
|
+
*/
|
|
1808
|
+
ingestion?: IngestionDep;
|
|
1809
|
+
/**
|
|
1810
|
+
* Optional consumer seam for the eager-sync layer: lets pose-transform
|
|
1811
|
+
* actions (resize/move/nudge/flip — NOT rotate) ALSO rewrite a node's
|
|
1812
|
+
* data-held geometry. Given a node and the affine `m` applied to its pose,
|
|
1813
|
+
* `transform(node, m)` returns updated `data` (geometry mapped by `m`) or
|
|
1814
|
+
* `null` for nodes with no data-held geometry.
|
|
1815
|
+
*
|
|
1816
|
+
* Strictly opt-in: when absent (or when `transform` returns null), the kit
|
|
1817
|
+
* emits only the pose op and leaves `data` untouched. apps/draw wires this
|
|
1818
|
+
* to mirror `data.path` through `transformPath`. Rotate intentionally never
|
|
1819
|
+
* consults this seam (rotation lives on the pose, baked at render).
|
|
1820
|
+
*/
|
|
1821
|
+
geometryProjection?: GeometryProjection;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Every dep name the registry knows about — derived from {@link DepSchema} so
|
|
1825
|
+
* the two can't drift.
|
|
1826
|
+
*
|
|
1827
|
+
* Declared here rather than beside the registry so that this `keyof` reference
|
|
1828
|
+
* resolves to the exported `DepSchema` declaration; from another module it
|
|
1829
|
+
* resolves to that module's import alias, which the API docs can't link.
|
|
1830
|
+
*/
|
|
1831
|
+
type DepName = keyof DepSchema;
|
|
1832
|
+
|
|
1833
|
+
interface DepRegistry {
|
|
1834
|
+
register<K extends DepName>(name: K, source: () => DepSchema[K]): () => void;
|
|
1835
|
+
get<K extends DepName>(name: K): DepSchema[K] | undefined;
|
|
1836
|
+
}
|
|
1837
|
+
declare function DepRegistryProvider({ children }: {
|
|
1838
|
+
children: ReactNode;
|
|
1839
|
+
}): react_jsx_runtime.JSX.Element;
|
|
1840
|
+
declare function useDepRegistry(): DepRegistry;
|
|
1841
|
+
/**
|
|
1842
|
+
* Like `useDepRegistry`, but returns `null` when no `<DepRegistryProvider>` is
|
|
1843
|
+
* in scope instead of throwing. Used by `useStandardActions` to preserve its
|
|
1844
|
+
* silent-no-op contract when neither provider is present.
|
|
1845
|
+
*/
|
|
1846
|
+
declare function useOptionalDepRegistry(): DepRegistry | null;
|
|
1847
|
+
/** Register a live source for `name` for the lifetime of the calling
|
|
1848
|
+
* component. The `source` thunk is called at dispatch time and should
|
|
1849
|
+
* return the latest value. */
|
|
1850
|
+
declare function useDepSource<K extends DepName>(name: K, source: () => DepSchema[K]): void;
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* GestureBinding — connects a GestureSpec to an Action id (with per-binding
|
|
1854
|
+
* options). Tools own arrays of these on their `bindings` field; ambient
|
|
1855
|
+
* gesture-bindings are registered globally.
|
|
1856
|
+
*
|
|
1857
|
+
* See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
|
|
1858
|
+
*/
|
|
1859
|
+
|
|
1860
|
+
interface GestureBinding {
|
|
1861
|
+
spec: GestureSpec;
|
|
1862
|
+
actionId: string;
|
|
1863
|
+
opts?: BindingOpts;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/** Modifier-key snapshot at event dispatch time. `space` is included
|
|
1867
|
+
* because tools commonly use space as a hotkey-slot trigger and may
|
|
1868
|
+
* also want to read it as a flag mid-gesture. */
|
|
1869
|
+
interface ToolModifiers {
|
|
1870
|
+
alt: boolean;
|
|
1871
|
+
shift: boolean;
|
|
1872
|
+
meta: boolean;
|
|
1873
|
+
ctrl: boolean;
|
|
1874
|
+
space: boolean;
|
|
1875
|
+
}
|
|
1876
|
+
/** Per-event context passed to every channel handler. `scratch` is typed
|
|
1877
|
+
* via the tool's `TScratch` parameter; it survives across a single
|
|
1878
|
+
* gesture (pointer-down through end/cancel) and is replaced on next
|
|
1879
|
+
* gesture start by `initScratch()`. */
|
|
1880
|
+
interface ToolCtx<TScratch = unknown> {
|
|
1881
|
+
worldX: number;
|
|
1882
|
+
worldY: number;
|
|
1883
|
+
modifiers: ToolModifiers;
|
|
1884
|
+
selection: SelectionApi;
|
|
1885
|
+
/** Adapter/scene access — opaque at this layer; tools that need it
|
|
1886
|
+
* cast to a known shape. This layer doesn't constrain it. */
|
|
1887
|
+
adapter: unknown;
|
|
1888
|
+
applyOps: (ops: Op[], label: string) => void;
|
|
1889
|
+
/** Current viewport. Reflects camera-position semantics — see
|
|
1890
|
+
* `View` JSDoc. */
|
|
1891
|
+
view: View;
|
|
1892
|
+
/** Mutate the viewport. In controlled mode this calls the consumer's
|
|
1893
|
+
* `onViewChange`; in uncontrolled mode it updates Canvas's internal
|
|
1894
|
+
* state. View changes are not undoable. */
|
|
1895
|
+
setView: (next: View) => void;
|
|
1896
|
+
/** Bounding rect of the canvas element in viewport coords. Used by
|
|
1897
|
+
* zoom/pan tools to convert event clientX/clientY to canvas-relative
|
|
1898
|
+
* anchors. */
|
|
1899
|
+
canvasRect: DOMRect;
|
|
1900
|
+
/** Screen-space pointer coords relative to `canvasRect`. Useful for
|
|
1901
|
+
* viewport tools that pan/zoom in screen space (e.g. hand-pan
|
|
1902
|
+
* computes deltas in pixels, not world units). Optional — populated
|
|
1903
|
+
* by the dispatcher on pointer events; absent on keyboard events. */
|
|
1904
|
+
screenPoint?: {
|
|
1905
|
+
x: number;
|
|
1906
|
+
y: number;
|
|
1907
|
+
};
|
|
1908
|
+
/** Optional debug sink. When `<Canvas debug={...}>` is enabled, Canvas
|
|
1909
|
+
* threads its sink here so tool-internal hit math (handle hitboxes,
|
|
1910
|
+
* rotation handle, etc.) lands in the same overlay as Canvas's own
|
|
1911
|
+
* bounds/origin records. Tools should call this conditionally with `?.`. */
|
|
1912
|
+
debug?: DebugSink;
|
|
1913
|
+
scratch: TScratch;
|
|
1914
|
+
}
|
|
1915
|
+
/** Hotkey-slot trigger key. The slot is engaged while this key is held —
|
|
1916
|
+
* hence "hotkey": active as long as the key is hot. `null` (or omitted)
|
|
1917
|
+
* means the tool is not eligible for the hotkey slot. */
|
|
1918
|
+
type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift';
|
|
1919
|
+
/** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
|
|
1920
|
+
* `Bounds` type — the optional `rotation` field carries through so a tool
|
|
1921
|
+
* can report an oriented preview rect (e.g. mid-rotate). */
|
|
1922
|
+
type ToolBounds = Bounds;
|
|
1923
|
+
/** Presentation metadata for tool palettes / menus. Optional on every
|
|
1924
|
+
* tool — consumers that render a palette (`<ToolPalette>`) read these
|
|
1925
|
+
* fields to display the tool; consumers that don't can ignore them.
|
|
1926
|
+
*
|
|
1927
|
+
* Note: cursor is NOT here. The top-level `Tool.cursor` field below is
|
|
1928
|
+
* already plumbed through `<Canvas>` to `style.cursor` on the host. */
|
|
1929
|
+
interface ToolPresentation<TScratch = unknown> {
|
|
1930
|
+
/** Human-readable label, distinct from the `id`. Falls back to `id`. */
|
|
1931
|
+
label?: string;
|
|
1932
|
+
/** Inline-SVG icon component output. May be a static `ReactNode` or a
|
|
1933
|
+
* function of scratch state (rare; useful for shape-aware affordances). */
|
|
1934
|
+
icon?: React.ReactNode | ((scratch?: TScratch) => React.ReactNode);
|
|
1935
|
+
/** Palette grouping key. Tools sharing a group render contiguously
|
|
1936
|
+
* with separators between groups. Free-form string; the kit
|
|
1937
|
+
* recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
|
|
1938
|
+
group?: string;
|
|
1939
|
+
/** Display override for the keyboard shortcut. When omitted the palette
|
|
1940
|
+
* derives one from `Tool.keybinding` via its own formatter. */
|
|
1941
|
+
shortcut?: string;
|
|
1942
|
+
}
|
|
1943
|
+
/** Full Tool record. */
|
|
1944
|
+
interface Tool<TScratch = unknown> {
|
|
1945
|
+
id: string;
|
|
1946
|
+
/**
|
|
1947
|
+
* App-level capability tags for modality. The `weasel-modes` package's
|
|
1948
|
+
* `eligibleForMode(mode, capabilities)` predicate consumes these to decide
|
|
1949
|
+
* whether the tool is usable in the active mode. Tags are extensible
|
|
1950
|
+
* strings — apps can define their own. Untagged tools are treated as
|
|
1951
|
+
* ineligible by all modes except those whose `allows` list includes
|
|
1952
|
+
* every implicit-or-declared tag (i.e. `normal` in the default preset).
|
|
1953
|
+
*/
|
|
1954
|
+
capabilities?: CapabilityTag[];
|
|
1955
|
+
/**
|
|
1956
|
+
* Actions this tool owns and needs registered while it is in the tools
|
|
1957
|
+
* registry — e.g. polygon's `polygon.adjustSides`, which its own bindings
|
|
1958
|
+
* reference by id.
|
|
1959
|
+
*
|
|
1960
|
+
* Declared here rather than registered by the hook with `useAction`,
|
|
1961
|
+
* because tool hooks run wherever the consumer calls them — for
|
|
1962
|
+
* `<SceneCanvas>` that is ABOVE `<ActionsProviderIfRoot>`, where
|
|
1963
|
+
* `useActionsRegistry()` returns null and `useAction` silently no-ops. The
|
|
1964
|
+
* result was a binding pointing at an action id nothing had registered, so
|
|
1965
|
+
* the gesture fell through to whatever matched next (polygon's
|
|
1966
|
+
* wheel/arrow-key side adjustment did nothing and `nudge.*` moved the
|
|
1967
|
+
* selection instead). `<ToolActionsMounter>` registers these from inside
|
|
1968
|
+
* the provider.
|
|
1969
|
+
*/
|
|
1970
|
+
actions?: Action[];
|
|
1971
|
+
/** Optional caller-supplied key. Most built-in tools have their activation
|
|
1972
|
+
* key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
|
|
1973
|
+
* is for tools that want their activation key to be configurable by the
|
|
1974
|
+
* host (currently Lasso and Eyedropper). The dynamic loop in
|
|
1975
|
+
* `useKeybindings.ts` picks this up and appends a binding entry to the
|
|
1976
|
+
* consolidated `tool.activate` action (with `opts.params.toolId` set so
|
|
1977
|
+
* the invoker knows which tool to switch to). */
|
|
1978
|
+
keybinding?: ToolKeybinding;
|
|
1979
|
+
initScratch?: () => TScratch;
|
|
1980
|
+
onActivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
1981
|
+
onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
1982
|
+
cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
|
|
1983
|
+
/** Presentation metadata for tool palettes. See `ToolPresentation`. */
|
|
1984
|
+
presentation?: ToolPresentation<TScratch>;
|
|
1985
|
+
/** Returns the in-flight preview pose for `id` if this tool is mid-gesture
|
|
1986
|
+
* on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
|
|
1987
|
+
* reflect live gesture state without reaching into hook internals. The
|
|
1988
|
+
* return type is `unknown` here because the Tool interface is pose-agnostic;
|
|
1989
|
+
* callers that know the pose shape (e.g. Canvas typed by `TPose`) cast at
|
|
1990
|
+
* the use site. */
|
|
1991
|
+
previewPose?: (id: string) => unknown;
|
|
1992
|
+
/** Returns the in-flight preview bounds for `id` if this tool is mid-gesture
|
|
1993
|
+
* on it; otherwise `null`. Optional companion to `previewPose` for tools that
|
|
1994
|
+
* can compute bounds without round-tripping through a geometry adapter. */
|
|
1995
|
+
previewBounds?: (id: string) => ToolBounds | null;
|
|
1996
|
+
/** Returns ids whose committed scene-render should be suppressed while this
|
|
1997
|
+
* tool is mid-gesture (e.g. cascade move's dragged + descendant ids whose
|
|
1998
|
+
* preview ghosts replace the committed pose). The standard scene slot
|
|
1999
|
+
* consults this alongside `previewPose` to avoid double-rendering. Returns
|
|
2000
|
+
* `null` when no gesture is in flight. */
|
|
2001
|
+
previewIds?: () => Iterable<string> | null;
|
|
2002
|
+
/** Optional overlay layer rendered on top of the scene/chrome whenever
|
|
2003
|
+
* this tool is in any active slot (active, hotkey, or ambient).
|
|
2004
|
+
* The layer's `draw` function reads from this tool's scratch via React
|
|
2005
|
+
* closure (re-evaluated each render). Return early from `draw` to render
|
|
2006
|
+
* nothing — typically gated on a scratch field like
|
|
2007
|
+
* `if (!scratch.overlay) return`. */
|
|
2008
|
+
overlay?: RenderLayer<unknown>;
|
|
2009
|
+
/** Declarative gesture bindings — the tool's entire input surface. The
|
|
2010
|
+
* gesture dispatcher consults these at active scope while this tool is
|
|
2011
|
+
* active, and at hotkey scope while it is held. See
|
|
2012
|
+
* `docs/superpowers/specs/2026-05-16-registry-unification-design.md`. */
|
|
2013
|
+
bindings?: GestureBinding[];
|
|
2014
|
+
/** Reflection escape hatch: when this `Tool` was produced by `defineTool`,
|
|
2015
|
+
* the source `ToolDef` is attached here so introspection consumers
|
|
2016
|
+
* (`buildRouteRegistry`, `findConflicts`, the toolkit-builder UI, the
|
|
2017
|
+
* reflection demo) can read the authored form — `hookName` in particular,
|
|
2018
|
+
* which the runtime `Tool` doesn't carry. Tools constructed without
|
|
2019
|
+
* `defineTool` may leave this undefined. Typed as `unknown` to keep this
|
|
2020
|
+
* file from importing the routing types — consumers cast at the use site. */
|
|
2021
|
+
def?: unknown;
|
|
2022
|
+
}
|
|
2023
|
+
/** Internal — which slot a tool occupies in the dispatch order. */
|
|
2024
|
+
type ToolSlot = 'hotkey' | 'active' | 'ambient';
|
|
2025
|
+
/** Internal alias for "a Tool of any scratch type" — used in registries and
|
|
2026
|
+
* dispatchers that hold tools of heterogeneous scratch shapes. `any` is
|
|
2027
|
+
* intentional: `Tool<TScratch>` is invariant in TScratch, so `Tool<unknown>`
|
|
2028
|
+
* is too strict for containers that accept any concrete `Tool<T>`. */
|
|
2029
|
+
type AnyTool = Tool<any>;
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* Pure matcher primitives live in `@weasel-js/gestures`. This file
|
|
2033
|
+
* re-exports them for kit-internal consumers and layers the actions-layer
|
|
2034
|
+
* binding-scope / matchBest logic on top.
|
|
2035
|
+
*/
|
|
2036
|
+
|
|
2037
|
+
type BindingScope = 'ambient' | 'active' | 'hotkey';
|
|
2038
|
+
interface ScopedBinding {
|
|
2039
|
+
binding: GestureBinding;
|
|
2040
|
+
scope: BindingScope;
|
|
2041
|
+
/** Tool id that owns this binding — `'&'`-channel phase atoms resolve
|
|
2042
|
+
* to this. `null` for ambient bindings that came from a registered
|
|
2043
|
+
* Action with no owning tool. */
|
|
2044
|
+
ownerToolId: string | null;
|
|
2045
|
+
}
|
|
2046
|
+
interface MatchResult {
|
|
2047
|
+
binding: GestureBinding;
|
|
2048
|
+
scope: BindingScope;
|
|
2049
|
+
/** Tool id that owns the binding — propagated from `ScopedBinding`
|
|
2050
|
+
* so the dispatcher can record it as the handle owner. */
|
|
2051
|
+
ownerToolId: string | null;
|
|
2052
|
+
}
|
|
2053
|
+
/** CSS-style specificity tuple for a GestureSpec. Higher tuple wins under
|
|
2054
|
+
* lexicographic compare. Dimensions, in order of precedence:
|
|
2055
|
+
*
|
|
2056
|
+
* [0] target — how much the spec's target narrows; see `targetRank`.
|
|
2057
|
+
* [1] mods — count of required modifier keys (shift/alt/ctrl/meta/mod).
|
|
2058
|
+
* `'optional'` does NOT count.
|
|
2059
|
+
* [2] phase — 1 if the spec declares a `phase` field, else 0.
|
|
2060
|
+
* [3] exact — per-kind tiebreak: 2 for a drop/paste spec with a
|
|
2061
|
+
* non-empty `types` MIME filter, else 1.
|
|
2062
|
+
*
|
|
2063
|
+
* Identical tuples fall back to registration order in the matcher's
|
|
2064
|
+
* stable sort, preserving the pre-specificity tiebreaker. */
|
|
2065
|
+
declare function specificity(spec: GestureSpec): readonly [number, number, number, number];
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* Dispatcher orchestrator — pure module, no React, no DOM.
|
|
2069
|
+
*
|
|
2070
|
+
* Assembles `ScopedBinding[]` from the actions registry, active tool, and
|
|
2071
|
+
* hotkey stack; matches input events via `matchSorted`; gates each candidate
|
|
2072
|
+
* on `enabled()`; then invokes `immediate` or `ongoing` invokers and tracks
|
|
2073
|
+
* in-flight handles.
|
|
2074
|
+
*
|
|
2075
|
+
* ## Specificity-ordered fall-through
|
|
2076
|
+
* `matchSorted` returns every matching binding in precedence order
|
|
2077
|
+
* (hotkey > active > ambient, first-declared within scope). The dispatcher
|
|
2078
|
+
* walks that list and fires the first action whose `enabled()` returns
|
|
2079
|
+
* `true`. If every candidate's `enabled()` returns a disabled reason, the
|
|
2080
|
+
* event is unhandled. This mirrors CSS-style specificity matching with a
|
|
2081
|
+
* `:not(:disabled)` filter, and lets a tool declare a high-specificity
|
|
2082
|
+
* binding (e.g. drag-on-empty → areaSelect) that gracefully falls through
|
|
2083
|
+
* to a lower-specificity ambient binding (e.g. drag → viewport.dragPan)
|
|
2084
|
+
* when its required deps aren't wired.
|
|
2085
|
+
*
|
|
2086
|
+
* ## gestureId scheme
|
|
2087
|
+
* - `key-held` ongoing actions: `key-held-<key>` (e.g. `key-held- ` for Space).
|
|
2088
|
+
* Chosen because key-held gestures are identified by the held key alone.
|
|
2089
|
+
* - `pointerdown` / drag ongoing actions: `pointer-<pointerId>`, where
|
|
2090
|
+
* `pointerId` defaults to `'mouse'` (Phase 3 has no real pointer IDs; the
|
|
2091
|
+
* React seam in Task 4 will supply the actual DOM pointerId).
|
|
2092
|
+
* - `multitouch` ongoing actions: `multitouch-<fingers>`.
|
|
2093
|
+
* - Fallback for any other kind that triggers an ongoing invoker: `ongoing-<kind>`.
|
|
2094
|
+
*
|
|
2095
|
+
* ## Action-lookup miss behavior
|
|
2096
|
+
* When `matchBest` resolves a binding whose `actionId` has no entry in
|
|
2097
|
+
* `ctx.actions.list()`, the dispatcher emits `console.warn` and returns
|
|
2098
|
+
* `'unhandled'`. The user's input gesture falls through as if unmatched.
|
|
2099
|
+
* This preserves input flow (nothing is swallowed silently) while flagging
|
|
2100
|
+
* the misconfiguration at dev time.
|
|
2101
|
+
*/
|
|
2102
|
+
|
|
2103
|
+
interface DispatcherContext {
|
|
2104
|
+
/** All registered actions; the dispatcher walks `.defaultBinding` for ambient bindings. */
|
|
2105
|
+
actions: ActionsRegistry;
|
|
2106
|
+
/** Dep sources keyed by name. */
|
|
2107
|
+
depRegistry: DepRegistry;
|
|
2108
|
+
/** Active tool's id (from ActiveToolContext). */
|
|
2109
|
+
activeToolId: string;
|
|
2110
|
+
/** Held-hotkey stack, top of stack last. */
|
|
2111
|
+
hotkeyStack: readonly string[];
|
|
2112
|
+
/**
|
|
2113
|
+
* Ids of always-on tools. Their bindings are assembled at AMBIENT scope, so
|
|
2114
|
+
* they lose to the active tool on a tie — which is what "always listening,
|
|
2115
|
+
* never in the way" needs. Chrome that floats over the scene lives here:
|
|
2116
|
+
* `@weasel-js/hud`'s tool is the worked example.
|
|
2117
|
+
*
|
|
2118
|
+
* Without this an ambient tool's `bindings` were assembled nowhere at all:
|
|
2119
|
+
* the walk covered hotkey and active tools plus actions' `defaultBinding`,
|
|
2120
|
+
* and an ambient tool is in neither set.
|
|
2121
|
+
*/
|
|
2122
|
+
ambientToolIds?: readonly string[];
|
|
2123
|
+
/** Lookup for tool definitions. */
|
|
2124
|
+
toolsById: ReadonlyMap<string, Tool>;
|
|
2125
|
+
/** Platform flag for `mod` shorthand resolution. */
|
|
2126
|
+
isMac: boolean;
|
|
2127
|
+
/**
|
|
2128
|
+
* Thunk returning a fresh `RuleCtx` for the current frame. When
|
|
2129
|
+
* supplied, the dispatcher filters matched candidates by their
|
|
2130
|
+
* declared `Action.eligible` rule (omitted => always eligible).
|
|
2131
|
+
* When omitted, no eligibility filtering is applied — preserves
|
|
2132
|
+
* backward compatibility for callers (tests, legacy harnesses) that
|
|
2133
|
+
* don't wire up chrome-caps state.
|
|
2134
|
+
*/
|
|
2135
|
+
getRuleCtx?: () => RuleCtx;
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Handle returned by `Dispatcher.beginUiOngoing()` for driving an
|
|
2139
|
+
* ongoing invoker from a UI control (color picker, slider).
|
|
2140
|
+
*
|
|
2141
|
+
* - `update(params)` rebuilds an `InvocationCtx` with the new params and
|
|
2142
|
+
* calls the handle's `onMove`. Safe to call many times.
|
|
2143
|
+
* - `end(reason)` calls `onEnd(ctx, reason)` once and removes the handle
|
|
2144
|
+
* from the in-flight map. Idempotent — further calls are no-ops.
|
|
2145
|
+
*/
|
|
2146
|
+
interface UiOngoingControl {
|
|
2147
|
+
readonly gestureId: string;
|
|
2148
|
+
update(params?: Record<string, unknown>): void;
|
|
2149
|
+
end(reason: 'commit' | 'cancel'): void;
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Successful `Dispatcher.resolveOnly` prediction: the binding + action that
|
|
2153
|
+
* would fire if `event` were dispatched for real. `action` is the resolved
|
|
2154
|
+
* descriptor so callers (the hover-cursor pump) can read metadata like
|
|
2155
|
+
* `Action.cursor` without a second registry lookup.
|
|
2156
|
+
*/
|
|
2157
|
+
interface ResolveOnlyResult {
|
|
2158
|
+
actionId: string;
|
|
2159
|
+
action: Action;
|
|
2160
|
+
scope: BindingScope;
|
|
2161
|
+
/** Tool id owning the winning binding; `null` for ambient action bindings. */
|
|
2162
|
+
ownerToolId: string | null;
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* One candidate from `Dispatcher.resolveAll` — a binding that matched the
|
|
2166
|
+
* event, with why it did or didn't get to fire.
|
|
2167
|
+
*
|
|
2168
|
+
* Verdicts:
|
|
2169
|
+
* - `would-fire` — eligible, `enabled()` passed, and nothing above it fired.
|
|
2170
|
+
* At most one candidate per call carries this.
|
|
2171
|
+
* - `ineligible` — the action's `eligible` rule evaluated false against the
|
|
2172
|
+
* live `RuleCtx`. `reason` is the rule, serialized.
|
|
2173
|
+
* - `disabled` — `enabled()` returned a disabled reason, carried verbatim.
|
|
2174
|
+
* - `shadowed` — never asked, for one of two reasons: something above it
|
|
2175
|
+
* already fired, or it is a repeat binding of the action that itself won
|
|
2176
|
+
* higher in the list (several bindings may point at one action, and the
|
|
2177
|
+
* dispatcher runs each action at most once). A repeat of an action that was
|
|
2178
|
+
* already judged `ineligible` or `disabled` is NOT shadowed — it inherits
|
|
2179
|
+
* that action's verdict, since that is the reason it doesn't fire.
|
|
2180
|
+
*/
|
|
2181
|
+
interface ResolvedCandidate {
|
|
2182
|
+
actionId: string;
|
|
2183
|
+
action: Action;
|
|
2184
|
+
binding: GestureBinding;
|
|
2185
|
+
scope: BindingScope;
|
|
2186
|
+
ownerToolId: string | null;
|
|
2187
|
+
/** The tuple from `specificity(binding.spec)`, surfaced so a reader can see
|
|
2188
|
+
* why one candidate outranks another rather than inferring it. */
|
|
2189
|
+
specificity: readonly [number, number, number, number];
|
|
2190
|
+
verdict: {
|
|
2191
|
+
kind: 'would-fire';
|
|
2192
|
+
} | {
|
|
2193
|
+
kind: 'ineligible';
|
|
2194
|
+
reason: string;
|
|
2195
|
+
} | {
|
|
2196
|
+
kind: 'disabled';
|
|
2197
|
+
reason: string;
|
|
2198
|
+
} | {
|
|
2199
|
+
kind: 'shadowed';
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
/** Options for {@link Dispatcher.resolveAll}. */
|
|
2203
|
+
interface ResolveAllOptions {
|
|
2204
|
+
/**
|
|
2205
|
+
* Evaluate eligibility and `enabled()` for candidates below the winner
|
|
2206
|
+
* instead of short-circuiting them to `shadowed`.
|
|
2207
|
+
*
|
|
2208
|
+
* Off by default, and deliberately so: the default walk's early exit is what
|
|
2209
|
+
* keeps `resolveOnly`'s `enabled()` call count identical to a real dispatch,
|
|
2210
|
+
* and `enabled()` predicates are only contractually pure — not free.
|
|
2211
|
+
*
|
|
2212
|
+
* Turn it on for diagnostics, where "this one was outranked" is a less
|
|
2213
|
+
* useful answer than "this one was outranked AND would have been disabled
|
|
2214
|
+
* anyway". With it on, `shadowed` narrows to its precise meaning: this
|
|
2215
|
+
* candidate would have fired, but something above it did.
|
|
2216
|
+
*/
|
|
2217
|
+
evaluateShadowed?: boolean;
|
|
2218
|
+
}
|
|
2219
|
+
interface Dispatcher {
|
|
2220
|
+
/**
|
|
2221
|
+
* Route an input event through the binding pipeline. Returns `'handled'`
|
|
2222
|
+
* when a binding matched and the action invoked successfully (whether it
|
|
2223
|
+
* returned ops or not). Returns `'unhandled'` when no binding matched or
|
|
2224
|
+
* the matched action's `enabled()` returned a disabled reason.
|
|
2225
|
+
*/
|
|
2226
|
+
handleInput(event: InputEvent, ctx: DispatcherContext): 'handled' | 'unhandled';
|
|
2227
|
+
/**
|
|
2228
|
+
* Predict which action `event` would route to WITHOUT invoking it. Replays
|
|
2229
|
+
* the same walk as `handleInput` — scope assembly, specificity-sorted
|
|
2230
|
+
* match, eligibility filter, per-candidate `enabled()` gate — and returns
|
|
2231
|
+
* the first candidate that would fire, or `null` when the event would go
|
|
2232
|
+
* unhandled. Pure query: no invoker runs, no in-flight state changes, no
|
|
2233
|
+
* trace-log entry.
|
|
2234
|
+
*
|
|
2235
|
+
* Known divergence from a real dispatch: an ongoing invoker that matches
|
|
2236
|
+
* but returns an empty handle at `start()` (runtime bail) makes the real
|
|
2237
|
+
* dispatch fall through to the next candidate; prediction cannot see that
|
|
2238
|
+
* and reports the bailing action. Keep `enabled()` accurate on actions
|
|
2239
|
+
* that rely on prediction (hover cursors).
|
|
2240
|
+
*/
|
|
2241
|
+
resolveOnly(event: InputEvent, ctx: DispatcherContext): ResolveOnlyResult | null;
|
|
2242
|
+
/**
|
|
2243
|
+
* Every binding that matches `event`, in dispatch precedence order, each
|
|
2244
|
+
* with a verdict explaining whether it would fire. Same walk as
|
|
2245
|
+
* `resolveOnly` — scope assembly, specificity-sorted match, eligibility
|
|
2246
|
+
* check, per-candidate `enabled()` gate — without stopping at the winner
|
|
2247
|
+
* and without invoking anything. Nothing is dropped: candidates that
|
|
2248
|
+
* `resolveOnly`'s walk would filter out are kept here and labelled
|
|
2249
|
+
* `ineligible` instead. Pure query: no invoker runs, no in-flight state
|
|
2250
|
+
* changes, no trace-log entry.
|
|
2251
|
+
*
|
|
2252
|
+
* `resolveOnly` is the first `would-fire` entry of this list.
|
|
2253
|
+
*
|
|
2254
|
+
* Shares `resolveOnly`'s known divergence from a real dispatch: an ongoing
|
|
2255
|
+
* invoker that matches but returns an empty handle at `start()` makes the
|
|
2256
|
+
* real dispatch fall through, and this cannot see that.
|
|
2257
|
+
*
|
|
2258
|
+
* By default everything below the winner is `shadowed` without being asked,
|
|
2259
|
+
* which is what keeps this walk as cheap as the dispatch it replays. Pass
|
|
2260
|
+
* `{ evaluateShadowed: true }` to keep evaluating past the winner, so a
|
|
2261
|
+
* lower candidate that is ALSO ineligible or disabled says so — see
|
|
2262
|
+
* {@link ResolveAllOptions.evaluateShadowed}.
|
|
2263
|
+
*/
|
|
2264
|
+
resolveAll(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): ResolvedCandidate[];
|
|
2265
|
+
/**
|
|
2266
|
+
* Synthesize an end-of-gesture for every in-flight ongoing handle.
|
|
2267
|
+
* Used by tool-switch cancellation (Q2 decision).
|
|
2268
|
+
*/
|
|
2269
|
+
cancelAll(reason: 'commit' | 'cancel'): void;
|
|
2270
|
+
/**
|
|
2271
|
+
* Read-only view of currently in-flight ongoing handles, keyed by gestureId.
|
|
2272
|
+
* For debug/testing.
|
|
2273
|
+
*/
|
|
2274
|
+
inFlight(): ReadonlyMap<string, OngoingHandle>;
|
|
2275
|
+
/**
|
|
2276
|
+
* CSS cursor for the gesture currently in flight, or `null` when nothing
|
|
2277
|
+
* is. Reads `Action.activeCursor` (falling back to `Action.cursor`) off the
|
|
2278
|
+
* action whose handle is open — the hover pump applies this instead of its
|
|
2279
|
+
* prediction once a gesture starts, which is how `grab` becomes `grabbing`.
|
|
2280
|
+
*/
|
|
2281
|
+
inFlightCursor(): string | null;
|
|
2282
|
+
/**
|
|
2283
|
+
* Read-only iterator over currently in-flight `OngoingHandle` instances.
|
|
2284
|
+
*
|
|
2285
|
+
* Surface for the canvas's preview-ghost layer (`usePreviewGhostLayer`)
|
|
2286
|
+
* to walk each handle's `previewIds()` / `previewPose(id)` and render
|
|
2287
|
+
* dispatcher-driven gesture previews. Read-only by design:
|
|
2288
|
+
* external consumers must not mutate the in-flight map.
|
|
2289
|
+
*/
|
|
2290
|
+
getInFlightHandles(): Iterable<OngoingHandle>;
|
|
2291
|
+
/**
|
|
2292
|
+
* Subscribe to in-flight state changes. The callback fires after every
|
|
2293
|
+
* mutation that affects what the preview-ghost / dispatcher-overlay
|
|
2294
|
+
* layers read — handle start, every `onMove` pump, end, cancel,
|
|
2295
|
+
* cancel-all. Consumers re-read `getInFlightHandles()` and re-render.
|
|
2296
|
+
*
|
|
2297
|
+
* Returns an unsubscribe function.
|
|
2298
|
+
*/
|
|
2299
|
+
subscribe(fn: () => void): () => void;
|
|
2300
|
+
/**
|
|
2301
|
+
* Monotonic counter bumped on exactly the events {@link subscribe} fires
|
|
2302
|
+
* on. The snapshot half of the `useSyncExternalStore` contract: pair it
|
|
2303
|
+
* with `subscribe` to drive a render off in-flight gesture state without
|
|
2304
|
+
* a `useReducer` force-rerender.
|
|
2305
|
+
*
|
|
2306
|
+
* Starts at 0 and only ever increases. Two reads returning the same number
|
|
2307
|
+
* mean nothing pumped in between; it does **not** guarantee that a bump
|
|
2308
|
+
* changed anything observable (a pump that matched no binding still
|
|
2309
|
+
* counts — see `subscribe`).
|
|
2310
|
+
*/
|
|
2311
|
+
getVersion(): number;
|
|
2312
|
+
/**
|
|
2313
|
+
* Snapshot of the currently active action, for surfaces (chrome-caps
|
|
2314
|
+
* visibility rules, debug HUDs) that need to react to "what action
|
|
2315
|
+
* is in flight right now."
|
|
2316
|
+
*
|
|
2317
|
+
* - `kind` — the `OngoingHandle.kind` reported by the in-flight
|
|
2318
|
+
* handle (e.g. `'marquee'`, `'move'`). `null` when no action is
|
|
2319
|
+
* in flight OR the handle didn't declare a kind.
|
|
2320
|
+
* - `id` — the dispatcher's internal `gestureId` (`pointer-mouse`,
|
|
2321
|
+
* `key-held-Space`, …) — the pointer/key channel the action rode
|
|
2322
|
+
* in on. `null` when no action is in flight.
|
|
2323
|
+
*
|
|
2324
|
+
* When multiple handles are in flight simultaneously (e.g. a key-held
|
|
2325
|
+
* action overlapping a pointer action), the most-recently-started
|
|
2326
|
+
* handle wins. This matches user intent: the latest interaction is
|
|
2327
|
+
* the one consumers care about.
|
|
2328
|
+
*/
|
|
2329
|
+
getActiveAction(): {
|
|
2330
|
+
kind: string | null;
|
|
2331
|
+
id: string | null;
|
|
2332
|
+
};
|
|
2333
|
+
/**
|
|
2334
|
+
* Start an ongoing action driven by UI (not a gesture). Builds an
|
|
2335
|
+
* `InvocationCtx` with the given `deps` and `params`, calls
|
|
2336
|
+
* `action.invoker.start(ctx, { params })`, and registers the returned
|
|
2337
|
+
* handle in the in-flight map so `getInFlightHandles()` reports it —
|
|
2338
|
+
* enabling preview rendering via `SceneCanvas`.
|
|
2339
|
+
*
|
|
2340
|
+
* Returns `null` if `actionId` is unknown, the action's invoker is not
|
|
2341
|
+
* ongoing, or `start` returned an empty handle.
|
|
2342
|
+
*
|
|
2343
|
+
* If a UI-driven handle for the same `actionId` is already in flight,
|
|
2344
|
+
* it is committed (`end('commit')`) before the new one starts.
|
|
2345
|
+
*/
|
|
2346
|
+
beginUiOngoing(actionId: string, deps: ActionDeps, params?: Record<string, unknown>): UiOngoingControl | null;
|
|
2347
|
+
}
|
|
2348
|
+
declare function createDispatcher(opts?: {
|
|
2349
|
+
getAction?: (id: string) => Action | undefined;
|
|
2350
|
+
}): Dispatcher;
|
|
2351
|
+
|
|
2352
|
+
/**
|
|
2353
|
+
* @experimental
|
|
2354
|
+
* A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
|
|
2355
|
+
* (no per-binding opts) or an object form that pairs a spec with
|
|
2356
|
+
* `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
|
|
2357
|
+
* Use the object form when two bindings for the same action differ only in
|
|
2358
|
+
* a runtime parameter — the dispatcher extracts `opts.params` and passes
|
|
2359
|
+
* them to `ImmediateInvoker.run` as its second argument.
|
|
2360
|
+
*/
|
|
2361
|
+
type BoundGesture = GestureSpec | {
|
|
2362
|
+
spec: GestureSpec;
|
|
2363
|
+
opts: BindingOpts;
|
|
2364
|
+
};
|
|
2365
|
+
/**
|
|
2366
|
+
* @experimental
|
|
2367
|
+
* Single registered action. v1: one binding per action.
|
|
2368
|
+
*/
|
|
2369
|
+
interface Action {
|
|
2370
|
+
id: string;
|
|
2371
|
+
label: string;
|
|
2372
|
+
/** The gesture-spec form of the binding, read by the gesture dispatcher.
|
|
2373
|
+
* May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
|
|
2374
|
+
* or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
|
|
2375
|
+
* `{ spec, opts }` — use the object form for parametric actions where two
|
|
2376
|
+
* bindings for the same action differ only by `opts.params` (e.g. `flip`
|
|
2377
|
+
* with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
|
|
2378
|
+
* passes them to `ImmediateInvoker.run` as its second argument. */
|
|
2379
|
+
defaultBinding?: GestureSpec | BoundGesture[];
|
|
2380
|
+
/** Names of the deps this action's invoker reads (keys of `DepSchema`).
|
|
2381
|
+
* The dispatcher (and `trigger`, when `requires` is present) resolves
|
|
2382
|
+
* each name against the `DepRegistry` at invocation time and passes the
|
|
2383
|
+
* resulting bag to the invoker. Dev builds warn when the invoker reads a
|
|
2384
|
+
* dep it didn't declare here — see `buildDepsFromRequires`. */
|
|
2385
|
+
requires?: readonly DepName[];
|
|
2386
|
+
/** Inline-SVG icon for palette / toolbar surfaces. Mirrors
|
|
2387
|
+
* `ToolPresentation.icon` so a generic `<ActionBar>` can render from
|
|
2388
|
+
* action metadata the same way `<ToolPalette>` renders from tool
|
|
2389
|
+
* metadata. May be a static `ReactNode` or a function (rare; useful
|
|
2390
|
+
* for state-aware icons like a "lock" toggle). */
|
|
2391
|
+
icon?: ReactNode | (() => ReactNode);
|
|
2392
|
+
/** Grouping key for palette/menu surfaces. Free-form string; the kit
|
|
2393
|
+
* ships defaults for `'align'` (six edges/centers), `'distribute'`
|
|
2394
|
+
* (two axes), and recommends `'pathfinder'` for boolean ops. */
|
|
2395
|
+
group?: string;
|
|
2396
|
+
/** Display override for the keyboard shortcut. When omitted, palette
|
|
2397
|
+
* surfaces derive a label from `defaultBinding` via their own
|
|
2398
|
+
* formatter. */
|
|
2399
|
+
shortcut?: string;
|
|
2400
|
+
/** Pluggable invocation strategy. The gesture dispatcher routes matched
|
|
2401
|
+
* bindings through `invoker.start` / `invoker.run` depending on timing.
|
|
2402
|
+
* All kit-standard descriptors ship one; consumer-supplied actions
|
|
2403
|
+
* without an invoker can still register but won't be triggered. */
|
|
2404
|
+
invoker?: Invoker;
|
|
2405
|
+
/** When set to `'hotkey'`, this action's `defaultBinding` rides the hotkey
|
|
2406
|
+
* `BindingScope` instead of the ambient scope — meaning it beats any
|
|
2407
|
+
* active-tool binding on the same input shape. Use for tool-switch
|
|
2408
|
+
* shortcuts and global held-key triggers. Default: ambient. */
|
|
2409
|
+
scope?: 'hotkey';
|
|
2410
|
+
/**
|
|
2411
|
+
* @experimental
|
|
2412
|
+
* Optional predicate the command palette consults when rendering. Return
|
|
2413
|
+
* `true` when the action is currently triggerable. Return a reason string
|
|
2414
|
+
* (e.g. `'Selection required'`) when disabled — the palette greys out
|
|
2415
|
+
* the row, skips it in keyboard nav, ignores clicks, and shows the
|
|
2416
|
+
* reason next to the label. Keystroke dispatch (the registered binding)
|
|
2417
|
+
* is unaffected; the action's own `run` should self-guard.
|
|
2418
|
+
*
|
|
2419
|
+
* **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
|
|
2420
|
+
* must not throw. If a call throws or exceeds the budget in dev mode,
|
|
2421
|
+
* `evaluateEnabled` logs a one-time warning per action id; throws are
|
|
2422
|
+
* caught and treated as disabled with reason `'(predicate threw)'`.
|
|
2423
|
+
*
|
|
2424
|
+
* Snapshot-on-open semantics: the palette evaluates `enabled` once when
|
|
2425
|
+
* opened and does NOT re-evaluate on selection changes while open. Live
|
|
2426
|
+
* reactive updates are deferred — palette is short-lived.
|
|
2427
|
+
*
|
|
2428
|
+
* The reason set is a closed enum — to add a new reason, edit
|
|
2429
|
+
* `ActionDisabledReason` and the consumer's display map.
|
|
2430
|
+
*
|
|
2431
|
+
* The optional `deps` argument is the same bag passed to
|
|
2432
|
+
* `ImmediateInvoker.run`; callers (`evaluateEnabled` / the ActionBar) may
|
|
2433
|
+
* synthesize it from the surrounding `DepRegistry` so predicates can
|
|
2434
|
+
* inspect selection / scene / etc. Predicates that don't need deps just
|
|
2435
|
+
* ignore the arg.
|
|
2436
|
+
*/
|
|
2437
|
+
enabled?: (deps?: ActionDeps) => true | ActionDisabledReason;
|
|
2438
|
+
/**
|
|
2439
|
+
* Declarative eligibility rule, evaluated against the current
|
|
2440
|
+
* `RuleCtx` by the dispatcher before invoking `start()`. Omitted =
|
|
2441
|
+
* always eligible.
|
|
2442
|
+
*
|
|
2443
|
+
* Accepts either a fluent `Condition` (callable with `.rule`) or a
|
|
2444
|
+
* raw `Rule` tree; the dispatcher normalizes via `.rule` unwrap.
|
|
2445
|
+
*
|
|
2446
|
+
* Prefer `capability:`-based rules (e.g. `{ capability: 'transforms-selection' }`)
|
|
2447
|
+
* over `mode:` rules — capability rules survive new modes being added
|
|
2448
|
+
* that allow the same capability.
|
|
2449
|
+
*/
|
|
2450
|
+
eligible?: Rule | Condition;
|
|
2451
|
+
/**
|
|
2452
|
+
* CSS cursor shown while the pointer hovers a spot where this action
|
|
2453
|
+
* would win the drag. The hover-cursor pump (in `useGestureDispatcher`)
|
|
2454
|
+
* runs `Dispatcher.resolveOnly` on each idle pointermove — the same
|
|
2455
|
+
* match walk a real pointerdown takes — and applies the winning
|
|
2456
|
+
* action's `cursor`, so the hint and the actual click target stay in
|
|
2457
|
+
* sync by construction. Omitted = no override (the active tool's
|
|
2458
|
+
* `Tool.cursor` shows). Affordance hits are resolved earlier in the
|
|
2459
|
+
* pump via `AffordanceRegion.cursor` and never reach this field.
|
|
2460
|
+
*
|
|
2461
|
+
* Static string only. Prediction runs `enabled()` but cannot run the
|
|
2462
|
+
* invoker, so an action that matches yet bails at `start()` (empty
|
|
2463
|
+
* handle) may still show its cursor — keep `enabled` accurate for
|
|
2464
|
+
* actions that declare one.
|
|
2465
|
+
*/
|
|
2466
|
+
cursor?: string;
|
|
2467
|
+
/**
|
|
2468
|
+
* CSS cursor shown while THIS action's ongoing handle is in flight —
|
|
2469
|
+
* grabbing while panning, `move` while dragging a selection, `crosshair`
|
|
2470
|
+
* while pulling a marquee.
|
|
2471
|
+
*
|
|
2472
|
+
* Separate from `cursor` because the two answer different questions:
|
|
2473
|
+
* `cursor` is a prediction ("a drag from here would pan"), this is a state
|
|
2474
|
+
* ("you are panning"). An action can declare either, both, or neither;
|
|
2475
|
+
* with only `cursor` set, the hover hint holds for the duration of the
|
|
2476
|
+
* gesture.
|
|
2477
|
+
*
|
|
2478
|
+
* This is where mid-gesture cursors live now. They used to come from the
|
|
2479
|
+
* tool side — `ViewportToolDef.engaged.cursor` for a phase-gated string,
|
|
2480
|
+
* or a function-form `Tool.cursor` reading the gesture scratch out of the
|
|
2481
|
+
* tool-routing dispatcher. Both belonged to a pipeline whose whole job was
|
|
2482
|
+
* being taken over by bindings, and neither could describe a cursor for an
|
|
2483
|
+
* action a tool doesn't own.
|
|
2484
|
+
*/
|
|
2485
|
+
activeCursor?: string;
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* @experimental
|
|
2489
|
+
* Closed enum of reasons an action might report itself as disabled. The
|
|
2490
|
+
* consumer (palette, menu, etc.) maps these symbolic values to display
|
|
2491
|
+
* strings via its own label map — see `demo/CommandPalette.tsx` for the
|
|
2492
|
+
* canonical mapping.
|
|
2493
|
+
*/
|
|
2494
|
+
declare const ActionDisabledReason: {
|
|
2495
|
+
readonly SelectionRequired: "selection-required";
|
|
2496
|
+
readonly SceneEmpty: "scene-empty";
|
|
2497
|
+
readonly NotApplicable: "not-applicable";
|
|
2498
|
+
/** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
|
|
2499
|
+
readonly PredicateThrew: "predicate-threw";
|
|
2500
|
+
};
|
|
2501
|
+
type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDisabledReason];
|
|
2502
|
+
/**
|
|
2503
|
+
* @experimental
|
|
2504
|
+
* Result of evaluating an Action's `enabled` predicate.
|
|
2505
|
+
*/
|
|
2506
|
+
interface ActionEnabledResult {
|
|
2507
|
+
enabled: boolean;
|
|
2508
|
+
reason?: ActionDisabledReason;
|
|
2509
|
+
}
|
|
2510
|
+
declare function evaluateEnabled(action: Action, deps?: ActionDeps): ActionEnabledResult;
|
|
2511
|
+
/**
|
|
2512
|
+
* @experimental
|
|
2513
|
+
* Partial override or full descriptor passed via `<SceneCanvas actions={...}>`.
|
|
2514
|
+
* `null` disables a default at this id.
|
|
2515
|
+
*/
|
|
2516
|
+
type ActionEntry = null | Partial<Action> | Action;
|
|
2517
|
+
/**
|
|
2518
|
+
* @experimental
|
|
2519
|
+
* Shape of the `actions` prop on `<SceneCanvas>`. `null` disables all defaults.
|
|
2520
|
+
*/
|
|
2521
|
+
type ActionsProp = null | Record<string, ActionEntry>;
|
|
2522
|
+
/**
|
|
2523
|
+
* @experimental
|
|
2524
|
+
* Imperative API exposed by `useActionsRegistry()`.
|
|
2525
|
+
*/
|
|
2526
|
+
interface ActionsRegistry {
|
|
2527
|
+
register(action: Action): () => void;
|
|
2528
|
+
unregister(id: string): void;
|
|
2529
|
+
list(): readonly Action[];
|
|
2530
|
+
/** Fire an immediate-invoker action by id. The optional `params` arg is
|
|
2531
|
+
* forwarded to `ImmediateInvoker.run` as its second argument — use it for
|
|
2532
|
+
* parametric actions (e.g. `trigger('tool.activate', { toolId: 'rect' })`).
|
|
2533
|
+
* Ongoing-invoker actions are not reachable from `trigger`. */
|
|
2534
|
+
trigger(id: string, params?: Record<string, unknown>): boolean;
|
|
2535
|
+
/**
|
|
2536
|
+
* Subscribe to registry mutations. The callback fires after any
|
|
2537
|
+
* `register`/`unregister` that changes the version. Returns an
|
|
2538
|
+
* unsubscribe function. Designed for `useSyncExternalStore`-driven
|
|
2539
|
+
* surfaces (e.g. `<ActionBar>` in `@weasel-js/ui`) that need to
|
|
2540
|
+
* re-render when the action set changes.
|
|
2541
|
+
*/
|
|
2542
|
+
subscribe(listener: () => void): () => void;
|
|
2543
|
+
/**
|
|
2544
|
+
* Start an ongoing action driven by UI (color picker, opacity slider).
|
|
2545
|
+
* Returns a control object with `update(params)` and `end(reason)`.
|
|
2546
|
+
*
|
|
2547
|
+
* Returns `null` if no dispatcher is wired into this registry, the
|
|
2548
|
+
* action is unknown, or its invoker is not ongoing.
|
|
2549
|
+
*
|
|
2550
|
+
* See `Dispatcher.beginUiOngoing` for full semantics including
|
|
2551
|
+
* auto-commit when a prior UI handle for the same action is in flight.
|
|
2552
|
+
*/
|
|
2553
|
+
begin(id: string, params?: Record<string, unknown>): UiOngoingControl | null;
|
|
2554
|
+
/** Wire a dispatcher into the registry so `begin()` can delegate to it.
|
|
2555
|
+
* Call with `null` to detach. Idempotent. */
|
|
2556
|
+
setDispatcher(d: Dispatcher | null): void;
|
|
2557
|
+
/** Wire a `DepRegistry` into the registry so `trigger()` / `begin()` can
|
|
2558
|
+
* resolve action deps even when this provider is mounted ABOVE the dep
|
|
2559
|
+
* registry (e.g. a consumer's root `<ActionsProvider>` reused by
|
|
2560
|
+
* SceneCanvas's `ActionsProviderIfRoot`). Takes precedence over the dep
|
|
2561
|
+
* registry read from context at the provider's own level. Call with
|
|
2562
|
+
* `null` to detach. */
|
|
2563
|
+
setDepRegistry(r: DepRegistry | null): void;
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* @experimental
|
|
2567
|
+
* Mounts an `ActionsRegistry` for its lifetime. Children call
|
|
2568
|
+
* `useActionsRegistry()` or `useAction()` to participate. Mounts no input
|
|
2569
|
+
* listener of its own — the gesture dispatcher owns input.
|
|
2570
|
+
*/
|
|
2571
|
+
declare function ActionsProvider({ children }: {
|
|
2572
|
+
children: ReactNode;
|
|
2573
|
+
}): ReactElement;
|
|
2574
|
+
/**
|
|
2575
|
+
* @experimental
|
|
2576
|
+
* Returns the parent `ActionsRegistry`, or `null` when no provider is in scope.
|
|
2577
|
+
*/
|
|
2578
|
+
declare function useActionsRegistry(): ActionsRegistry | null;
|
|
2579
|
+
/**
|
|
2580
|
+
* @experimental
|
|
2581
|
+
* Register an `Action` for the lifetime of the calling component. No-op when
|
|
2582
|
+
* no `ActionsProvider` is in scope. Re-registers on `action` reference change
|
|
2583
|
+
* (consumers should memoize stable identities to avoid churn).
|
|
2584
|
+
*/
|
|
2585
|
+
declare function useAction(action: Action): void;
|
|
2586
|
+
|
|
2587
|
+
/**
|
|
2588
|
+
* Configurable activation-key descriptor for tools that expose their
|
|
2589
|
+
* keybinding to the host (currently Lasso and Eyedropper). Captures
|
|
2590
|
+
* only the fields meaningful to a caller-supplied tool-select key —
|
|
2591
|
+
* dispatcher-internal fields (`skipInEditable`, `enabled`,
|
|
2592
|
+
* `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
|
|
2593
|
+
* not part of the configurable surface.
|
|
2594
|
+
*/
|
|
2595
|
+
interface ToolKeybinding {
|
|
2596
|
+
/** Key or list of keys to match (case-insensitive against `event.key`). */
|
|
2597
|
+
key: string | readonly string[];
|
|
2598
|
+
/** Require Cmd (mac) / Ctrl (others). Default `false`. */
|
|
2599
|
+
mod?: boolean;
|
|
2600
|
+
/** Require Alt. Default `false`. */
|
|
2601
|
+
alt?: boolean;
|
|
2602
|
+
/**
|
|
2603
|
+
* Shift policy. `undefined`/`false` forbids shift, `true` requires
|
|
2604
|
+
* shift, `'optional'` allows either.
|
|
2605
|
+
*/
|
|
2606
|
+
shift?: boolean | 'optional';
|
|
2607
|
+
}
|
|
2608
|
+
interface ToolDef<TScratch = void> {
|
|
2609
|
+
id: string;
|
|
2610
|
+
/** Capability tags for modality eligibility. Forwarded onto `Tool.capabilities`. */
|
|
2611
|
+
capabilities?: CapabilityTag[];
|
|
2612
|
+
/**
|
|
2613
|
+
* Actions this tool owns and needs registered while it is in the tools
|
|
2614
|
+
* registry — e.g. polygon's `polygon.adjustSides`, which its own bindings
|
|
2615
|
+
* reference by id.
|
|
2616
|
+
*
|
|
2617
|
+
* Declared here rather than registered by the hook with `useAction`,
|
|
2618
|
+
* because tool hooks run wherever the consumer calls them — for
|
|
2619
|
+
* `<SceneCanvas>` that is ABOVE `<ActionsProviderIfRoot>`, where
|
|
2620
|
+
* `useActionsRegistry()` returns null and `useAction` silently no-ops. The
|
|
2621
|
+
* result was a binding pointing at an action id nothing had registered, so
|
|
2622
|
+
* the gesture fell through to whatever matched next (polygon's
|
|
2623
|
+
* wheel/arrow-key side adjustment did nothing and `nudge.*` moved the
|
|
2624
|
+
* selection instead). `<ToolActionsMounter>` registers these from inside
|
|
2625
|
+
* the provider.
|
|
2626
|
+
*/
|
|
2627
|
+
actions?: Action[];
|
|
2628
|
+
/** Hook name as exported from the kit barrel (e.g. `'useHandTool'`).
|
|
2629
|
+
* Set by built-in hooks for inspector / debugging. Consumer-authored
|
|
2630
|
+
* tools may set this to surface their hook name; omitted is fine.
|
|
2631
|
+
* Introspection-only — do not make this load-bearing in production.
|
|
2632
|
+
* Read off the def via `Tool.def` (the reflection escape hatch). */
|
|
2633
|
+
hookName?: string;
|
|
2634
|
+
presentation?: ToolPresentation<TScratch>;
|
|
2635
|
+
/** Optional caller-supplied activation key. Most built-in tools have their
|
|
2636
|
+
* activation key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`;
|
|
2637
|
+
* this field is for tools that want their activation key to be
|
|
2638
|
+
* configurable by the host (currently Lasso and Eyedropper). The dynamic
|
|
2639
|
+
* loop in `useKeybindings.ts` picks this up and appends a binding entry
|
|
2640
|
+
* to the consolidated `tool.activate` action (with `opts.params.toolId`
|
|
2641
|
+
* set so the invoker knows which tool to switch to). */
|
|
2642
|
+
keybinding?: ToolKeybinding;
|
|
2643
|
+
/** Declarative held-key trigger (reflection / inspector only). When set,
|
|
2644
|
+
* signals to the host that this tool can engage via a held key; the host
|
|
2645
|
+
* must register the activation via the consolidated `tool.offhand` action
|
|
2646
|
+
* (`makeToolOffhandAction` + `buildToolOffhandBindings`). Built-in tools
|
|
2647
|
+
* declare held keys in `BUILTIN_OFFHAND_ACTIONS`; configurable-hotkey
|
|
2648
|
+
* tools rely on the host to wire the binding. Setting this field does NOT
|
|
2649
|
+
* automatically engage the held-key behavior. */
|
|
2650
|
+
hotkey?: HotkeyTrigger;
|
|
2651
|
+
onActivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
2652
|
+
onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
|
|
2653
|
+
cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
|
|
2654
|
+
/** Override the default scratch initializer. Default is `() => null`
|
|
2655
|
+
* cast to `TScratch`, which works for tools whose scratch is fresh
|
|
2656
|
+
* every gesture. Tools that need scratch identity to survive across
|
|
2657
|
+
* gesture boundaries (e.g. the pen tool's multi-click subpath state)
|
|
2658
|
+
* pass a stable-ref-returning thunk here. The factory forwards this
|
|
2659
|
+
* onto the returned `Tool.initScratch`. */
|
|
2660
|
+
initScratch?: () => TScratch;
|
|
2661
|
+
/** Declarative gesture bindings, forwarded onto `Tool.bindings`. The
|
|
2662
|
+
* gesture dispatcher consults these at active scope while this tool is the
|
|
2663
|
+
* active one, and at hotkey scope while it is held. This is the tool's
|
|
2664
|
+
* entire input surface — the `initial` / `engaged` phase tables that used
|
|
2665
|
+
* to sit beside it are gone, along with the second dispatcher that read
|
|
2666
|
+
* them. */
|
|
2667
|
+
bindings?: GestureBinding[];
|
|
2668
|
+
/** Optional overlay layer rendered while the tool occupies any slot
|
|
2669
|
+
* (active, hotkey, or ambient), surfaced on `Tool.overlay`.
|
|
2670
|
+
*
|
|
2671
|
+
* The layer's `draw` closure should read dynamic state through refs or
|
|
2672
|
+
* closures captured in the enclosing render scope, and gate on it there —
|
|
2673
|
+
* `if (!scratch.something) return []` — rather than expecting the kit to
|
|
2674
|
+
* swap layers as the gesture progresses. */
|
|
2675
|
+
overlay?: RenderLayer<unknown>;
|
|
2676
|
+
}
|
|
2677
|
+
/** Viewport-tool spec. Once phase tables went away this stopped differing
|
|
2678
|
+
* from `ToolDef` in any structural way; `defineViewportTool` survives as the
|
|
2679
|
+
* authoring signal that a tool pans/zooms the view rather than the scene. */
|
|
2680
|
+
type ViewportToolDef<TScratch = void> = ToolDef<TScratch>;
|
|
2681
|
+
|
|
2682
|
+
/**
|
|
2683
|
+
* Build a `Tool<TScratch>` from a declarative `ToolDef<TScratch>`.
|
|
2684
|
+
*
|
|
2685
|
+
* This used to be a translator: `ToolDef` carried `initial` / `engaged` phase
|
|
2686
|
+
* tables of hit-keyed route handlers, and `defineTool` compiled them into the
|
|
2687
|
+
* imperative `pointer` / `drag` / `keyboard` / `wheel` handlers the
|
|
2688
|
+
* tool-routing dispatcher called. Both ends of that are gone — tools declare
|
|
2689
|
+
* `bindings` and the gesture dispatcher routes them — so what remains is
|
|
2690
|
+
* identity plumbing plus the id check and the cursor resolver.
|
|
2691
|
+
*
|
|
2692
|
+
* It stays a function rather than becoming a spread because the id validation
|
|
2693
|
+
* and the `initScratch` / `cursor` defaults are worth applying uniformly, and
|
|
2694
|
+
* because `Tool.def` gives reflection a handle on the authored form.
|
|
2695
|
+
*/
|
|
2696
|
+
declare function defineTool<TScratch = void>(def: ToolDef<TScratch>): Tool<TScratch>;
|
|
2697
|
+
|
|
2698
|
+
/**
|
|
2699
|
+
* Define a tool that acts on the viewport rather than the scene.
|
|
2700
|
+
*
|
|
2701
|
+
* This used to do real work: `ViewportPhaseDef` was a narrowed `PhaseDef`
|
|
2702
|
+
* (no click routes, drag restricted to the function form), and the factory
|
|
2703
|
+
* lifted it back to the permissive shape before handing it to `defineTool`.
|
|
2704
|
+
* With phase tables gone there is no shape left to narrow — a viewport tool
|
|
2705
|
+
* declares `bindings` like any other, pointing at `viewport.*` actions.
|
|
2706
|
+
*
|
|
2707
|
+
* It survives as an authoring signal. `defineViewportTool` at the top of a
|
|
2708
|
+
* hook says "this tool moves the camera, not the drawing", which is worth
|
|
2709
|
+
* more than the type gymnastics it replaced.
|
|
2710
|
+
*/
|
|
2711
|
+
declare function defineViewportTool<TScratch = void>(def: ViewportToolDef<TScratch>): Tool<TScratch>;
|
|
2712
|
+
|
|
2713
|
+
/**
|
|
2714
|
+
* One row in the route registry — a single `GestureBinding` on one tool,
|
|
2715
|
+
* flattened into the route grammar's vocabulary so the inspector, the
|
|
2716
|
+
* conflict checker, and `describeRoute` can all read the same shape.
|
|
2717
|
+
*
|
|
2718
|
+
* Multiple rows can share (gesture, arg, target, modifiers) if different tools
|
|
2719
|
+
* declare them; consumers walk the list and group client-side.
|
|
2720
|
+
*/
|
|
2721
|
+
interface RegistryEntry {
|
|
2722
|
+
toolId: string;
|
|
2723
|
+
/** Which phase the binding's `phase` spec restricts it to. `'any'` when it
|
|
2724
|
+
* declares none, declares `'*'`, or declares an atom list whose atoms
|
|
2725
|
+
* don't agree on one phase — such a binding fires in either phase, and
|
|
2726
|
+
* reporting it as `'initial'` made it collide with genuinely-initial
|
|
2727
|
+
* bindings in `findConflicts`' bucket key. */
|
|
2728
|
+
phase: 'initial' | 'engaged' | 'any';
|
|
2729
|
+
/** Structured v3 modifier requirements. Empty object = "no modifiers
|
|
2730
|
+
* held" (the strict default). */
|
|
2731
|
+
modifiers: ParsedModifiers;
|
|
2732
|
+
gesture: GestureName;
|
|
2733
|
+
/** Resolved arg value for arg-bearing gestures (wheel direction,
|
|
2734
|
+
* key name, multiTouchTap fingers). Undefined for gestures whose
|
|
2735
|
+
* descriptor has no `arg`. */
|
|
2736
|
+
arg: string | undefined;
|
|
2737
|
+
/** Target class for hit-testing gestures. `undefined` when the descriptor
|
|
2738
|
+
* has `hasTarget: false` or the spec declares no target;
|
|
2739
|
+
* {@link PREDICATE_TARGET} when the spec uses a `kindOf` predicate, which
|
|
2740
|
+
* the route grammar has no notation for. */
|
|
2741
|
+
target: string | undefined;
|
|
2742
|
+
/** The action this binding fires. */
|
|
2743
|
+
actionId: string;
|
|
2744
|
+
/** The `GestureSpec` this row was flattened from, by reference. Reflection
|
|
2745
|
+
* consumers that need something the grammar doesn't capture — the
|
|
2746
|
+
* specificity tuple, a `kindOf` predicate identity — read it here rather
|
|
2747
|
+
* than re-walking `Tool.bindings`. */
|
|
2748
|
+
spec: GestureSpec;
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Stand-in target for a `{ kindOf }` predicate spec.
|
|
2752
|
+
*
|
|
2753
|
+
* The route grammar can name target *classes* (`empty`, `selected-body`,
|
|
2754
|
+
* `kind:rect`) but not arbitrary predicates, so three distinct select-tool
|
|
2755
|
+
* bindings all render as this one token. Narrowing it would mean giving the
|
|
2756
|
+
* grammar a way to describe a function, which is a real design question and
|
|
2757
|
+
* not one this reflection layer should answer by inventing syntax.
|
|
2758
|
+
*/
|
|
2759
|
+
declare const PREDICATE_TARGET = "predicate";
|
|
2760
|
+
/**
|
|
2761
|
+
* Flatten every tool's `bindings` into route-registry rows.
|
|
2762
|
+
*
|
|
2763
|
+
* This was `buildActionRegistry`, and it walked `ToolDef.initial` /
|
|
2764
|
+
* `.engaged` phase tables — the grammar that no longer exists. It was also
|
|
2765
|
+
* blind to `Tool.bindings` the entire time the two lived side by side, which
|
|
2766
|
+
* is why the inspector under-reported select by more than half. The rename
|
|
2767
|
+
* fixes a second thing: it never had anything to do with the Actions
|
|
2768
|
+
* Registry, and reading `buildActionRegistry` next to `ActionsRegistry`
|
|
2769
|
+
* suggested otherwise.
|
|
2770
|
+
*/
|
|
2771
|
+
declare function buildRouteRegistry(tools: readonly Tool<unknown>[]): RegistryEntry[];
|
|
2772
|
+
|
|
2773
|
+
/** Two or more tools declare the same exact (phase, gesture, arg, target,
|
|
2774
|
+
* modifiers) tuple — the dispatcher's slot precedence picks one
|
|
2775
|
+
* arbitrarily (well, deterministically by slot order, but the author
|
|
2776
|
+
* probably didn't intend the duplication). */
|
|
2777
|
+
interface Conflict {
|
|
2778
|
+
phase: 'initial' | 'engaged' | 'any';
|
|
2779
|
+
gesture: GestureName;
|
|
2780
|
+
arg: string | undefined;
|
|
2781
|
+
target: string | undefined;
|
|
2782
|
+
modifiers: ParsedModifiers;
|
|
2783
|
+
/** All tool ids that registered the same tuple. At least 2 by
|
|
2784
|
+
* construction. Order matches the input tools[] order. */
|
|
2785
|
+
toolIds: string[];
|
|
2786
|
+
}
|
|
2787
|
+
/** Detect exact-tuple overlaps across a tool registration set.
|
|
2788
|
+
*
|
|
2789
|
+
* Intentionally NOT flagged:
|
|
2790
|
+
* - Broad vs. narrow targets (e.g. an untargeted `click` alongside
|
|
2791
|
+
* `click` on `empty`) — the dispatcher's specificity ordering resolves
|
|
2792
|
+
* those cleanly, and the broad one is usually the intended fallback.
|
|
2793
|
+
* - Different modifier requirements on the same target — they fire on
|
|
2794
|
+
* different inputs.
|
|
2795
|
+
* - A binding whose action declines via `enabled()` so a lower-priority
|
|
2796
|
+
* one can take the gesture. Detecting that intent would mean evaluating
|
|
2797
|
+
* the action; consumers can suppress known-intentional compositions in
|
|
2798
|
+
* their UI layer.
|
|
2799
|
+
*
|
|
2800
|
+
* Note that two bindings sharing a tuple on the SAME tool are now possible
|
|
2801
|
+
* (bindings are an array, where phase tables were objects with unique
|
|
2802
|
+
* keys) — so a conflict may name one tool twice.
|
|
2803
|
+
*/
|
|
2804
|
+
declare function findConflicts(tools: readonly Tool<unknown>[]): Conflict[];
|
|
2805
|
+
|
|
2806
|
+
declare const index_ChannelRef: typeof ChannelRef;
|
|
2807
|
+
type index_Conflict = Conflict;
|
|
2808
|
+
declare const index_DescribeRouteOptions: typeof DescribeRouteOptions;
|
|
2809
|
+
declare const index_GESTURE_DESCRIPTORS: typeof GESTURE_DESCRIPTORS;
|
|
2810
|
+
declare const index_GestureArgSpec: typeof GestureArgSpec;
|
|
2811
|
+
declare const index_GestureDescriptor: typeof GestureDescriptor;
|
|
2812
|
+
declare const index_GestureName: typeof GestureName;
|
|
2813
|
+
declare const index_ModRequirement: typeof ModRequirement;
|
|
2814
|
+
declare const index_ModifierKey: typeof ModifierKey;
|
|
2815
|
+
declare const index_PREDICATE_TARGET: typeof PREDICATE_TARGET;
|
|
2816
|
+
declare const index_ParsedModifiers: typeof ParsedModifiers;
|
|
2817
|
+
declare const index_ParsedRoute: typeof ParsedRoute;
|
|
2818
|
+
declare const index_PhaseAtom: typeof PhaseAtom;
|
|
2819
|
+
declare const index_RESERVED_ID_NAMES: typeof RESERVED_ID_NAMES;
|
|
2820
|
+
declare const index_RESERVED_ID_PREFIXES: typeof RESERVED_ID_PREFIXES;
|
|
2821
|
+
declare const index_ROUTE_FIELD_DEFINITIONS: typeof ROUTE_FIELD_DEFINITIONS;
|
|
2822
|
+
declare const index_ROUTE_TERMS: typeof ROUTE_TERMS;
|
|
2823
|
+
type index_RegistryEntry = RegistryEntry;
|
|
2824
|
+
declare const index_RouteDescriptionPart: typeof RouteDescriptionPart;
|
|
2825
|
+
declare const index_RouteFieldName: typeof RouteFieldName;
|
|
2826
|
+
declare const index_RouteTermLabel: typeof RouteTermLabel;
|
|
2827
|
+
type index_ToolDef<TScratch = void> = ToolDef<TScratch>;
|
|
2828
|
+
type index_ToolKeybinding = ToolKeybinding;
|
|
2829
|
+
type index_ViewportToolDef<TScratch = void> = ViewportToolDef<TScratch>;
|
|
2830
|
+
declare const index_buildRouteRegistry: typeof buildRouteRegistry;
|
|
2831
|
+
declare const index_canonicalModifiers: typeof canonicalModifiers;
|
|
2832
|
+
declare const index_collapseShiftPairs: typeof collapseShiftPairs;
|
|
2833
|
+
declare const index_defineTool: typeof defineTool;
|
|
2834
|
+
declare const index_defineViewportTool: typeof defineViewportTool;
|
|
2835
|
+
declare const index_describeRoute: typeof describeRoute;
|
|
2836
|
+
declare const index_describeRouteParts: typeof describeRouteParts;
|
|
2837
|
+
declare const index_findConflicts: typeof findConflicts;
|
|
2838
|
+
declare const index_formatPhaseAtom: typeof formatPhaseAtom;
|
|
2839
|
+
declare const index_formatRoute: typeof formatRoute;
|
|
2840
|
+
declare const index_getGestureDescriptor: typeof getGestureDescriptor;
|
|
2841
|
+
declare const index_isKnownGestureName: typeof isKnownGestureName;
|
|
2842
|
+
declare const index_parseRoute: typeof parseRoute;
|
|
2843
|
+
declare namespace index {
|
|
2844
|
+
export { index_ChannelRef as ChannelRef, type index_Conflict as Conflict, index_DescribeRouteOptions as DescribeRouteOptions, index_GESTURE_DESCRIPTORS as GESTURE_DESCRIPTORS, index_GestureArgSpec as GestureArgSpec, index_GestureDescriptor as GestureDescriptor, index_GestureName as GestureName, index_ModRequirement as ModRequirement, index_ModifierKey as ModifierKey, index_PREDICATE_TARGET as PREDICATE_TARGET, index_ParsedModifiers as ParsedModifiers, index_ParsedRoute as ParsedRoute, index_PhaseAtom as PhaseAtom, index_RESERVED_ID_NAMES as RESERVED_ID_NAMES, index_RESERVED_ID_PREFIXES as RESERVED_ID_PREFIXES, index_ROUTE_FIELD_DEFINITIONS as ROUTE_FIELD_DEFINITIONS, index_ROUTE_TERMS as ROUTE_TERMS, type index_RegistryEntry as RegistryEntry, index_RouteDescriptionPart as RouteDescriptionPart, index_RouteFieldName as RouteFieldName, index_RouteTermLabel as RouteTermLabel, type index_ToolDef as ToolDef, type index_ToolKeybinding as ToolKeybinding, type index_ViewportToolDef as ViewportToolDef, index_buildRouteRegistry as buildRouteRegistry, index_canonicalModifiers as canonicalModifiers, index_collapseShiftPairs as collapseShiftPairs, index_defineTool as defineTool, index_defineViewportTool as defineViewportTool, index_describeRoute as describeRoute, index_describeRouteParts as describeRouteParts, index_findConflicts as findConflicts, index_formatPhaseAtom as formatPhaseAtom, index_formatRoute as formatRoute, index_getGestureDescriptor as getGestureDescriptor, index_isKnownGestureName as isKnownGestureName, index_parseRoute as parseRoute };
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
export { type DispatcherContext as $, type Action as A, type BooleansAdapter as B, type Condition as C, type Dims as D, type ActiveToolContextProviderProps as E, type ActiveToolContextValue as F, type GeometryProjection as G, type HotkeyTrigger as H, type InsertExtras as I, type AffordanceRegion as J, type AreaSelectDep as K, type BindingOpts as L, type BindingScope as M, type BooleanOp as N, type BooleanOpResult as O, type BoundGesture as P, type BuildRuleCtxArgs as Q, type RenderLayer as R, type SliceDep as S, type Tool as T, type UseSelectionOptions as U, type VisibilityRules as V, type ClipboardIngestCtx as W, type CustomPaintContext as X, type DepName as Y, type DepRegistry as Z, DepRegistryProvider as _, type Rule as a, buildRouteRegistry as a$, type DragSample as a0, type EditAnchorsDep as a1, type GestureBinding as a2, type ImmediateInvoker as a3, type IngestCtx as a4, type IngestionDep as a5, type InsertDep as a6, type InvocationCtx as a7, type Invoker as a8, type LassoSelectDep as a9, type ViewApi as aA, applyBooleanOp as aB, buildRuleCtx as aC, createDispatcher as aD, describeRule as aE, drawLayers as aF, enterTextEditAction as aG, evaluate as aH, evaluateEnabled as aI, registerContentHandler as aJ, index as aK, sliceAction as aL, specificity as aM, useAction as aN, useActionsRegistry as aO, useActiveToolContext as aP, useDepRegistry as aQ, useDepSource as aR, useOptionalActiveToolContext as aS, useOptionalDepRegistry as aT, usePointerContext as aU, useSelection as aV, type Conflict as aW, PREDICATE_TARGET as aX, type RegistryEntry as aY, type ToolDef as aZ, type ViewportToolDef as a_, type LayoutDep as aa, type MatchResult as ab, NEVER as ac, type NodeAtPointDep as ad, type OngoingHandle as ae, type OngoingInvoker as af, type OngoingOverlay as ag, type Point2 as ah, PointerContextProvider as ai, type PointerContextValue as aj, type PointerWorldPos as ak, type ResizePolicy as al, type ResolveAllOptions as am, type ResolveOnlyResult as an, type ResolvedCandidate as ao, type ScopedBinding as ap, type SelectionExtendKey as aq, type SelectionMode as ar, type Selector as as, type SnapDep as at, type SvgUnpacker as au, type TextEditDep as av, type ToolModifiers as aw, type ToolPresentation as ax, type ToolSlot as ay, type UiOngoingControl as az, type RuleCtx as b, defineTool as b0, defineViewportTool as b1, findConflicts as b2, type ChromeCtx as c, type ChromeId as d, type DepSchema as e, type ActionsRegistry as f, type AffordanceHit as g, type Dispatcher as h, type AnyTool as i, type ToolCtx as j, type ToolKeybinding as k, type AffordanceBinding as l, type ChromeState as m, type SelectionApi as n, type ContentHandlerEntry as o, type SvgIngestOptions as p, type ActionsProp as q, type Affordance as r, ALWAYS as s, type ActionDeps as t, ActionDisabledReason as u, type ActionEnabledResult as v, type ActionEntry as w, ActionsProvider as x, ActiveToolContextProvider as y, ActiveToolContextProviderIfRoot as z };
|