@grida/hud 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3140 @@
1
+ import { a as HUDPaintStripes, f as HUDSemanticGroup, r as HUDPaint, t as HUDDraw } from "./types-3wwFisZs.js";
2
+ import { i as RotationCorner, n as CursorRenderer, r as ResizeDirection, t as CursorIcon } from "./cursor-CxS8EMvm.js";
3
+ import cmath from "@grida/cmath";
4
+ import { guide } from "@grida/cmath/_snap";
5
+ import { Measurement } from "@grida/cmath/_measurement";
6
+
7
+ //#region primitives/pixel-grid.d.ts
8
+ declare const DEFAULT_PIXEL_GRID_COLOR = "rgba(150, 150, 150, 0.15)";
9
+ declare const DEFAULT_PIXEL_GRID_STEPS: [number, number];
10
+ interface PixelGridConfig {
11
+ enabled: boolean;
12
+ /** Minimum `transform[0][0]` (uniform scale) at which the grid renders. */
13
+ zoomThreshold: number;
14
+ /**
15
+ * Optional camera transform used to space the grid. Hosts that drive the
16
+ * HUD canvas's own `setTransform` can omit this — the pixel grid falls
17
+ * back to the canvas's chrome transform. Hosts that keep the HUD at
18
+ * identity (applying the camera elsewhere — e.g. as a CSS transform on
19
+ * an outer element) must supply this explicitly and update it on every
20
+ * camera change via `setPixelGridTransform`.
21
+ */
22
+ transform?: cmath.Transform;
23
+ color?: string;
24
+ steps?: [number, number];
25
+ }
26
+ interface DrawPixelGridParams {
27
+ ctx: CanvasRenderingContext2D;
28
+ transform: cmath.Transform;
29
+ width: number;
30
+ height: number;
31
+ dpr: number;
32
+ color?: string;
33
+ steps?: [number, number];
34
+ }
35
+ declare function drawPixelGrid(p: DrawPixelGridParams): void;
36
+ //#endregion
37
+ //#region primitives/ruler.d.ts
38
+ type RulerAxis = "x" | "y";
39
+ /**
40
+ * Width (in CSS pixels) of the strip the top ruler occupies along the
41
+ * top edge of the viewport, and equivalently the width of the strip the
42
+ * left ruler occupies along the left edge. Same value for both so the
43
+ * corner clip is square — change one and the L-shape stops aligning.
44
+ */
45
+ declare const DEFAULT_RULER_STRIP = 20;
46
+ declare const DEFAULT_RULER_TICK_HEIGHT = 6;
47
+ declare const DEFAULT_RULER_OVERLAP_THRESHOLD = 80;
48
+ declare const DEFAULT_RULER_TEXT_SIDE_OFFSET = 12;
49
+ declare const DEFAULT_RULER_FONT = "10px sans-serif";
50
+ declare const DEFAULT_RULER_COLOR = "rgba(128, 128, 128, 0.5)";
51
+ declare const DEFAULT_RULER_ACCENT_BACKGROUND = "rgba(80, 200, 255, 0.25)";
52
+ declare const DEFAULT_RULER_ACCENT_COLOR = "rgba(80, 200, 255, 1)";
53
+ declare const DEFAULT_RULER_BACKGROUND = "transparent";
54
+ declare const DEFAULT_RULER_STEPS: readonly number[];
55
+ /**
56
+ * Recommended drag distance threshold (in CSS pixels) for the ruler's
57
+ * create-guide gesture. Hosts implementing drag-from-strip should not
58
+ * commit a new guide until the pointer has moved this far from
59
+ * pointer-down — without it, a stray click on the strip creates an
60
+ * unwanted guide.
61
+ *
62
+ * Threshold-only — hud does not own the gesture. It owns the value
63
+ * because the value is a property of the ruler chrome's UX, not of
64
+ * any particular host's gesture pipeline.
65
+ */
66
+ declare const DEFAULT_RULER_DRAG_THRESHOLD = 4;
67
+ type RulerRange = [a: number, b: number];
68
+ /**
69
+ * A priority mark on a ruler strip — typically a guide position the host
70
+ * wants the user to see at all zooms, regardless of where the regular
71
+ * step ticks land.
72
+ *
73
+ * With only `pos` set, the mark renders identically to a regular step
74
+ * tick — short stroke at `tickHeight`, label color = `color`. The extra
75
+ * fields below let a consumer render a mark as a full-strip line with
76
+ * an accent stroke + label color — the standard guide-position
77
+ * affordance every editor ships:
78
+ *
79
+ * ```ts
80
+ * { pos: 120, strokeHeight: strip, strokeColor: red, color: red, text: "120" }
81
+ * ```
82
+ *
83
+ * Defaults are chosen so an existing minimal `{ pos }` mark keeps
84
+ * rendering exactly as before. All extra fields are optional.
85
+ */
86
+ interface RulerMark {
87
+ pos: number;
88
+ /** Tick stroke + (default) label color. */
89
+ color?: string;
90
+ /** Label text. */
91
+ text?: string;
92
+ /** Override the stroke color independently of the label color. */
93
+ strokeColor?: string;
94
+ /** Stroke width in CSS pixels. Default 1. */
95
+ strokeWidth?: number;
96
+ /**
97
+ * Stroke height in CSS pixels. Default `tickHeight`. Pass `strip`
98
+ * (the strip width) for a full-strip mark — the standard
99
+ * guide-position affordance.
100
+ */
101
+ strokeHeight?: number;
102
+ /** Label color. Defaults to `color` if omitted. */
103
+ textColor?: string;
104
+ /** Label alignment. Default "center". */
105
+ textAlign?: CanvasTextAlign;
106
+ /** Label position offset from `pos`. Default 0. */
107
+ textAlignOffset?: number;
108
+ }
109
+ /**
110
+ * Public config for the back-most ruler chrome.
111
+ *
112
+ * The HUD draws both axes in one pass: a horizontal strip across the top
113
+ * edge and a vertical strip down the left edge, with the corner square
114
+ * left blank. This is the L-shape every editor ruler ships.
115
+ *
116
+ * Coordinate model: identical to `PixelGridConfig` — `transform` is the
117
+ * camera (screen ↔ doc). If omitted, the HUD's chrome transform is used.
118
+ * Hosts that drive the HUD canvas at identity (e.g. the camera is on the
119
+ * underlying SVG/canvas) MUST supply this and update it on camera change
120
+ * via `setRulerTransform`.
121
+ */
122
+ interface RulerConfig {
123
+ enabled: boolean;
124
+ /** Optional camera transform. See `PixelGridConfig.transform` for the
125
+ * two-transform contract. */
126
+ transform?: cmath.Transform;
127
+ /** Which axes to render. Default both. */
128
+ axes?: readonly RulerAxis[];
129
+ /** Strip width in CSS pixels. Default {@link DEFAULT_RULER_STRIP}. */
130
+ strip?: number;
131
+ /** Tick line height in CSS pixels. */
132
+ tickHeight?: number;
133
+ /** Ranges to highlight (in doc-space units), per axis. */
134
+ ranges?: {
135
+ x?: readonly RulerRange[];
136
+ y?: readonly RulerRange[];
137
+ };
138
+ /** Priority marks (in doc-space units), per axis. */
139
+ marks?: {
140
+ x?: readonly RulerMark[];
141
+ y?: readonly RulerMark[];
142
+ };
143
+ /** Minimum tick spacing in screen px before ticks fade near priority points. */
144
+ overlapThreshold?: number;
145
+ /** Offset from the strip edge for the label text. */
146
+ textSideOffset?: number;
147
+ /** Label font. */
148
+ font?: string;
149
+ /** Background fill for the ruler strip. */
150
+ backgroundColor?: string;
151
+ /** Tick + label color. */
152
+ color?: string;
153
+ /**
154
+ * Color of the 1-px inner-edge separator (the line where the strip
155
+ * meets the editing area). Defaults to `color` — the tick color —
156
+ * so existing consumers don't regress.
157
+ *
158
+ * Every production editor (Figma, Sketch, XD, Illustrator, Affinity)
159
+ * paints this line distinctly LIGHTER than the ticks. With shared
160
+ * color the host has to choose: ticks readable but separator looks
161
+ * like a heavy underline, OR separator correct but ticks too faint.
162
+ * Pass a lighter value here to match the universal convention; e.g.
163
+ * the OKLCH `border` token most design systems already define.
164
+ *
165
+ * The separator field is decoupled from `color` precisely because
166
+ * the two responsibilities (read-the-number vs. mark-the-edge) want
167
+ * different weights, and no single token gets both right.
168
+ */
169
+ borderColor?: string;
170
+ /** Range fill color. */
171
+ accentBackgroundColor?: string;
172
+ /** Range label / boundary color. */
173
+ accentColor?: string;
174
+ /** Custom step series, e.g. for non-decimal units. */
175
+ steps?: readonly number[];
176
+ /**
177
+ * Subdivisions between major ticks. See `@grida/ruler` for the heuristic.
178
+ * - `false` / `0`: no subticks (default)
179
+ * - `true` / `"auto"`: 1-2-5 heuristic
180
+ * - `number`: fixed subdivision count
181
+ */
182
+ subticks?: false | true | "auto" | number;
183
+ /** Subtick line height. Defaults to `round(tickHeight * 0.4)`. */
184
+ subtickHeight?: number;
185
+ /** Subtick color. Defaults to `color`. */
186
+ subtickColor?: string;
187
+ }
188
+ interface DrawRulerParams {
189
+ ctx: CanvasRenderingContext2D;
190
+ transform: cmath.Transform;
191
+ /** Viewport width in CSS pixels. */
192
+ width: number;
193
+ /** Viewport height in CSS pixels. */
194
+ height: number;
195
+ /** Device pixel ratio of the canvas. */
196
+ dpr: number;
197
+ config: RulerConfig;
198
+ }
199
+ /**
200
+ * Paint the L-shape ruler chrome (top + left strips) into the canvas
201
+ * context. Stateless: every call clears and redraws the strips in
202
+ * screen-space.
203
+ *
204
+ * The function assumes the caller has reset the ctx transform to identity
205
+ * for the device pixel scale; it applies `dpr` itself via `setTransform`.
206
+ *
207
+ * The corner square (`strip × strip` at origin) is deliberately left
208
+ * blank — neither axis is in charge of it. Hosts that want a corner fill
209
+ * draw it as a host-fed extra above the HUD.
210
+ */
211
+ declare function drawRuler(p: DrawRulerParams): void;
212
+ //#endregion
213
+ //#region primitives/transform-box.d.ts
214
+ /**
215
+ * 2×3 affine transform. Structurally compatible with `AffineTransform`
216
+ * from `@grida/cg` — the HUD package keeps a local alias to avoid
217
+ * acquiring a `@grida/cg` workspace dep just for the type.
218
+ */
219
+ type AffineTransform = [[number, number, number], [number, number, number]];
220
+ type TransformBoxAction = {
221
+ type: "translate";
222
+ delta: cmath.Vector2;
223
+ } | {
224
+ type: "scale-side";
225
+ side: cmath.RectangleSide;
226
+ delta: cmath.Vector2;
227
+ } | {
228
+ type: "rotate";
229
+ corner: cmath.IntercardinalDirection;
230
+ delta: cmath.Vector2;
231
+ };
232
+ interface TransformBoxOptions {
233
+ size: cmath.Vector2;
234
+ }
235
+ type TransformBoxCorners = {
236
+ nw: cmath.Vector2;
237
+ ne: cmath.Vector2;
238
+ se: cmath.Vector2;
239
+ sw: cmath.Vector2;
240
+ };
241
+ /**
242
+ * Reduces a transform-box affine in response to a UI action.
243
+ */
244
+ declare function reduceTransformBox(base: AffineTransform, action: TransformBoxAction, options: TransformBoxOptions): AffineTransform;
245
+ /**
246
+ * Given a transform-box matrix and the box size, returns the four
247
+ * transformed corners in pixel space.
248
+ *
249
+ * Each corner = transform * box_corner
250
+ */
251
+ declare function getTransformBoxCorners(transform: AffineTransform, size: cmath.Vector2): TransformBoxCorners;
252
+ /**
253
+ * Decomposes an affine transform into rotation (deg), scale [sx, sy],
254
+ * and translation [tx, ty] components.
255
+ */
256
+ declare function decompose(transform: AffineTransform): {
257
+ rotation: number;
258
+ scale: cmath.Vector2;
259
+ translation: cmath.Vector2;
260
+ };
261
+ /**
262
+ * Composes rotation (deg), scale, and translation into an affine
263
+ * transform, anchored so the supplied `center` is preserved.
264
+ */
265
+ declare function compose(rotation: number, scale: cmath.Vector2, translation: cmath.Vector2, center: cmath.Vector2): AffineTransform;
266
+ /**
267
+ * Convert pixel-space corners back to a box-relative transform.
268
+ */
269
+ declare function cornersToBoxTransform(corners: TransformBoxCorners, size: cmath.Vector2): AffineTransform;
270
+ //#endregion
271
+ //#region event/shape.d.ts
272
+ /**
273
+ * Selection shape — what the chrome wraps.
274
+ *
275
+ * Hosts return a `SelectionShape` from `shapeOf(id)` so the HUD can lay out
276
+ * the right kind of chrome:
277
+ *
278
+ * - **`rect`** — standard bounding box. Renders selection outline + 4 corner
279
+ * knobs, with virtual edge / rotation hit regions.
280
+ * - **`transformed`** — an axis-aligned local bbox PLUS a 2×3 affine matrix
281
+ * mapping local-frame points into doc-space. Covers rotation, skew,
282
+ * non-uniform scale, and mirror with one code path. `local.width × local.height`
283
+ * are the artwork's true dims (read by the size badge and by `applyResize`
284
+ * in local space). Identity matrix here is byte-equivalent to
285
+ * `{ kind: "rect", rect: local }`.
286
+ * - **`line`** — two-endpoint primitive (e.g. SVG `<line>`). Renders the
287
+ * line segment + 2 endpoint knobs. No edge / rotation regions.
288
+ * - **`unresolved`** — internal-only. Used inside `SelectionGroup` when the
289
+ * host gave a flat `NodeId[]` to `setSelection` — the chrome builder
290
+ * resolves the real shape by calling `shapeOf(id)` at frame build time.
291
+ * Hosts never emit this; treating it as host-facing would be a bug.
292
+ *
293
+ * Future kinds (e.g. `polyline`, `ellipse_oriented`) can be added without
294
+ * breaking the existing kinds — the chrome builder branches on `kind`.
295
+ */
296
+ type SelectionShape = {
297
+ kind: "rect";
298
+ rect: Rect;
299
+ } | {
300
+ kind: "transformed"; /** Local-frame AABB; width/height are artwork's true dims. */
301
+ local: Rect; /** 2×3 affine mapping local-frame points → doc-space. */
302
+ matrix: cmath.Transform;
303
+ } | {
304
+ kind: "line";
305
+ p1: cmath.Vector2;
306
+ p2: cmath.Vector2;
307
+ } | {
308
+ kind: "unresolved";
309
+ id: string;
310
+ };
311
+ /**
312
+ * A logical selection group. The HUD renders one chrome instance per group.
313
+ *
314
+ * The host pre-computes `shape` (typically the union of member bounds for
315
+ * multi-node groups). Member `ids` are the gesture target — they all
316
+ * translate/resize together as a unit.
317
+ */
318
+ interface SelectionGroup {
319
+ ids: readonly NodeId[];
320
+ shape: SelectionShape;
321
+ }
322
+ //#endregion
323
+ //#region event/gesture.d.ts
324
+ /**
325
+ * Rect type local to the event/ layer.
326
+ *
327
+ * Mirrors `cmath.Rectangle` shape; declared here to keep `event/` free of
328
+ * imports beyond `cmath` types.
329
+ */
330
+ interface Rect {
331
+ x: number;
332
+ y: number;
333
+ width: number;
334
+ height: number;
335
+ }
336
+ type NodeId = string;
337
+ /**
338
+ * Active interaction state for the surface.
339
+ *
340
+ * Coordinates inside each variant are documented per-field. The surface
341
+ * stores anchor points in document-space (so they survive camera pans during
342
+ * a gesture); incremental deltas are computed against the anchor each move.
343
+ */
344
+ type SurfaceGesture = {
345
+ kind: "idle";
346
+ } | {
347
+ kind: "pan"; /** Last screen-space pointer position. */
348
+ prev_screen: cmath.Vector2;
349
+ } | {
350
+ kind: "marquee"; /** Anchor (pointer-down) in document-space. */
351
+ anchor_doc: cmath.Vector2; /** Current pointer in document-space. */
352
+ current_doc: cmath.Vector2;
353
+ } | {
354
+ /**
355
+ * Freeform polygon selection. Sibling to `marquee` — empty-space drag
356
+ * promotes here when the host has set the surface's
357
+ * `vectorSelectionMode` to `"lasso"`. The HUD doesn't decide which
358
+ * gesture to use; the host pushes the mode in alongside its tool
359
+ * toggle. See `Surface.setVectorSelectionMode`.
360
+ */
361
+ kind: "lasso"; /** First sample (pointer-down position in document-space). */
362
+ anchor_doc: cmath.Vector2;
363
+ /**
364
+ * Polyline samples in document-space, oldest-first.
365
+ * `points[0] === anchor_doc`. Per pointer_move samples are appended
366
+ * only when the rounded screen-pixel differs from the last sample,
367
+ * keeping growth bounded on slow drags. The host's hit-test closes
368
+ * the polygon implicitly (last → first) — matches
369
+ * `cmath.polygon.pointInPolygon`'s ray-cast.
370
+ */
371
+ points: cmath.Vector2[];
372
+ } | {
373
+ kind: "translate"; /** Selected ids at the start of the gesture. */
374
+ ids: NodeId[]; /** Anchor (pointer-down) in document-space. */
375
+ anchor_doc: cmath.Vector2; /** Last reported pointer in document-space. */
376
+ last_doc: cmath.Vector2;
377
+ } | {
378
+ kind: "resize"; /** Member ids of the group being resized (1 or more). */
379
+ ids: NodeId[]; /** Which handle the user grabbed. */
380
+ direction: ResizeDirection;
381
+ /**
382
+ * Selection shape at gesture start. For `kind: "rect"` this is the
383
+ * doc-space bbox; for `kind: "transformed"` it carries the local-frame
384
+ * AABB + matrix so resize math runs in the rotated/skewed local frame.
385
+ */
386
+ initial_shape: SelectionShape; /** Anchor (pointer-down) in document-space. */
387
+ anchor_doc: cmath.Vector2; /** Current shape during the gesture. Same kind as `initial_shape`. */
388
+ current_shape: SelectionShape;
389
+ } | {
390
+ kind: "rotate";
391
+ ids: NodeId[];
392
+ corner: RotationCorner; /** Subject center in document-space. */
393
+ center_doc: cmath.Vector2; /** Angle at gesture start (radians). */
394
+ anchor_angle: number; /** Current angle (radians). */
395
+ current_angle: number;
396
+ /**
397
+ * Selection's screen-space rotation at gesture start (radians).
398
+ * Composed with `current_angle - anchor_angle` each frame to set
399
+ * the rotate-cursor's `baseAngle` — so cursors on already-rotated
400
+ * selections continue tilting correctly mid-gesture instead of
401
+ * snapping back to 0 + delta.
402
+ */
403
+ initial_cursor_angle: number;
404
+ } | {
405
+ kind: "endpoint";
406
+ id: NodeId;
407
+ endpoint: "p1" | "p2"; /** Current endpoint position in document-space. */
408
+ pos_doc: cmath.Vector2;
409
+ } | {
410
+ /**
411
+ * Dragging one or more vertices of a path under content-edit. The
412
+ * `indices` are captured at gesture start; intra-gesture selection
413
+ * mirror changes do not affect what the gesture moves.
414
+ */
415
+ kind: "translate_vertex"; /** Path node id under content-edit. */
416
+ node_id: NodeId; /** Vertex indices being moved. */
417
+ indices: number[]; /** Anchor (pointer-down) in document-space. */
418
+ anchor_doc: cmath.Vector2; /** Last reported pointer in document-space. */
419
+ last_doc: cmath.Vector2;
420
+ } | {
421
+ /**
422
+ * Dragging the path-edit sub-selection. Triggered by segment-body
423
+ * drag (the default — Meta switches to `bend_segment` instead) and
424
+ * any future drag origin that targets the whole sub-selection
425
+ * (multi-vertex drag is the planned follow-up).
426
+ *
427
+ * The HUD doesn't know which vertices are in the sub-selection
428
+ * (host owns it). It DOES know "this gesture additionally targets
429
+ * these vertex indices" — e.g. the endpoints of the segment that
430
+ * initiated the drag, when that segment isn't yet in the
431
+ * sub-selection. The host UNIONs `additional_vertex_indices` with
432
+ * its authoritative sub-selection on each preview frame.
433
+ */
434
+ kind: "translate_vector_selection";
435
+ node_id: NodeId;
436
+ additional_vertex_indices: readonly number[];
437
+ anchor_doc: cmath.Vector2;
438
+ last_doc: cmath.Vector2;
439
+ } | {
440
+ /**
441
+ * Dragging a tangent control point. The host applies the mirror
442
+ * policy (`auto` by default — infer smooth-vs-broken). The chrome
443
+ * builder owned the anchor; this gesture only tracks the moving
444
+ * end of the handle line.
445
+ */
446
+ kind: "translate_tangent";
447
+ node_id: NodeId;
448
+ tangent: readonly [number, 0 | 1];
449
+ /** Tangent control point's doc-space position at gesture start
450
+ * (carried from chrome via `decision.pos`). */
451
+ anchor_doc: cmath.Vector2;
452
+ last_doc: cmath.Vector2;
453
+ /** Pointer's doc-space position at gesture start. Distinct from
454
+ * `anchor_doc` because the cursor lands within the knob's Fitts'-
455
+ * tolerant hit area, not pixel-perfect on the control point.
456
+ * Used to detect "click-no-drag": commit is skipped when
457
+ * `last_doc === down_doc`, otherwise the absolute set_tangent
458
+ * would snap the control point to the cursor's down position. */
459
+ down_doc: cmath.Vector2;
460
+ } | {
461
+ /**
462
+ * Bending a segment. Press-down sampled a point on the curve at
463
+ * parameter `ca`; drag moves that point toward `last_doc`. The
464
+ * host re-solves tangents on every frame. Endpoints (a, b) stay
465
+ * fixed for the duration of the gesture.
466
+ */
467
+ kind: "bend_segment";
468
+ node_id: NodeId;
469
+ segment: number; /** Frozen parametric position of the sampled point. */
470
+ ca: number;
471
+ anchor_doc: cmath.Vector2;
472
+ last_doc: cmath.Vector2;
473
+ } | {
474
+ /**
475
+ * Dragging a corner-radius handle.
476
+ *
477
+ * For RECT geometry the gesture carries the rect AABB and the
478
+ * candidate-anchor set captured at pointer_down. When the user
479
+ * grabs a single-corner knob (sub-max radii), `candidates` has
480
+ * length 1 and `anchor` is set. When the user grabs a
481
+ * coincidence group (oblong-max pair, square-max quadruple),
482
+ * `candidates` has length 2+ and `anchor` is `null` until the
483
+ * drag crosses the threshold; the state machine resolves the
484
+ * anchor from drag direction AMONG `candidates` only.
485
+ *
486
+ * For LINE geometry the gesture carries `a` and `b`; the
487
+ * projection axis is `a → b`. `anchor` is always `null` and
488
+ * `candidates` is empty.
489
+ *
490
+ * The new radius is derived each frame by projecting the
491
+ * cursor onto the relevant axis (rect: corner →
492
+ * `corner + (sign_x, sign_y)`; line: `a → b`). `explicit`
493
+ * latches the alt modifier at gesture start — the intent kind
494
+ * is decided once.
495
+ */
496
+ kind: "corner_radius";
497
+ node_id: NodeId;
498
+ geometry: "rect" | "line";
499
+ /** RECT only — the rect AABB in LOCAL space. Used to derive
500
+ * per-anchor corner positions for projection. Undefined for
501
+ * line. */
502
+ rect?: {
503
+ x: number;
504
+ y: number;
505
+ width: number;
506
+ height: number;
507
+ };
508
+ /** RECT only — optional local → doc transform. Threaded
509
+ * through from the input so the gesture projects the cursor
510
+ * along the ROTATED axis on a rotated rect. */
511
+ transform?: cmath.Transform;
512
+ /** RECT only — the candidate anchors this gesture was opened
513
+ * for. Length 1 means anchor is locked at start; length 2 or
514
+ * 4 means anchor is `null` until threshold + direction
515
+ * resolution picks one. Empty for line. */
516
+ candidates: readonly ("nw" | "ne" | "se" | "sw")[];
517
+ /** Resolved corner anchor for rect (one of `candidates`).
518
+ * `null` while pre-resolution on a multi-candidate group, OR
519
+ * always for line geometry. */
520
+ anchor: "nw" | "ne" | "se" | "sw" | null; /** LINE only — the line endpoints. Undefined for rect. */
521
+ a?: cmath.Vector2;
522
+ b?: cmath.Vector2;
523
+ /** Screen-space pointer-down anchor — used by the multi-
524
+ * candidate threshold + direction resolution. */
525
+ anchor_screen: cmath.Vector2;
526
+ /** Doc-space pointer-down anchor — paired with `transform` to
527
+ * resolve the right corner on a rotated rect. The threshold
528
+ * check stays in screen-space (pixels), but the
529
+ * direction-resolution dot-product compares the doc-space drag
530
+ * delta (`point_doc - anchor_doc`) against the rect's
531
+ * rotated-into-doc sign vectors (`T.linear · sign_local`). */
532
+ anchor_doc: cmath.Vector2;
533
+ /** Whether alt was held at pointer_down. Latches at gesture
534
+ * start; intent kind is decided once. */
535
+ explicit: boolean; /** Most-recent doc-space pointer. */
536
+ last_doc: cmath.Vector2; /** Most-recent computed radius value (doc-space units). */
537
+ value: number;
538
+ /** Flips to `true` the first time `pointer_move` advances the
539
+ * gesture state (resolves the anchor or updates `value`).
540
+ * Pointer-up consults this to skip the commit on click-only
541
+ * interactions — `value` is seeded from the inset-padded knob
542
+ * position at pointer-down, so a pure click would otherwise
543
+ * commit a non-zero radius the user never intended. */
544
+ dragged: boolean;
545
+ } | {
546
+ /**
547
+ * Drag of a `parametric_handle` knob — opened from a
548
+ * `parametric_knob` overlay action and emits `parametric_handle`
549
+ * intents on every move + commit.
550
+ *
551
+ * When the action carries multiple `candidates` (a coincident
552
+ * group), `handle_id` is `null` until the drag crosses the
553
+ * threshold and direction-resolution picks one. `candidates`
554
+ * is the ordered list of (handle_id, curve, domain) tuples
555
+ * that hit-region stood in for.
556
+ *
557
+ * `modifiers` is the latched alt/shift state at pointer_down —
558
+ * intent payload reports it unchanged; host interprets.
559
+ */
560
+ kind: "parametric_handle";
561
+ node_id: NodeId;
562
+ candidates: readonly {
563
+ handle_id: string;
564
+ track_doc: cmath.ui.Curve | cmath.ui.PointSet;
565
+ domain: {
566
+ min: number;
567
+ max: number;
568
+ step?: number;
569
+ };
570
+ }[];
571
+ handle_id: string | null;
572
+ anchor_screen: cmath.Vector2;
573
+ modifiers: {
574
+ alt: boolean;
575
+ shift: boolean;
576
+ };
577
+ last_doc: cmath.Vector2; /** Last computed value in host units (post-step-quantization). */
578
+ value: number;
579
+ /** Flips to `true` the first time `pointer_move` advances the
580
+ * gesture state (resolves the handle or updates `value`).
581
+ * Pointer-up skips commit on click-only interactions because
582
+ * `value` is seeded from the inset-padded knob position at
583
+ * pointer-down. */
584
+ dragged: boolean;
585
+ } | {
586
+ /**
587
+ * Drag of a `padding_handle` knob (padding named class). Opened
588
+ * eagerly from a `padding_handle` overlay action; emits
589
+ * `padding_handle` intents on every move + commit.
590
+ *
591
+ * `mirror` is NOT latched at gesture start — read live on each
592
+ * frame from `state.modifiers.alt`. Per the doctrine: "modifier
593
+ * change mid-gesture updates `mirror` on subsequent previews."
594
+ *
595
+ * The container `rect` and starting value are captured at
596
+ * pointer_down so subsequent value projection (`projectPaddingValue`)
597
+ * stays exact through camera moves — value math runs in doc-space
598
+ * against the rect snapshot, not the live geometry.
599
+ */
600
+ kind: "padding_handle";
601
+ node_id: NodeId;
602
+ side: cmath.RectangleSide; /** Container rect snapshot at gesture start (doc-space). */
603
+ rect: Rect; /** Initial padding value at gesture start (doc-space units). */
604
+ initial_value: number; /** Most-recent doc-space pointer. */
605
+ last_doc: cmath.Vector2; /** Most-recent computed value (doc-space units, clamped to [0, max]). */
606
+ value: number;
607
+ /** Flips to `true` the first time `pointer_move` advances the
608
+ * gesture state. Pointer-up skips commit on click-only
609
+ * interactions — `value === initial_value` on first frame. */
610
+ dragged: boolean;
611
+ } | {
612
+ /**
613
+ * Drag of a transform-box handle (transform-box named class).
614
+ * Opened eagerly from a `transform_box_{body,side,corner}` overlay
615
+ * action; emits `transform_box` intents on every move + commit.
616
+ *
617
+ * `base_transform` is the transform at gesture start (frozen);
618
+ * each preview reduces from `base_transform` against the
619
+ * cumulative doc-space delta. `size` and `rotation` are the
620
+ * container parameters at gesture start (frozen — container
621
+ * doesn't change shape mid-gesture).
622
+ */
623
+ kind: "transform_box";
624
+ id: string;
625
+ op: {
626
+ type: "translate";
627
+ } | {
628
+ type: "scale_side";
629
+ side: cmath.RectangleSide;
630
+ } | {
631
+ type: "rotate";
632
+ corner: cmath.IntercardinalDirection;
633
+ }; /** Box size in doc-space units (frozen at gesture start). */
634
+ size: cmath.Vector2;
635
+ /** Container rotation (degrees) at gesture start. Used to de-rotate
636
+ * doc-space cursor delta into box-local frame before reducing. */
637
+ rotation: number; /** Transform at gesture start (frozen). */
638
+ base_transform: AffineTransform; /** Pointer-down doc-space position. */
639
+ start_doc: cmath.Vector2; /** Most-recent doc-space pointer. */
640
+ last_doc: cmath.Vector2; /** Most-recent reduced transform (used for the commit emit). */
641
+ transform: AffineTransform; /** Flips to `true` once `pointer_move` advances. Click-no-drag → no commit. */
642
+ dragged: boolean;
643
+ };
644
+ //#endregion
645
+ //#region primitives/corner-radius.d.ts
646
+ /**
647
+ * Screen-px inset of the handle's RESTING position from its anchoring
648
+ * corner (rect) or endpoint (line).
649
+ *
650
+ * The handle is floored to this distance whenever no gesture is in
651
+ * flight, so a radius=0 knob still sits inside the rect — visible and
652
+ * grabbable, not occluded by the resize-corner knob. During a gesture
653
+ * the floor is lifted: position follows the cursor.
654
+ *
655
+ * Locked, not configurable. Per `/sdk-design`: "core, not customizable."
656
+ */
657
+ declare const DEFAULT_CORNER_RADIUS_HANDLE_INSET = 16;
658
+ /** Default screen-px size of the knob. Matches the visual size of
659
+ * resize-corner knobs by convention. */
660
+ declare const DEFAULT_CORNER_RADIUS_HANDLE_SIZE = 8;
661
+ /** Default screen-px hit-rect size. Padded above the visual knob per
662
+ * the package's render/hit asymmetry rule. */
663
+ declare const DEFAULT_CORNER_RADIUS_HIT_SIZE = 16;
664
+ /**
665
+ * One of the four named corners of a rect. Matches the package's
666
+ * existing `IntercardinalDirection` convention used by resize handles.
667
+ */
668
+ type CornerRadiusAnchor = "nw" | "ne" | "se" | "sw";
669
+ /**
670
+ * Per-corner rectangular radius. Hosts populate all four — even
671
+ * "all-uniform" rounded rects pass `{ tl, tr, br, bl }` with the same
672
+ * value. The HUD never assumes uniformity; the surface dedup-collapses
673
+ * geometrically (when handles coincide) rather than value-checking.
674
+ */
675
+ interface CornerRadiusRectangular {
676
+ tl: number;
677
+ tr: number;
678
+ br: number;
679
+ bl: number;
680
+ }
681
+ /**
682
+ * Public input handed to `surface.setCornerRadius(...)`.
683
+ *
684
+ * - `rect` ↔ `CornerRadiusRectangular` (four per-corner values, each
685
+ * knob travels at 45° from its corner; max radius `min(w,h)/2`).
686
+ * - `line` ↔ `{ value }` (one radius, knob travels along a→b).
687
+ *
688
+ * Illegal combinations (uniform-on-rect, rectangular-on-line) are
689
+ * unreachable by type.
690
+ */
691
+ type CornerRadiusInput = {
692
+ node_id: NodeId;
693
+ geometry: {
694
+ kind: "rect";
695
+ /** Rect in LOCAL space (axis-aligned). When `transform` is
696
+ * omitted, "local" and "doc" coincide and the rect is the
697
+ * doc-space AABB. */
698
+ rect?: Rect;
699
+ /**
700
+ * Optional local → doc transform (2×3 affine). When set,
701
+ * handle positions are computed in local space (the four
702
+ * intercardinal diagonals from each corner) and then
703
+ * transformed into doc space — so rotated rects show their
704
+ * knobs along the ROTATED diagonals, not the doc-axis ones.
705
+ * Pure rotation (orthonormal) is the supported common case;
706
+ * non-orthonormal transforms work for rendering but are
707
+ * untested for the gesture's projection math.
708
+ */
709
+ transform?: cmath.Transform;
710
+ };
711
+ radius: CornerRadiusRectangular;
712
+ } | {
713
+ node_id: NodeId;
714
+ geometry: {
715
+ kind: "line";
716
+ a: cmath.Vector2;
717
+ b: cmath.Vector2;
718
+ };
719
+ radius: {
720
+ value: number;
721
+ };
722
+ };
723
+ /**
724
+ * One handle the producer wants on screen: the doc-space anchor it
725
+ * sits at (so the camera moves it with the document), the screen-px
726
+ * size of the knob and its hit AABB, and a stable label identifying
727
+ * which interaction it triggers.
728
+ *
729
+ * `anchor` is `"nw" | "ne" | "se" | "sw"` for per-corner rect handles,
730
+ * `"line"` for line geometry. The surface owns the coincidence
731
+ * collapsing (and `cornerRadiusLayoutGroups` exposes the grouping
732
+ * predicate); the pure layout always returns one entry per corner.
733
+ */
734
+ interface CornerRadiusHandleLayout {
735
+ /** Doc-space anchor — projected through the camera each frame. */
736
+ pos: cmath.Vector2;
737
+ /** Screen-px visual knob size. */
738
+ size: number;
739
+ /** Screen-px hit AABB size (>= size, padded per Fitts'). */
740
+ hit_size: number;
741
+ /** `nw/ne/se/sw` for per-corner rect handles, `"line"` for line. */
742
+ anchor: CornerRadiusAnchor | "line";
743
+ /** Stable identifier — `"corner_radius:<anchor>"` or
744
+ * `"corner_radius:line"`. */
745
+ label: string;
746
+ }
747
+ /**
748
+ * Sign of the intercardinal direction from a corner toward the rect's
749
+ * interior. NW corner → (+1, +1); NE → (-1, +1); SE → (-1, -1);
750
+ * SW → (+1, -1). The handle travels in this direction from the corner
751
+ * by `r` in BOTH x and y.
752
+ */
753
+ declare function cornerRadiusAnchorSign(anchor: CornerRadiusAnchor): readonly [-1 | 1, -1 | 1];
754
+ /**
755
+ * Compute the handle position for one corner of a rect.
756
+ *
757
+ * The math — the handle IS the arc center of the rounded corner:
758
+ *
759
+ * reach = radius (during gesture)
760
+ * reach = max(radius, pad_screen / zoom) (at rest / commit)
761
+ * reach_cap = min(w, h) / 2 (closest-edge half)
762
+ * reach = clamp(reach, 0, reach_cap)
763
+ * handle = corner + (sign_x · reach, sign_y · reach)
764
+ *
765
+ * `reach` is the doc-space distance the arc center sits from the
766
+ * corner along EACH axis (x AND y). For a square at max, all four
767
+ * arc centers land at the rect's center. For an oblong at max
768
+ * (reach = min(w,h)/2), pairs along the SHORTER axis coincide:
769
+ *
770
+ * - w > h: TL/BL share `(r, h/2)`; TR/BR share `(w-r, h/2)`
771
+ * - h > w: TL/TR share `(w/2, r)`; BL/BR share `(w/2, h-r)`
772
+ *
773
+ * `during_gesture` lifts the padded floor: the handle can sit AT the
774
+ * corner at radius=0. After release the floor is re-applied and the
775
+ * handle snaps back to a grabbable position.
776
+ *
777
+ * `zoom` is the camera's uniform scale (`transform[0][0]`); pass `1`
778
+ * for unit tests against doc-space math.
779
+ */
780
+ declare function cornerRadiusHandlePosRect(rect: Rect, anchor: CornerRadiusAnchor, radius: number, zoom: number, opts?: {
781
+ pad?: number;
782
+ during_gesture?: boolean;
783
+ transform?: cmath.Transform;
784
+ }): cmath.Vector2;
785
+ /**
786
+ * Compute the handle position along a line's `a → b` axis. Single
787
+ * handle for `line` geometry; same gesture-aware padding rule as the
788
+ * rect case. The saturation cap is `dist(a, b)`.
789
+ *
790
+ * The track here is the a→b segment of arbitrary direction, so the
791
+ * per-axis convention used by the rect case doesn't apply — `pad`
792
+ * converts to track-units as `pad / zoom`.
793
+ */
794
+ declare function cornerRadiusHandlePosLine(a: cmath.Vector2, b: cmath.Vector2, radius: number, zoom: number, opts?: {
795
+ pad?: number;
796
+ during_gesture?: boolean;
797
+ }): cmath.Vector2;
798
+ /**
799
+ * Compute the layout for the corner-radius handles of one input.
800
+ *
801
+ * Pure: same inputs → same handles. No camera reads, no DOM.
802
+ *
803
+ * - `rect` geometry → 4 handles in `nw/ne/se/sw` declaration order.
804
+ * The center coincidence collapse is a downstream concern of the
805
+ * surface (which registers one hit region per coincidence group)
806
+ * and the renderer (which dedup-paints overlapping circles). The
807
+ * pure layout stays four-entry so consumers can inspect per-corner
808
+ * positions.
809
+ * - `line` geometry → 1 handle labelled `corner_radius:line`.
810
+ *
811
+ * `during_gesture` flips the handle position from "floored to padded
812
+ * inset" (at rest / on commit) to "raw radius value" (during drag).
813
+ * The surface passes `true` when its gesture machine is in a
814
+ * `corner_radius` state, `false` otherwise.
815
+ */
816
+ declare function computeCornerRadiusLayout(input: CornerRadiusInput, fallback_rect: Rect | null, zoom: number, opts?: {
817
+ size?: number;
818
+ hit_size?: number;
819
+ pad?: number;
820
+ during_gesture?: boolean;
821
+ }): CornerRadiusHandleLayout[];
822
+ /**
823
+ * Group the four rect handle entries by doc-space coincidence (within
824
+ * `eps_doc`). Returns one group per distinct position, in nw/ne/se/sw
825
+ * iteration order. Each inner array is the list of anchors sharing
826
+ * that position.
827
+ *
828
+ * Geometric truth:
829
+ * - sub-max (each radius < min(w,h)/2) → 4 groups, one per corner
830
+ * - oblong max (w > h, each radius = h/2) → 2 groups:
831
+ * [['nw','sw'], ['ne','se']] (left pair, right pair)
832
+ * - oblong max (h > w, each radius = w/2) → 2 groups:
833
+ * [['nw','ne'], ['sw','se']] (top pair, bottom pair)
834
+ * - square max (each radius = w/2 = h/2) → 1 group:
835
+ * [['nw','ne','se','sw']] (all at the center)
836
+ *
837
+ * Returns `[]` for non-rect layouts (line geometry — there's no
838
+ * notion of multi-corner coincidence on a single-handle layout).
839
+ */
840
+ declare function cornerRadiusLayoutGroups(layout: readonly CornerRadiusHandleLayout[], eps_doc?: number): CornerRadiusAnchor[][];
841
+ /**
842
+ * Resolve a drag's direction to the anchor whose intercardinal
843
+ * direction `(sign_x, sign_y)` best matches the drag delta.
844
+ *
845
+ * Used when a coincidence group has more than one candidate
846
+ * (oblong max — 2 candidates; square max — 4 candidates). Picks
847
+ * AMONG `candidates` only — never invents a corner outside the
848
+ * group. Tie-break: first listed candidate wins.
849
+ *
850
+ * `dx, dy` is the drag delta (cursor moved by). The user pulls the
851
+ * handle TOWARD the corner the radius will shrink at — so the drag
852
+ * direction matches `(sign_x, sign_y)` directly (TL knob exists at
853
+ * (r, r); pulling toward TL means cursor moves in (-x, -y); but
854
+ * that's the SAME as cursor moves "back along the (sign_x, sign_y)
855
+ * direction" which means delta · sign is negative). To keep the
856
+ * intent intuitive — "pulling toward NW selects NW" — we pick the
857
+ * candidate whose `(sign_x, sign_y)` minimizes the dot product
858
+ * (i.e. delta most opposite the corner→interior vector).
859
+ *
860
+ * Equivalent (and what the code does): maximize the dot product
861
+ * with the NEGATED delta.
862
+ *
863
+ * For a rotated rect, callers pass the rect's local→doc `transform`
864
+ * along with a DOC-space drag delta; the function rotates the local
865
+ * sign vectors through `T.linear` before comparing so the resolved
866
+ * corner is the one the user actually dragged toward — not the one
867
+ * that happens to line up with world axes. With `transform`
868
+ * omitted, the local frame coincides with the doc frame and the
869
+ * rotation is a no-op (back-compatible with axis-aligned callers).
870
+ */
871
+ declare function resolveCornerDragAnchor(dx: number, dy: number, candidates: readonly CornerRadiusAnchor[], transform?: cmath.Transform): CornerRadiusAnchor;
872
+ /** @deprecated use `resolveCornerDragAnchor`. */
873
+ declare function resolveCenterDragAnchor(dx: number, dy: number): CornerRadiusAnchor;
874
+ interface DrawCornerRadiusParams {
875
+ ctx: CanvasRenderingContext2D;
876
+ transform: cmath.Transform;
877
+ /** Viewport width in CSS pixels. */
878
+ width: number;
879
+ /** Viewport height in CSS pixels. */
880
+ height: number;
881
+ /** Device pixel ratio of the canvas. */
882
+ dpr: number;
883
+ /** Pre-computed handle layout. Always one entry per corner (rect)
884
+ * or one entry (line); the painter dedups by screen-pixel so
885
+ * coincident handles paint as one circle. */
886
+ handles: readonly CornerRadiusHandleLayout[];
887
+ /** Stroke + fill color for the knob. */
888
+ color: string;
889
+ /** Fill color for the interior. Falls back to white for the standard
890
+ * hollow-ring look. */
891
+ fillColor?: string;
892
+ }
893
+ /**
894
+ * Paint corner-radius handles into the canvas context. Stateless.
895
+ *
896
+ * Coincident handles are dedup-painted at integer-pixel granularity:
897
+ * when multiple entries project to the same pixel (oblong-max pairs,
898
+ * square-max quadruple) only one circle is drawn. The deduplication
899
+ * mirrors `cornerRadiusLayoutGroups` — same geometric truth, two
900
+ * different consumers.
901
+ */
902
+ declare function drawCornerRadius(p: DrawCornerRadiusParams): void;
903
+ //#endregion
904
+ //#region primitives/parametric-handle.d.ts
905
+ type ParametricHandle = cmath.parametric.ParametricHandle;
906
+ type ParametricHandleGroup = cmath.parametric.ParametricHandleGroup;
907
+ /**
908
+ * Hud-local protocol record: one node's `ParametricHandleSet` plus
909
+ * the `node_id` the surface uses to route per-handle intents back to
910
+ * the host. The math content (handles / groups / transform) carries
911
+ * the same semantics as `cmath.parametric.ParametricHandleSet`; the
912
+ * `node_id` is the hud-side routing tag.
913
+ *
914
+ * `inset` on each handle is in **track-units** (cmath contract).
915
+ * Hud's convention for translating its 16-screen-px snap-back to
916
+ * track-units lives in the corner-radius primitive (see
917
+ * `primitives/corner-radius.ts`); other producers that want the same
918
+ * convention compute `inset = screen_px / zoom · k` for whatever
919
+ * geometric factor `k` their track demands.
920
+ *
921
+ * `groups`, if provided, MUST be disjoint — each handle id may appear
922
+ * in at most one group. Overlapping declarations would register the
923
+ * same handle in multiple hit regions and make routing ambiguous.
924
+ * The layout-grouper enforces this at runtime by first-declared-wins:
925
+ * later groups that mention an already-claimed id are dropped (with a
926
+ * dev-mode warning).
927
+ */
928
+ interface ParametricHandleInput {
929
+ node_id: NodeId;
930
+ handles: readonly ParametricHandle[];
931
+ groups?: readonly ParametricHandleGroup[];
932
+ transform?: cmath.Transform;
933
+ }
934
+ /**
935
+ * Hud's screen-px convention for the knob's resting floor from the
936
+ * curve's `t = 0` endpoint, used so a knob at value=0 doesn't collide
937
+ * with the resize-corner knob beneath it.
938
+ *
939
+ * This is hud's UX choice, NOT the unit the math layer reads.
940
+ * `ParametricHandle.inset` (in cmath) is documented in track-units;
941
+ * each producer translates this screen-px value through `zoom` (and
942
+ * any track-geometry factor) before populating the field.
943
+ */
944
+ declare const DEFAULT_PARAMETRIC_HANDLE_INSET = 16;
945
+ /** Default screen-px size of the knob. Matches resize-corner knobs. */
946
+ declare const DEFAULT_PARAMETRIC_HANDLE_SIZE = 8;
947
+ /**
948
+ * Default screen-px hit AABB size. Padded above the visual knob per
949
+ * the package's render/hit asymmetry rule.
950
+ */
951
+ declare const DEFAULT_PARAMETRIC_HIT_SIZE = 16;
952
+ /**
953
+ * One handle's render-time layout. `pos` is doc-space (after the
954
+ * input's `transform` has been applied); `track_doc` is the doc-space
955
+ * track (curve or point set) used by the gesture's projection.
956
+ * `domain` is the effective domain with defaults filled in
957
+ * (`min = 0`, `max = 1`).
958
+ */
959
+ interface ParametricHandleLayout {
960
+ /** The node id of the owning input — routes intents back. */
961
+ node_id: NodeId;
962
+ /** The handle id within the input — names the manipulated parameter. */
963
+ handle_id: string;
964
+ /** Doc-space position of the knob center. */
965
+ pos: cmath.Vector2;
966
+ /** Screen-px visual knob size. */
967
+ size: number;
968
+ /** Screen-px hit AABB size (>= size, padded). */
969
+ hit_size: number;
970
+ /** Stable label — `parametric:<node_id>:<handle_id>`. */
971
+ label: string;
972
+ /** Doc-space track for the gesture's projection — continuous curve
973
+ * OR discrete point set. */
974
+ track_doc: cmath.ui.Curve | cmath.ui.PointSet;
975
+ /** Effective domain (`min`/`max` defaulted; `step` preserved). */
976
+ domain: {
977
+ min: number;
978
+ max: number;
979
+ step?: number;
980
+ };
981
+ }
982
+ /**
983
+ * Compute the per-frame layout for every handle in an input. One
984
+ * layout entry per declared handle, in declaration order. Coincidence
985
+ * detection (the "4-corner collapse" semantic of corner-radius) is a
986
+ * separate concern — call {@link parametricHandleLayoutGroups} on the
987
+ * result.
988
+ *
989
+ * `during_gesture` lifts the snap-back floor: at rest, a handle whose
990
+ * `value` lands closer to `t = 0` than the input's `inset` (in
991
+ * track-units) is floored to `t_inset`; during a drag the knob
992
+ * follows the cursor down to `t = 0` so the gesture feels honest.
993
+ *
994
+ * Pure geometry; no zoom dependence — callers express `inset` in
995
+ * track-units, so screen-px → track-units conversion (if any) has
996
+ * already happened before this call.
997
+ */
998
+ declare function computeParametricHandleLayout(input: ParametricHandleInput, opts?: {
999
+ size?: number;
1000
+ hit_size?: number;
1001
+ during_gesture?: boolean;
1002
+ }): ParametricHandleLayout[];
1003
+ /**
1004
+ * Partition a layout into hit-region groups using the input's
1005
+ * declared coincidence groups.
1006
+ *
1007
+ * A declared group "fires" only when ALL its members are within
1008
+ * `eps_doc` of each other in doc-space — i.e. the handles have
1009
+ * geometrically collapsed onto one point. When a group fires, its
1010
+ * members are returned as one sublist and the producer registers a
1011
+ * single hit region with `candidates = members`; the gesture resolves
1012
+ * which member the pointer meant from drag direction.
1013
+ *
1014
+ * When a declared group doesn't fire (members are spread out), its
1015
+ * members are returned as singletons — one hit region per handle.
1016
+ *
1017
+ * Handles not mentioned in any declared group are always singletons.
1018
+ *
1019
+ * The function never invents groupings the input didn't declare —
1020
+ * that's an `/sdk-design` invariant ("coincidence is opt-in").
1021
+ */
1022
+ declare function parametricHandleLayoutGroups(input: ParametricHandleInput, layout: readonly ParametricHandleLayout[], eps_doc?: number): ParametricHandleLayout[][];
1023
+ /**
1024
+ * Resolve which handle in a coincident group the user is dragging.
1025
+ *
1026
+ * Each candidate has a curve whose tangent at the coincident position
1027
+ * points toward `t = 1`. The user "pulls the knob back along that
1028
+ * direction" to decrease value, so the drag delta most OPPOSITE the
1029
+ * tangent identifies the intended handle. Equivalent: maximize
1030
+ * `tangent · (-delta)`.
1031
+ *
1032
+ * Ties (rare — would require two curves with identical tangents
1033
+ * passing through the same point) break by first-listed candidate.
1034
+ */
1035
+ declare function resolveParametricHandleByDirection(group: readonly ParametricHandleLayout[], dx: number, dy: number): ParametricHandleLayout;
1036
+ /**
1037
+ * Project a doc-space point onto a handle's track and return both
1038
+ * the parameter `t` and the host-units `value` (denormalized through
1039
+ * `domain`, then snapped to `step` if set).
1040
+ *
1041
+ * The producer calls this once per pointer_move during a drag. The
1042
+ * `value` is what flows back to the host on the intent's `value`
1043
+ * field; `t` is internal (useful for tests and debug overlays).
1044
+ *
1045
+ * Dispatches on `track.kind`: continuous curves use
1046
+ * {@link cmath.ui.projectPointOnCurve}; point sets use
1047
+ * {@link cmath.ui.projectPointOnSet}, which snaps to the nearest
1048
+ * point. `step` quantization (if `domain.step > 0`) is applied on
1049
+ * top of either, then clamped to `[min, max]`.
1050
+ */
1051
+ declare function projectParametricHandleValue(layout: ParametricHandleLayout, point: cmath.Vector2): {
1052
+ t: number;
1053
+ value: number;
1054
+ };
1055
+ interface DrawParametricHandlesParams {
1056
+ ctx: CanvasRenderingContext2D;
1057
+ transform: cmath.Transform;
1058
+ /** Viewport width in CSS pixels. */
1059
+ width: number;
1060
+ /** Viewport height in CSS pixels. */
1061
+ height: number;
1062
+ /** Device pixel ratio of the canvas. */
1063
+ dpr: number;
1064
+ /** Pre-computed handle layouts. Coincident handles are dedup-painted
1065
+ * by screen pixel — overlapping knobs paint as one circle. */
1066
+ handles: readonly ParametricHandleLayout[];
1067
+ /** Stroke + fill color for the knob. */
1068
+ color: string;
1069
+ /** Fill color for the interior. Defaults to white for the standard
1070
+ * hollow-ring look (matches `drawCornerRadius`). */
1071
+ fillColor?: string;
1072
+ }
1073
+ /**
1074
+ * Paint parametric handles into the canvas context. Stateless,
1075
+ * pixel-dedup'd. Same visual shape as `drawCornerRadius` — the two
1076
+ * collapse into one painter in Phase 2 when corner-radius migrates.
1077
+ */
1078
+ declare function drawParametricHandles(p: DrawParametricHandlesParams): void;
1079
+ //#endregion
1080
+ //#region primitives/canvas.d.ts
1081
+ interface HUDCanvasOptions {
1082
+ color?: string;
1083
+ }
1084
+ /**
1085
+ * Imperative Canvas 2D renderer for the HUD overlay.
1086
+ *
1087
+ * Owns a single `<canvas>` element and draws {@link HUDDraw} command lists
1088
+ * each frame. All drawing is immediate-mode: the canvas is cleared and
1089
+ * fully redrawn on every `draw()` call.
1090
+ *
1091
+ * The viewport transform is assumed to be axis-aligned (scale + translate only,
1092
+ * no rotation/shear). The off-diagonal components of the transform matrix are
1093
+ * ignored.
1094
+ */
1095
+ declare class HUDCanvas {
1096
+ private canvas;
1097
+ private ctx;
1098
+ private dpr;
1099
+ private transform;
1100
+ private color;
1101
+ private width;
1102
+ private height;
1103
+ private pixelGrid;
1104
+ private ruler;
1105
+ private cornerRadiusHandles;
1106
+ private parametricHandles;
1107
+ constructor(canvas: HTMLCanvasElement, options?: HUDCanvasOptions);
1108
+ setColor(color?: string): void;
1109
+ setSize(w: number, h: number): void;
1110
+ setTransform(transform: cmath.Transform): void;
1111
+ /**
1112
+ * Configure the back-most pixel-grid layer. Pass `null` to disable.
1113
+ * Drawn before any HUD primitive, gated by `zoomThreshold`. See
1114
+ * `PixelGridConfig.transform` for the two-transform contract.
1115
+ */
1116
+ setPixelGrid(config: PixelGridConfig | null): void;
1117
+ /**
1118
+ * Update only the pixel grid's transform, without replacing the rest of
1119
+ * the config. Cheap to call per camera tick.
1120
+ */
1121
+ setPixelGridTransform(transform: cmath.Transform): void;
1122
+ /**
1123
+ * Configure the top-most ruler chrome (top + left strips). Pass `null`
1124
+ * to disable. Painted LAST in the frame — after the pixel grid,
1125
+ * selection chrome, marquee, handles, size meter, and host extras —
1126
+ * so the strips visually frame the viewport instead of being clipped
1127
+ * by anything drawn at the edges. See `RulerConfig.transform` for
1128
+ * the two-transform contract — same shape as the pixel grid.
1129
+ *
1130
+ * Paint-order rationale: pixel grid is a substrate (content-space,
1131
+ * the user reads it "under" the document), ruler is a frame
1132
+ * (viewport-space, the user reads it "around" the document). Frames
1133
+ * sit on top of everything they frame; substrates sit beneath. See
1134
+ * the README "Render path" section.
1135
+ */
1136
+ setRuler(config: RulerConfig | null): void;
1137
+ /**
1138
+ * Update only the ruler's transform. Cheap to call per camera tick.
1139
+ * No-op when no ruler config is set.
1140
+ */
1141
+ setRulerTransform(transform: cmath.Transform): void;
1142
+ /**
1143
+ * Configure the corner-radius handle overlay. Pass `null` to clear.
1144
+ *
1145
+ * Painted in the chrome band — ABOVE the surface's selection
1146
+ * outlines / resize knobs / host extras (which is the "handles, not
1147
+ * frame" band) and BELOW the ruler (the frame strip). The position
1148
+ * inside the chrome band — strictly above `screenRects` — matches
1149
+ * the layered-handle convention: a feature-specific knob always
1150
+ * sits over the generic resize chrome that draws under it.
1151
+ *
1152
+ * Hit-test entries are NOT registered from here; the Surface owns
1153
+ * the registry and pushes corner-radius regions alongside its
1154
+ * regular chrome regions. Render and hit live on independent
1155
+ * shapes per the package's render/hit asymmetry rule.
1156
+ */
1157
+ setCornerRadiusHandles(handles: readonly CornerRadiusHandleLayout[] | null): void;
1158
+ /**
1159
+ * Push the per-frame parametric-handle layouts. Painted in the same
1160
+ * z-band as corner-radius handles (knob, not frame) — feature-
1161
+ * specific knobs above generic resize chrome, below marquee/lasso
1162
+ * and the ruler.
1163
+ *
1164
+ * Hit-test entries are NOT registered here; the Surface owns the
1165
+ * registry and pushes parametric regions alongside its other
1166
+ * chrome regions. Render and hit live on independent shapes per the
1167
+ * package's render/hit asymmetry rule.
1168
+ */
1169
+ setParametricHandles(handles: readonly ParametricHandleLayout[] | null): void;
1170
+ /**
1171
+ * Clear the canvas and draw all primitives in `commands`.
1172
+ * Pass `undefined` to clear without drawing (e.g. when no overlay is active).
1173
+ */
1174
+ draw(commands: HUDDraw | undefined): void;
1175
+ private applyViewTransform;
1176
+ private applyScreenTransform;
1177
+ /** Project a scalar offset on `axis` to screen-space. */
1178
+ private deltaToScreen;
1179
+ private drawRules;
1180
+ /**
1181
+ * Resolve an optional `HUDPaint` to a Canvas 2D fill/stroke value,
1182
+ * falling back to the legacy `color` + `opacity` fields when absent.
1183
+ *
1184
+ * Used by the primitive renderers to switch through `HUDPaint` when
1185
+ * present; the legacy color path is preserved for callers that haven't
1186
+ * adopted paint yet. Pattern resolution happens here — including the
1187
+ * counter-CTM transform that keeps stripes screen-aligned.
1188
+ */
1189
+ private resolvePaintOrFallback;
1190
+ private drawLines;
1191
+ private drawRects;
1192
+ private drawPolylines;
1193
+ private drawPoints;
1194
+ /**
1195
+ * Draw rects whose **size is in screen-space** but whose **anchor is in
1196
+ * document-space**. The doc-space point is projected via the current
1197
+ * transform; the rect is then drawn at fixed CSS-pixel dimensions.
1198
+ *
1199
+ * This is the primitive used to draw resize / rotate handles — they must
1200
+ * remain a constant visual size regardless of viewport zoom.
1201
+ */
1202
+ private drawScreenRects;
1203
+ }
1204
+ //#endregion
1205
+ //#region primitives/paint.d.ts
1206
+ declare const DEFAULT_STRIPES_ANGLE_DEG = 45;
1207
+ declare const DEFAULT_STRIPES_SPACING_PX = 8;
1208
+ declare const DEFAULT_STRIPES_THICKNESS_PX = 1.5;
1209
+ /**
1210
+ * Result of `resolvePaint`. The caller assigns `style` to
1211
+ * `ctx.fillStyle` / `ctx.strokeStyle` and uses `opacity` as `globalAlpha`
1212
+ * for the operation (caller is responsible for save/restore).
1213
+ */
1214
+ interface ResolvedPaint {
1215
+ style: string | CanvasPattern;
1216
+ opacity: number;
1217
+ }
1218
+ /**
1219
+ * Pure geometry for a stripes tile. Computes the cross-stripe period
1220
+ * (tile height) and the per-stripe parameters in device pixels.
1221
+ * Separated from the rasterizer so the math is testable in Node.
1222
+ *
1223
+ * The tile is `1 × size`: one horizontal stripe period, axis-aligned.
1224
+ * Rotation is applied at draw time via `CanvasPattern.setTransform`,
1225
+ * NOT baked into the rasterized tile — baking rotation would force the
1226
+ * tile's axis-aligned period to `spacing / sin(angle)`, irrational for
1227
+ * the canonical 45° case, and produce visible breaks within each
1228
+ * rendered stripe (the previous behavior).
1229
+ *
1230
+ * To keep stripes screen-aligned regardless of viewport zoom, the tile
1231
+ * is built in *device pixels* and the consumer composes a counter-CTM
1232
+ * with the rotation in `setTransform`.
1233
+ */
1234
+ declare function computeStripesTileGeometry(paint: HUDPaintStripes, dpr: number): {
1235
+ /**
1236
+ * Tile height in device pixels — one cross-stripe period. The tile's
1237
+ * width is fixed at 1 (the stripe is constant along the stripe
1238
+ * direction; horizontal width doesn't carry information).
1239
+ */
1240
+ size: number;
1241
+ spacingPx: number;
1242
+ thicknessPx: number;
1243
+ angleRad: number;
1244
+ };
1245
+ /**
1246
+ * Rasterize an unrotated, axis-aligned stripes tile (device pixels).
1247
+ * The tile is `1 × size` — one horizontal stripe band wrapping the
1248
+ * `y=0` / `y=size` seam, so it tiles cleanly under `repeat`.
1249
+ *
1250
+ * Rotation is applied at draw time via the pattern transform in
1251
+ * `resolvePaint`. Baking rotation here would force the axis-aligned
1252
+ * tile dimensions to align with the rotated stripe lattice — irrational
1253
+ * for the canonical 45° case — and produce visible breaks within each
1254
+ * rendered stripe.
1255
+ */
1256
+ declare function buildStripesTile(paint: HUDPaintStripes, dpr: number): OffscreenCanvas;
1257
+ /**
1258
+ * Resolve an `HUDPaint` to a Canvas 2D paint value.
1259
+ *
1260
+ * Solid → CSS color string. Stripes → `CanvasPattern` (with the pattern
1261
+ * transform pre-applied to keep tiles aligned to device pixels).
1262
+ *
1263
+ * Throws on unknown `kind` — closed-taxonomy enforcement; HUD does not
1264
+ * silently passthrough unknown paint kinds.
1265
+ */
1266
+ declare function resolvePaint(ctx: CanvasRenderingContext2D, paint: HUDPaint, dpr: number): ResolvedPaint;
1267
+ //#endregion
1268
+ //#region primitives/draw.d.ts
1269
+ interface HUDGroupFilter {
1270
+ hidden?: Iterable<HUDSemanticGroup>;
1271
+ }
1272
+ /**
1273
+ * Filter a draw command list by semantic group.
1274
+ *
1275
+ * Ungrouped primitives are always kept. The function is intentionally shallow:
1276
+ * primitives are immutable command objects on the hot draw path, so preserving
1277
+ * object identity keeps this as a visibility pass rather than a rewrite.
1278
+ */
1279
+ declare function filterHUDDrawByGroup(draw: HUDDraw | undefined, filter: HUDGroupFilter): HUDDraw | undefined;
1280
+ //#endregion
1281
+ //#region primitives/snap-guide.d.ts
1282
+ /**
1283
+ * Convert a `guide.SnapGuide` (the output of `guide.plot()`) into a
1284
+ * generic {@link HUDDraw} command list.
1285
+ *
1286
+ * `color`, when supplied, is applied as the per-item stroke override
1287
+ * for every emitted line, rule, and point. When absent, the HUD
1288
+ * canvas's current color is used.
1289
+ */
1290
+ declare function snapGuideToHUDDraw(sg: guide.SnapGuide | undefined, color?: string): HUDDraw | undefined;
1291
+ //#endregion
1292
+ //#region primitives/measurement-guide.d.ts
1293
+ /**
1294
+ * Convert a {@link Measurement} (the output of `measure()`) into a
1295
+ * generic {@link HUDDraw} command list.
1296
+ *
1297
+ * All coordinates are in **document space** — the HUD canvas applies
1298
+ * the viewport transform.
1299
+ *
1300
+ * Produces:
1301
+ * - Two stroke-only rects for the A and B bounding boxes
1302
+ * - One labelled guide line per non-zero distance (solid)
1303
+ * - One auxiliary line per non-zero side connecting the guide to B (dashed)
1304
+ *
1305
+ * If `color` is provided, every emitted line and rect carries that color so
1306
+ * the guides render distinctly from the canvas's chrome color. When used via
1307
+ * `surface.draw(extra)` (the host-fed-extras channel) this is required to
1308
+ * separate measurement from selection chrome on a shared canvas.
1309
+ */
1310
+ declare function measurementToHUDDraw(m: Measurement, color?: string): HUDDraw;
1311
+ //#endregion
1312
+ //#region primitives/marquee.d.ts
1313
+ /**
1314
+ * Convert two marquee corner points into a {@link HUDDraw} command list.
1315
+ *
1316
+ * All coordinates are in **document space**.
1317
+ *
1318
+ * Produces a single rectangle with a stroke outline and a semi-transparent fill.
1319
+ */
1320
+ declare function marqueeToHUDDraw(a: cmath.Vector2, b: cmath.Vector2): HUDDraw;
1321
+ //#endregion
1322
+ //#region primitives/lasso.d.ts
1323
+ /**
1324
+ * Convert a lasso point sequence into a {@link HUDDraw} command list.
1325
+ *
1326
+ * All coordinates are in **document space**.
1327
+ *
1328
+ * Produces a single polyline with a dashed stroke and a semi-transparent fill.
1329
+ * The polyline lives on the {@link HUDDraw} TOP layer (`topPolylines`) so it
1330
+ * always paints above knobs, handles, outlines, and any other surface chrome
1331
+ * — the user must always see the live lasso region they are drawing,
1332
+ * regardless of what it overlaps. Standalone `<Lasso/>` overlays render on
1333
+ * their own canvas and don't need this guarantee, but routing through the
1334
+ * top slot makes hosts that inline the draw into the surface canvas behave
1335
+ * identically.
1336
+ */
1337
+ declare function lassoToHUDDraw(points: cmath.Vector2[]): HUDDraw | undefined;
1338
+ //#endregion
1339
+ //#region event/event.d.ts
1340
+ /** Modifier-key snapshot at the moment an event was produced. */
1341
+ interface Modifiers {
1342
+ shift: boolean;
1343
+ alt: boolean;
1344
+ meta: boolean;
1345
+ ctrl: boolean;
1346
+ }
1347
+ declare const NO_MODS: Modifiers;
1348
+ type PointerButton = "primary" | "secondary" | "middle";
1349
+ /**
1350
+ * Input event consumed by `Surface.dispatch`.
1351
+ *
1352
+ * All coordinates are **screen-space CSS pixels relative to the canvas**.
1353
+ * The surface owns the camera and converts to document-space internally.
1354
+ */
1355
+ type SurfaceEvent = {
1356
+ kind: "pointer_move";
1357
+ x: number;
1358
+ y: number;
1359
+ mods: Modifiers;
1360
+ } | {
1361
+ kind: "pointer_down";
1362
+ x: number;
1363
+ y: number;
1364
+ button: PointerButton;
1365
+ mods: Modifiers;
1366
+ } | {
1367
+ kind: "pointer_up";
1368
+ x: number;
1369
+ y: number;
1370
+ button: PointerButton;
1371
+ mods: Modifiers;
1372
+ } | {
1373
+ kind: "modifiers";
1374
+ mods: Modifiers;
1375
+ } | {
1376
+ kind: "wheel";
1377
+ x: number;
1378
+ y: number;
1379
+ dx: number;
1380
+ dy: number;
1381
+ mods: Modifiers;
1382
+ } | {
1383
+ kind: "key";
1384
+ phase: "down" | "up";
1385
+ code: string;
1386
+ mods: Modifiers;
1387
+ } | {
1388
+ kind: "blur";
1389
+ };
1390
+ /** Result of a `Surface.dispatch` call. */
1391
+ interface SurfaceResponse {
1392
+ needsRedraw: boolean;
1393
+ cursorChanged: boolean;
1394
+ hoverChanged: boolean;
1395
+ }
1396
+ //#endregion
1397
+ //#region classes/padding/intent.d.ts
1398
+ /**
1399
+ * Drag of a padding handle on a flex-parent container's side. The host
1400
+ * applies the new `value` to the node's `layout_padding_{side}` field.
1401
+ * When `mirror` is true (alt-held), host also applies the same value to
1402
+ * the opposite side; HUD paints both sides "selected" for the duration of
1403
+ * the gesture.
1404
+ *
1405
+ * The intent carries the resolved next padding value — not a pointer
1406
+ * delta. HUD owns the math; the host commits the outcome (D1).
1407
+ *
1408
+ * `value` is in doc-space units. The HUD clamps to 0 (no negative padding);
1409
+ * hosts wanting different clamps post-process.
1410
+ */
1411
+ type PaddingIntent = {
1412
+ kind: "padding_handle";
1413
+ node_id: NodeId;
1414
+ side: cmath.RectangleSide;
1415
+ value: number;
1416
+ mirror: boolean;
1417
+ phase: IntentPhase;
1418
+ };
1419
+ //#endregion
1420
+ //#region classes/transform-box/intent.d.ts
1421
+ /**
1422
+ * Drag of a transform-box handle. HUD has already reduced the gesture;
1423
+ * the host commits `transform` to whatever field it bound the input to
1424
+ * (image-fit paint transform, free-transform node local transform, …).
1425
+ *
1426
+ * `op` carries the gesture's TYPE and TARGET so the host can
1427
+ * route / log / debug, but never the pointer delta — the host doesn't
1428
+ * need to re-reduce. HUD's reduction is the public outcome (D1 —
1429
+ * subscribe to outcomes, not events).
1430
+ *
1431
+ * `transform` is box-relative (translation normalized [0..1] against
1432
+ * `TransformBoxInput.size`).
1433
+ */
1434
+ type TransformBoxIntent = {
1435
+ kind: "transform_box";
1436
+ id: string;
1437
+ op: {
1438
+ type: "translate";
1439
+ } | {
1440
+ type: "scale_side";
1441
+ side: cmath.RectangleSide;
1442
+ } | {
1443
+ type: "rotate";
1444
+ corner: cmath.IntercardinalDirection;
1445
+ };
1446
+ transform: AffineTransform;
1447
+ phase: IntentPhase;
1448
+ };
1449
+ //#endregion
1450
+ //#region classes/vector-path/intent.d.ts
1451
+ type VectorPathIntent = {
1452
+ /**
1453
+ * Select a single vertex within a path under content-edit. Mode
1454
+ * mirrors the node-level `select` intent. Hosts dispatch to their
1455
+ * path-edit session's sub-selection state.
1456
+ */
1457
+ kind: "select_vertex";
1458
+ node_id: NodeId;
1459
+ index: number;
1460
+ mode: SelectMode;
1461
+ } | {
1462
+ /**
1463
+ * Translate one or more vertices of a path under content-edit. The
1464
+ * delta is in document-space, measured from gesture start to the
1465
+ * current frame. Hosts apply the delta to each indexed vertex.
1466
+ */
1467
+ kind: "translate_vertices";
1468
+ node_id: NodeId;
1469
+ indices: number[];
1470
+ dx: number;
1471
+ dy: number;
1472
+ phase: IntentPhase;
1473
+ } | {
1474
+ /**
1475
+ * Translate the path-edit sub-selection. The host expands its
1476
+ * authoritative sub-selection (selected vertices ∪ endpoints of
1477
+ * selected segments) and UNIONs with `additional_vertex_indices`
1478
+ * before translating. Used by segment-body drag (default — Meta
1479
+ * switches to bend) so a drag of an unselected segment can still
1480
+ * translate its endpoints, and a drag of any item within a multi-
1481
+ * selection translates the whole selection coherently.
1482
+ */
1483
+ kind: "translate_vector_selection";
1484
+ node_id: NodeId;
1485
+ additional_vertex_indices: readonly number[];
1486
+ dx: number;
1487
+ dy: number;
1488
+ phase: IntentPhase;
1489
+ } | {
1490
+ /**
1491
+ * Clear the path-edit sub-selection (vertices / segments / tangents)
1492
+ * WITHOUT exiting content-edit mode and WITHOUT touching the
1493
+ * host's node-level selection.
1494
+ *
1495
+ * Fires when the user single-clicks empty space while in content-
1496
+ * edit. Mirrors the dblclick `exit_content_edit` pattern — same
1497
+ * "click outside to back off" gesture, one fewer step. Without this
1498
+ * the user has no mouse way to drop a vertex sub-selection short of
1499
+ * leaving content-edit entirely.
1500
+ */
1501
+ kind: "clear_vector_selection";
1502
+ } | {
1503
+ /**
1504
+ * Select a single segment within a path under content-edit. Mode
1505
+ * mirrors the node-level `select` intent. Fires when the user clicks
1506
+ * a segment OFF the ghost insertion knob — clicking the ghost itself
1507
+ * fires `split_segment` instead.
1508
+ */
1509
+ kind: "select_segment";
1510
+ node_id: NodeId;
1511
+ segment: number;
1512
+ mode: SelectMode;
1513
+ } | {
1514
+ /**
1515
+ * Select a single closed-loop "region" within a path under
1516
+ * content-edit. Fires when the user clicks the interior body of
1517
+ * a closed loop (no vertex / tangent / segment-strip control
1518
+ * intercepted the click). Mode mirrors the node-level `select`
1519
+ * intent.
1520
+ *
1521
+ * Subsequent drag (if any) promotes to `translate_vector_selection`
1522
+ * — the host applies the delta to the loop's segments and their
1523
+ * endpoint vertices, the same way segment-body drag works today.
1524
+ * No new translate intent kind.
1525
+ *
1526
+ * The host's region commit policy is its own choice: typical
1527
+ * hosts also push the loop's segment indices into
1528
+ * `VectorSubSelection.segments` (so segment chrome highlights too)
1529
+ * and the picked region's id into `VectorSubSelection.regions`
1530
+ * (so the region's `selected` paint shows). The HUD doesn't
1531
+ * presume — it only reports "the user clicked region N."
1532
+ */
1533
+ kind: "select_region";
1534
+ node_id: NodeId;
1535
+ region: number;
1536
+ mode: SelectMode;
1537
+ } | {
1538
+ /**
1539
+ * Select a single tangent within a path under content-edit. Mode
1540
+ * mirrors the node-level `select` intent.
1541
+ */
1542
+ kind: "select_tangent";
1543
+ node_id: NodeId; /** `[vertex_idx, 0]` = ta on segment whose a===v; `[v, 1]` = tb where b===v. */
1544
+ tangent: readonly [number, 0 | 1];
1545
+ mode: SelectMode;
1546
+ } | {
1547
+ /**
1548
+ * Move a single tangent control point to a new doc-space position.
1549
+ * The host applies the mirror policy (`auto` infers from current
1550
+ * smooth-join state, `none` only moves the one tangent, `angle`
1551
+ * mirrors the opposite tangent's angle while preserving its length,
1552
+ * `all` mirrors both angle and length).
1553
+ */
1554
+ kind: "set_tangent";
1555
+ node_id: NodeId;
1556
+ tangent: readonly [number, 0 | 1];
1557
+ pos: cmath.Vector2;
1558
+ mirror: "auto" | "none" | "angle" | "all";
1559
+ phase: IntentPhase;
1560
+ } | {
1561
+ /**
1562
+ * Insert a new vertex on segment `segment` at parametric position
1563
+ * `t ∈ [0,1]`. The split halves inherit the original's verb if
1564
+ * possible; arc groups are broken. Fires once per click — no
1565
+ * preview phase (split is atomic).
1566
+ */
1567
+ kind: "split_segment";
1568
+ node_id: NodeId;
1569
+ segment: number;
1570
+ t: number;
1571
+ } | {
1572
+ /**
1573
+ * Bend segment `segment` by dragging a point originally at parameter
1574
+ * `ca` toward a doc-space target `cb`. The host re-solves the
1575
+ * segment's tangents to put `B(ca) === cb`, holding the endpoints
1576
+ * fixed. `phase` lets the host bracket a history preview the same
1577
+ * way translate does.
1578
+ */
1579
+ kind: "bend_segment";
1580
+ node_id: NodeId;
1581
+ segment: number;
1582
+ ca: number;
1583
+ cb: cmath.Vector2;
1584
+ phase: IntentPhase;
1585
+ };
1586
+ //#endregion
1587
+ //#region event/intent.d.ts
1588
+ /** "preview" is emitted on every gesture move; "commit" once on release. */
1589
+ type IntentPhase = "preview" | "commit";
1590
+ /**
1591
+ * Selection mode for `select` intents.
1592
+ *
1593
+ * - `replace` — clear selection, then select the given ids
1594
+ * - `add` — union into the current selection
1595
+ * - `toggle` — flip each given id's membership
1596
+ */
1597
+ type SelectMode = "replace" | "add" | "toggle";
1598
+ /**
1599
+ * Actionable change emitted by the surface. The host commits the intent
1600
+ * (wrapping in `history.preview` for `phase: "preview"`, finalizing for
1601
+ * `phase: "commit"`).
1602
+ *
1603
+ * The surface itself never mutates the document.
1604
+ */
1605
+ type Intent = {
1606
+ kind: "select";
1607
+ ids: NodeId[];
1608
+ mode: SelectMode;
1609
+ } | {
1610
+ kind: "deselect_all";
1611
+ } | {
1612
+ kind: "translate";
1613
+ ids: NodeId[]; /** Total delta in document-space, from gesture start. */
1614
+ dx: number;
1615
+ dy: number;
1616
+ phase: IntentPhase;
1617
+ } | {
1618
+ kind: "resize"; /** Member ids of the group being resized (1 or more). */
1619
+ ids: NodeId[];
1620
+ anchor: ResizeDirection;
1621
+ /**
1622
+ * Target rect in document-space. For `transformed` selections this
1623
+ * is the AABB of the new shape — preserved so axis-aligned hosts
1624
+ * that ignore `shape` keep working unchanged.
1625
+ */
1626
+ rect: Rect;
1627
+ /**
1628
+ * Full target shape. Present whenever the gesture produced one
1629
+ * (which is always, post-Commit 2 of the affine-first plan).
1630
+ * Hosts that handle rotated/sheared selections consume this
1631
+ * directly; legacy hosts can read `rect` and ignore `shape`.
1632
+ */
1633
+ shape?: SelectionShape;
1634
+ phase: IntentPhase;
1635
+ } | {
1636
+ kind: "rotate"; /** Member ids of the group being rotated (typically 1). */
1637
+ ids: NodeId[]; /** Target angle delta in radians (relative to gesture start). */
1638
+ angle: number;
1639
+ phase: IntentPhase;
1640
+ } | {
1641
+ kind: "marquee_select"; /** Marquee rect in document-space (normalized). */
1642
+ rect: Rect;
1643
+ additive: boolean;
1644
+ phase: IntentPhase;
1645
+ } | {
1646
+ /**
1647
+ * Lasso (freeform polygon) selection. Symmetric to `marquee_select`
1648
+ * — emitted every pointer_move with `phase: "preview"` and on
1649
+ * pointer_up with `phase: "commit"`. The host runs its own hit-test;
1650
+ * for vector content-edit the predicate is
1651
+ * `cmath.polygon.pointInPolygon` against `polygon`.
1652
+ *
1653
+ * Lasso targets vertices and tangents only — segments are NOT tested
1654
+ * against the polygon. The host enforces that constraint; the HUD
1655
+ * just delivers the polygon.
1656
+ */
1657
+ kind: "lasso_select";
1658
+ /**
1659
+ * Doc-space polygon, oldest-first. Treated as closed
1660
+ * (`polygon[last] → polygon[0]` implicit).
1661
+ */
1662
+ polygon: cmath.Vector2[];
1663
+ additive: boolean;
1664
+ phase: IntentPhase;
1665
+ } | {
1666
+ kind: "set_endpoint"; /** Subject node id (line-shape selection). */
1667
+ id: NodeId; /** Which endpoint is being moved. */
1668
+ endpoint: "p1" | "p2"; /** Target position in document-space. */
1669
+ pos: cmath.Vector2;
1670
+ phase: IntentPhase;
1671
+ } | {
1672
+ kind: "enter_content_edit";
1673
+ id: NodeId;
1674
+ } | {
1675
+ /**
1676
+ * Exit content-edit mode. Fired by the HUD when the user dblclicks
1677
+ * "away" from the active edit (anywhere not on a vertex / tangent /
1678
+ * segment-strip overlay). No payload — the host knows which node
1679
+ * is under edit (it's the one it most recently pushed a vector
1680
+ * mirror for via `setVectorSelection`).
1681
+ *
1682
+ * Host policy: discard any in-flight preview, clear the vector
1683
+ * mirror (`setVectorSelection(null)`), return to whatever mode the
1684
+ * host considers "outside content-edit." The HUD doesn't presume
1685
+ * what that next mode is.
1686
+ */
1687
+ kind: "exit_content_edit";
1688
+ } | VectorPathIntent | {
1689
+ /**
1690
+ * Drag of a corner-radius handle on `rect` geometry, default
1691
+ * branch (no alt). For the per-corner case (radii NOT all
1692
+ * equal) `anchor` is the corner the user grabbed; for the
1693
+ * center-handle case (radii all equal) `anchor` is RESOLVED
1694
+ * after the drag-threshold from the pull direction
1695
+ * (`corner → center` vector best matching the negated drag
1696
+ * delta).
1697
+ *
1698
+ * The host decides whether to apply the new `value` to all
1699
+ * four radii or only to the named `anchor`. The HUD doesn't
1700
+ * presume — it tells the host which corner the gesture
1701
+ * names, and the host's interaction model (e.g. "all-equal
1702
+ * stays all-equal during a center drag") picks the policy.
1703
+ * Hosts that want unambiguous single-corner semantics gate
1704
+ * on alt and read `corner_radius_explicit` instead.
1705
+ */
1706
+ kind: "corner_radius";
1707
+ node_id: NodeId;
1708
+ anchor: "nw" | "ne" | "se" | "sw"; /** Target radius in doc-space units. */
1709
+ value: number;
1710
+ phase: IntentPhase;
1711
+ } | {
1712
+ /**
1713
+ * Same payload as `corner_radius`, but the user held alt
1714
+ * during the drag. Host MUST apply to the named `anchor`
1715
+ * only — never broadcast to other corners regardless of the
1716
+ * surrounding interaction model. Lets the user override an
1717
+ * all-equal default ("alt + drag a single corner makes it
1718
+ * different").
1719
+ *
1720
+ * Distinct kind, not a flag on `corner_radius`, so the host's
1721
+ * commit pipe doesn't have to branch on a hidden modifier
1722
+ * inside the payload — the modifier IS the intent.
1723
+ */
1724
+ kind: "corner_radius_explicit";
1725
+ node_id: NodeId;
1726
+ anchor: "nw" | "ne" | "se" | "sw";
1727
+ value: number;
1728
+ phase: IntentPhase;
1729
+ } | {
1730
+ /**
1731
+ * Drag of a corner-radius handle on `line` geometry. Lines
1732
+ * have a single `corner_radius` field on the node (not four
1733
+ * per-corner radii), so there is no anchor to resolve and no
1734
+ * alt branch — there's only one knob.
1735
+ *
1736
+ * Host applies the new `value` to the node's singular
1737
+ * `corner_radius` field.
1738
+ */
1739
+ kind: "corner_radius_uniform";
1740
+ node_id: NodeId;
1741
+ value: number;
1742
+ phase: IntentPhase;
1743
+ } | {
1744
+ /**
1745
+ * Drag of a parametric handle (the universal value-on-curve
1746
+ * affordance introduced by `surface.setParametricHandles`).
1747
+ * Hosts route to their reducer by `(node_id, handle_id)` and
1748
+ * decide policy from `modifiers` themselves — the producer
1749
+ * doesn't interpret `alt` / `shift` semantically.
1750
+ *
1751
+ * `value` is in host-units (whatever the host set on the
1752
+ * handle's `domain`), already snapped to `domain.step` if
1753
+ * configured.
1754
+ *
1755
+ * One intent kind for all consumers — corner-radius's
1756
+ * `corner_radius` / `corner_radius_explicit` / `corner_radius_uniform`
1757
+ * trio is a special case still emitted by `setCornerRadius`
1758
+ * for backward compatibility; new hosts standardize on this.
1759
+ */
1760
+ kind: "parametric_handle";
1761
+ node_id: NodeId;
1762
+ handle_id: string;
1763
+ value: number;
1764
+ modifiers: {
1765
+ alt: boolean;
1766
+ shift: boolean;
1767
+ };
1768
+ phase: IntentPhase;
1769
+ } | PaddingIntent | TransformBoxIntent | {
1770
+ kind: "cancel_gesture";
1771
+ };
1772
+ /** Callback the host implements to receive intents. */
1773
+ type IntentHandler = (intent: Intent) => void;
1774
+ //#endregion
1775
+ //#region event/transform.d.ts
1776
+ /**
1777
+ * Surface camera transform: axis-aligned (scale + translate only).
1778
+ *
1779
+ * Stored as a `cmath.Transform`:
1780
+ * `[[sx, 0, tx], [0, sy, ty]]`
1781
+ *
1782
+ * Off-diagonal components are ignored; the surface does not support rotation
1783
+ * or shear at the camera level.
1784
+ */
1785
+ type Transform = cmath.Transform;
1786
+ //#endregion
1787
+ //#region surface/style.d.ts
1788
+ /**
1789
+ * HUD style — colors, sizes, and offsets for the surface-owned chrome.
1790
+ *
1791
+ * All fields are optional; defaults follow.
1792
+ */
1793
+ interface HUDStyle {
1794
+ /** Primary chrome color (selection outline, handle border). */
1795
+ chromeColor: string;
1796
+ /** Secondary color used for hover outline (lighter than chrome). */
1797
+ hoverColor: string;
1798
+ /** Handle visual size in screen-px. */
1799
+ handleSize: number;
1800
+ /** Handle fill color. */
1801
+ handleFill: string;
1802
+ /** Handle stroke color. */
1803
+ handleStroke: string;
1804
+ /** Selection outline stroke width (in screen-px). */
1805
+ selectionOutlineWidth: number;
1806
+ /** Hover outline stroke width (in screen-px). Typically thicker than selection. */
1807
+ hoverOutlineWidth: number;
1808
+ /** Whether to render rotation handles in addition to resize handles. */
1809
+ showRotationHandles: boolean;
1810
+ /**
1811
+ * Color used for IDLE vector segment outlines. Painted as a thin overlay
1812
+ * stroke on top of each segment to indicate "you are in path edit mode
1813
+ * and these segments are interactive" — independent of the document's
1814
+ * own stroke style. Default is a neutral gray affordance stroke.
1815
+ */
1816
+ segmentIdleColor: string;
1817
+ /** Stroke width (screen-px) for IDLE segment outlines. */
1818
+ segmentIdleWidth: number;
1819
+ /**
1820
+ * Color used for HOVERED or SELECTED vector segment outlines. Same color
1821
+ * for both — hover differentiates via lower opacity (see `segmentHoverOpacity`).
1822
+ */
1823
+ segmentActiveColor: string;
1824
+ /** Stroke width (screen-px) for hovered/selected segment outlines. */
1825
+ segmentActiveWidth: number;
1826
+ /** Opacity (0..1) applied to the hovered (not selected) segment outline. */
1827
+ segmentHoverOpacity: number;
1828
+ /** Color for tangent handle lines (vertex → control point). */
1829
+ tangentLineColor: string;
1830
+ /** Stroke width (screen-px) for tangent lines when their tangent is NOT
1831
+ * selected. */
1832
+ tangentLineIdleWidth: number;
1833
+ /** Stroke width (screen-px) for tangent lines when their tangent IS
1834
+ * selected. */
1835
+ tangentLineActiveWidth: number;
1836
+ /**
1837
+ * Paint applied to a region's interior when the cursor is hovering
1838
+ * the loop body (no other vector control claimed the pixel). Hosts
1839
+ * typically use `HUDPaintStripes` here — the canonical
1840
+ * "highlighted region, not committed selection" affordance.
1841
+ *
1842
+ * @see {@link HUDPaintStripes}
1843
+ */
1844
+ vectorRegionHoverPaint: HUDPaint;
1845
+ /**
1846
+ * Paint applied to a region's interior when the loop is in the
1847
+ * host-pushed `VectorSubSelection.regions` array. Same shape vocabulary
1848
+ * as `vectorRegionHoverPaint`; conventionally a brighter / higher-
1849
+ * opacity variant so selected reads stronger than hover. Hover wins
1850
+ * over selected when both apply (existing precedence in vector chrome).
1851
+ */
1852
+ vectorRegionSelectedPaint: HUDPaint;
1853
+ /**
1854
+ * Paint applied to a padding-overlay side rect when the cursor is
1855
+ * hovering it (HUD-owned; reads modifier state for axis-mirror).
1856
+ *
1857
+ * @see {@link HUDPaintStripes}
1858
+ */
1859
+ paddingHoverPaint: HUDPaint;
1860
+ /**
1861
+ * Stroke color for the padding-overlay side rect when a `padding_handle`
1862
+ * gesture is in flight (HUD-derived from `state.gesture`). Painted as an
1863
+ * outline of the side rect — communicating "this is the padding box at
1864
+ * the current value" — instead of the hover stripe pattern. Cleaner read
1865
+ * during the drag than stripes, which clutter the geometry.
1866
+ *
1867
+ * Stroke width: `selectionOutlineWidth`. Fill: none.
1868
+ *
1869
+ * With axis-mirror on (alt-held), the opposite side also outlines.
1870
+ */
1871
+ paddingSelectedStroke: string;
1872
+ /** Fill color for the padding mid-edge drag handle (visual knob). */
1873
+ paddingHandleFill: string;
1874
+ /** Stroke color (ring) for the padding mid-edge drag handle. */
1875
+ paddingHandleStroke: string;
1876
+ /**
1877
+ * Stroke color for the transform-box quad outline. The quad is the
1878
+ * only visible chrome of the transform-box model in idle — handles
1879
+ * are invisible hit-only by default.
1880
+ *
1881
+ * Default `#a3a3a3` (neutral-400) — a muted stroke that reads as
1882
+ * "transform target boundary" without competing with selection chrome.
1883
+ */
1884
+ transformBoxStroke: string;
1885
+ /** Fill color for the (invisible by default) transform-box handles. */
1886
+ transformBoxHandleFill: string;
1887
+ /** Stroke color for transform-box handles (visible only in debug). */
1888
+ transformBoxHandleStroke: string;
1889
+ }
1890
+ //#endregion
1891
+ //#region classes/padding/input.d.ts
1892
+ /**
1893
+ * Input pushed by the host to enable padding chrome on a flex-parent
1894
+ * container. Schema-level feature flag — pass `null` (or never call
1895
+ * `setPaddingOverlay`) to disable. Hosts re-push on every padding /
1896
+ * container-rect change.
1897
+ *
1898
+ * @unstable Public shape may change until a second independent integration
1899
+ * ratifies it.
1900
+ */
1901
+ interface PaddingOverlayInput {
1902
+ node_id: NodeId;
1903
+ /** Container rect in doc-space (the node's bounding rect). */
1904
+ rect: Rect;
1905
+ /**
1906
+ * Per-side padding in doc-space units. Absent or `<= 0` = side not
1907
+ * drawn. Matches the 4-value model used by `layout_padding_{side}`.
1908
+ */
1909
+ padding: {
1910
+ top?: number;
1911
+ right?: number;
1912
+ bottom?: number;
1913
+ left?: number;
1914
+ };
1915
+ /**
1916
+ * Optional semantic group for visibility policy. The full overlay
1917
+ * fans out per-side rects + per-side handles; all carry the same
1918
+ * group so a single `SurfaceVisibilityPolicy` toggle suppresses the
1919
+ * whole model atomically.
1920
+ */
1921
+ group?: HUDSemanticGroup;
1922
+ }
1923
+ /**
1924
+ * Hover state for the padding model — HUD-owned. The padding region's
1925
+ * hover is set on every idle `pointer_move` from the hit-regions
1926
+ * registry; the chrome reads it each frame to render the stripe fill.
1927
+ *
1928
+ * `mirror_side` is non-null when alt is held AND the pointer is on a
1929
+ * `padding_region` — the renderer paints both the hovered side and its
1930
+ * opposite. Cleared on alt release on the next pointer-move (modifier
1931
+ * events alone don't re-derive hover; that's an acceptable v1 cost —
1932
+ * the user will be moving the mouse over the region to see the effect
1933
+ * anyway).
1934
+ */
1935
+ type PaddingHover = {
1936
+ kind: "padding_region";
1937
+ node_id: NodeId;
1938
+ side: cmath.RectangleSide;
1939
+ mirror_side?: cmath.RectangleSide;
1940
+ } | {
1941
+ kind: "padding_handle";
1942
+ node_id: NodeId;
1943
+ side: cmath.RectangleSide;
1944
+ };
1945
+ //#endregion
1946
+ //#region classes/transform-box/input.d.ts
1947
+ /**
1948
+ * Input pushed by the host to enable transform-box chrome.
1949
+ * Schema-level feature flag — pass `null` (or never call
1950
+ * `setTransformBox`) to disable.
1951
+ *
1952
+ * @unstable Public shape may change as the transform-box model
1953
+ * stabilises. The contract is shaped by a single integration today;
1954
+ * a second independent consumer will ratify it.
1955
+ */
1956
+ interface TransformBoxInput {
1957
+ /**
1958
+ * Host-defined identity, echoed back on every intent so the host
1959
+ * can correlate (e.g. `"node-1:fill[2]"` for an image-paint
1960
+ * transform, `"node-1:local"` for a free-transform). NOT a NodeId
1961
+ * — the granularity is "one transform" per setter call.
1962
+ */
1963
+ id: string;
1964
+ /**
1965
+ * Box-relative affine — same shape as AffineTransform (2×3).
1966
+ * Translation components ([0][2], [1][2]) are normalized [0..1]
1967
+ * against `size`.
1968
+ */
1969
+ transform: AffineTransform;
1970
+ /** Box size in doc-space units. */
1971
+ size: cmath.Vector2;
1972
+ /**
1973
+ * Doc-space anchor for the box's [0,0]. Combined with `rotation`
1974
+ * to project box-local → doc-space. The box's local frame is
1975
+ * rotated by `+rotation` around `origin` to land in doc-space.
1976
+ */
1977
+ origin: cmath.Vector2;
1978
+ /**
1979
+ * Container rotation in degrees (CCW). When non-zero, the
1980
+ * gesture's doc-space cursor delta is de-rotated by `-rotation`
1981
+ * before being fed to the math reducer.
1982
+ */
1983
+ rotation?: number;
1984
+ /**
1985
+ * Optional semantic group for visibility policy. All emitted
1986
+ * overlays carry the same group so a single `SurfaceVisibilityPolicy`
1987
+ * toggle suppresses the whole model atomically.
1988
+ */
1989
+ group?: HUDSemanticGroup;
1990
+ }
1991
+ /**
1992
+ * Hover state for the transform-box model — HUD-owned. The chrome
1993
+ * reads this each frame; in the current model nothing differs
1994
+ * visually per hover (handles are transparent by default), but the
1995
+ * cursor mapping reads it (`grab` for corner, `ns/ew-resize` for
1996
+ * sides, `move` for body). Cleared on pointer-out / blur.
1997
+ */
1998
+ type TransformBoxHover = {
1999
+ kind: "transform_box_body";
2000
+ id: string;
2001
+ } | {
2002
+ kind: "transform_box_side";
2003
+ id: string;
2004
+ side: cmath.RectangleSide;
2005
+ } | {
2006
+ kind: "transform_box_corner";
2007
+ id: string;
2008
+ corner: cmath.IntercardinalDirection;
2009
+ };
2010
+ /**
2011
+ * Active gesture descriptor that the host's chrome rebuilder uses to
2012
+ * suppress handles during a drag (mirrors padding-overlay). The
2013
+ * Surface derives this from `state.gesture` when the gesture matches
2014
+ * this input's `id`.
2015
+ */
2016
+ type TransformBoxActiveOp = {
2017
+ type: "translate";
2018
+ } | {
2019
+ type: "scale_side";
2020
+ side: cmath.RectangleSide;
2021
+ } | {
2022
+ type: "rotate";
2023
+ corner: cmath.IntercardinalDirection;
2024
+ };
2025
+ //#endregion
2026
+ //#region event/hit-regions.d.ts
2027
+ /**
2028
+ * Action a UI hit-region triggers when clicked.
2029
+ *
2030
+ * Each variant carries a snapshot of the relevant shape state at the time
2031
+ * the chrome was built — so the surface can start a gesture without an
2032
+ * extra round-trip to host providers. The chrome builder is the single
2033
+ * source of truth for "what does this hit region act on?"
2034
+ *
2035
+ * - `select_node` — user clicked on a node-representative UI region.
2036
+ * - `resize_handle` — one of 8 resize regions (4 corner knobs + 4 virtual
2037
+ * edges). Carries the group's member ids and the group's initial
2038
+ * `SelectionShape` — `rect` for axis-aligned groups, `transformed` for
2039
+ * rotated/sheared groups (so resize math runs in the local frame).
2040
+ * - `rotate_handle` — one of 4 virtual rotation regions outside the group's
2041
+ * corners. Carries the group's initial `SelectionShape` for center math
2042
+ * (pivot = center of `shapeBounds(initial_shape)`).
2043
+ * - `endpoint_handle` — endpoint of a line-shape selection. Carries the
2044
+ * line's current p1/p2 so dragging is relative to a stable snapshot.
2045
+ * - `translate_handle` — body region covering a selection group's bbox.
2046
+ * Pushed under the corner / edge / rotation regions so resize wins on
2047
+ * overlap. Lets the user grab any part of the selection chrome — including
2048
+ * transparent corners of a circle's bbox — to start a translate. Hud is
2049
+ * the event source once present.
2050
+ */
2051
+ type OverlayAction = {
2052
+ kind: "select_node";
2053
+ id: NodeId;
2054
+ } | {
2055
+ kind: "resize_handle";
2056
+ direction: ResizeDirection;
2057
+ ids: readonly NodeId[];
2058
+ initial_shape: SelectionShape;
2059
+ } | {
2060
+ kind: "rotate_handle";
2061
+ corner: RotationCorner;
2062
+ ids: readonly NodeId[];
2063
+ initial_shape: SelectionShape;
2064
+ } | {
2065
+ kind: "endpoint_handle";
2066
+ endpoint: "p1" | "p2";
2067
+ id: NodeId; /** Snapshot of the line endpoints in doc-space at chrome build time. */
2068
+ p1: [number, number];
2069
+ p2: [number, number];
2070
+ } | {
2071
+ kind: "translate_handle";
2072
+ ids: readonly NodeId[];
2073
+ } | {
2074
+ /**
2075
+ * A vertex knob on a path being content-edited. Drag → translate the
2076
+ * vertex (and any other vertices co-selected at down-time) in the
2077
+ * path's local frame; click → replace-select the vertex.
2078
+ */
2079
+ kind: "vertex_handle"; /** Path node id under content-edit. */
2080
+ node_id: NodeId; /** Index of the vertex within the path's vector network. */
2081
+ index: number; /** Doc-space position of the vertex at chrome build time. */
2082
+ pos: [number, number];
2083
+ } | {
2084
+ /**
2085
+ * A tangent control-point knob on a path under content-edit. Drag →
2086
+ * translate the tangent (the host applies mirror policy); click →
2087
+ * replace-select the tangent.
2088
+ */
2089
+ kind: "tangent_handle";
2090
+ node_id: NodeId; /** `[vertex_idx, 0]` for ta on segment with a===v; `[v, 1]` for tb where b===v. */
2091
+ tangent: readonly [number, 0 | 1]; /** Doc-space position of the tangent control point at chrome build time. */
2092
+ pos: [number, number];
2093
+ } | {
2094
+ /**
2095
+ * A path segment under content-edit, claimed by the chrome's single
2096
+ * per-segment region. Click → split the segment at the **projected**
2097
+ * t (cursor → nearest point on curve). Drag → start a bend gesture
2098
+ * with `ca` frozen to that same projected t.
2099
+ *
2100
+ * Single-candidate insertion model: at any cursor position there is
2101
+ * EXACTLY ONE candidate insertion point per segment — the point on
2102
+ * the cubic closest to the cursor. The action carries the segment's
2103
+ * four doc-space control points; `event/state.ts` computes `t` on
2104
+ * demand via `cmath.bezier.project` at the cursor's current doc-space
2105
+ * position. No pre-sampled grid.
2106
+ */
2107
+ kind: "segment_strip";
2108
+ node_id: NodeId;
2109
+ segment: number;
2110
+ /** Vertex index of endpoint `a` (used by the segment-drag → translate
2111
+ * path so the host can route the gesture to a vertex translation
2112
+ * without consulting its own segment table). */
2113
+ a_idx: number; /** Vertex index of endpoint `b`. */
2114
+ b_idx: number;
2115
+ /** Doc-space cubic control points. The consumer projects the cursor
2116
+ * against these on demand to get the live `t`. */
2117
+ a: readonly [number, number];
2118
+ b: readonly [number, number];
2119
+ a_control: readonly [number, number];
2120
+ b_control: readonly [number, number];
2121
+ } | {
2122
+ /**
2123
+ * Ghost insertion knob — the visible half-point preview on a hovered
2124
+ * segment. Sits in the priority ladder ABOVE `segment_strip` (so it
2125
+ * wins clicks at the marker) and BELOW `vertex_handle` (so a real
2126
+ * vertex collapsed onto the ghost still wins). Carries the segment's
2127
+ * cubic control points so the consumer can recompute `t` live —
2128
+ * mirrors `segment_strip`, but with explicitly "this is the half-
2129
+ * point control" semantics. Pointer-down dispatches to `split_segment`;
2130
+ * drag promotes to `bend_segment` with `ca` = the live `t`.
2131
+ */
2132
+ kind: "ghost_handle";
2133
+ node_id: NodeId;
2134
+ segment: number; /** Vertex indices of the segment's endpoints, mirrors `segment_strip`. */
2135
+ a_idx: number;
2136
+ b_idx: number;
2137
+ a: readonly [number, number];
2138
+ b: readonly [number, number];
2139
+ a_control: readonly [number, number];
2140
+ b_control: readonly [number, number];
2141
+ } | {
2142
+ /**
2143
+ * Corner-radius handle — the built-in chrome promoted from labs to
2144
+ * `surface.setCornerRadius(input)`. One action variant covers all
2145
+ * three intent flavors the gesture emits (`corner_radius`,
2146
+ * `corner_radius_explicit`, `corner_radius_uniform`); the surface
2147
+ * branches on `geometry` + modifiers to pick the intent kind at
2148
+ * preview/commit time.
2149
+ *
2150
+ * - `geometry: "rect"`, `anchor: "nw"|"ne"|"se"|"sw"` — one of four
2151
+ * per-corner knobs. Carries the corner being acted on directly;
2152
+ * alt-held drag emits `corner_radius_explicit`, otherwise
2153
+ * `corner_radius`.
2154
+ * - `geometry: "rect"`, `anchor: null` — the singleton center
2155
+ * handle on an all-equal-radii rect. The surface resolves the
2156
+ * pulled-toward corner after the drag-threshold and emits
2157
+ * `corner_radius` / `corner_radius_explicit` from there. Alt
2158
+ * semantics still apply (after the resolution).
2159
+ * - `geometry: "line"`, `anchor: null` — single uniform handle on
2160
+ * the line's `a → b` axis. Emits `corner_radius_uniform`. Alt
2161
+ * has no effect (no anchor to pin).
2162
+ *
2163
+ * The action stores the original `pos` (doc-space anchor of the
2164
+ * handle at chrome build time). The gesture computes the new
2165
+ * radius from the cursor's projection onto the corresponding
2166
+ * geometry axis each frame.
2167
+ */
2168
+ kind: "corner_radius_handle";
2169
+ node_id: NodeId;
2170
+ geometry: "rect" | "line"; /** Doc-space position of the handle at chrome build time. */
2171
+ pos: readonly [number, number];
2172
+ /**
2173
+ * RECT geometry only — the rect in LOCAL space (axis-aligned).
2174
+ * The gesture re-derives per-anchor corner positions from
2175
+ * this on every projection. When the input carries a
2176
+ * `transform`, this rect is the local AABB and `transform`
2177
+ * maps it to doc space; otherwise the rect IS in doc space.
2178
+ */
2179
+ rect?: {
2180
+ x: number;
2181
+ y: number;
2182
+ width: number;
2183
+ height: number;
2184
+ };
2185
+ /**
2186
+ * RECT geometry only — optional local → doc affine transform.
2187
+ * Present when the host's selection is a rotated / sheared
2188
+ * rect; absent when axis-aligned. The state machine applies
2189
+ * this when projecting the cursor onto each anchor's
2190
+ * diagonal so a knob on a rotated rect tracks the rotated
2191
+ * axis, not the doc-space one.
2192
+ */
2193
+ transform?: cmath.Transform;
2194
+ /**
2195
+ * RECT geometry only — the corner anchors this hit region
2196
+ * stands in for. Length 1 for a single-corner knob (sub-max
2197
+ * radii); length 2 for an oblong-max pair (TL/BL or TR/BR);
2198
+ * length 4 for square-max (all four collapsed to one). When
2199
+ * the length is > 1, the state machine resolves the user's
2200
+ * intended anchor from drag direction after a small
2201
+ * threshold, picking AMONG these candidates only.
2202
+ */
2203
+ candidates?: readonly ("nw" | "ne" | "se" | "sw")[];
2204
+ /**
2205
+ * LINE geometry only — the line endpoints. `a` is the radius-
2206
+ * zero end; `b` is saturation. The gesture projects the cursor
2207
+ * onto a → b.
2208
+ */
2209
+ a?: readonly [number, number];
2210
+ b?: readonly [number, number];
2211
+ } | {
2212
+ /**
2213
+ * Closed-loop "region" body within a path under content-edit. The
2214
+ * user can click the interior of a closed sub-path to select that
2215
+ * region. Drag from the region body promotes to
2216
+ * `translate_vector_selection` over the loop's segments.
2217
+ *
2218
+ * Hit detection is **polygon-in-screen-space**: the chrome
2219
+ * builder rasterises the cubic loop to N samples per segment,
2220
+ * registers the screen-space AABB of the rasterised polygon,
2221
+ * and pairs it with a `customHitTest` closure that runs
2222
+ * `pointInPolygon`. Mirrors the segment-strip's AABB-plus-
2223
+ * refinement model — see `surface/vector-chrome.ts`.
2224
+ *
2225
+ * Hit-priority (`REGION_PRIORITY = 9` in vector-chrome.ts):
2226
+ * sits just below `SEGMENT_STRIP_PRIORITY` (8), so any vertex /
2227
+ * tangent / ghost / segment-strip control within the loop wins.
2228
+ * The region claims only the "empty body" of the loop.
2229
+ */
2230
+ kind: "region";
2231
+ node_id: NodeId; /** Region index within `VectorOverlay.regions`. */
2232
+ region: number;
2233
+ /** Segment indices forming the closed loop (carried so the host
2234
+ * can route a region select intent to its segment-aware
2235
+ * selection state without a separate lookup). */
2236
+ segments: readonly number[];
2237
+ /** Union of the loop's endpoint vertex indices. Used by drag
2238
+ * promotion to seed `translate_vector_selection.additional_vertex_indices`
2239
+ * so the gesture can translate the whole loop even before the
2240
+ * host has echoed the select_region back into the sub-selection
2241
+ * mirror. */
2242
+ vertices: readonly number[];
2243
+ } | {
2244
+ /**
2245
+ * Body of a padding-overlay side on a flex-parent container. The
2246
+ * user can hover the inset rect to see the diagonal-stripe affordance
2247
+ * (HUD-owned hover paint). Click without drag is a no-op; drag is
2248
+ * claimed by the paired `padding_handle` action at higher priority.
2249
+ *
2250
+ * The region itself emits no intent; only the handle does.
2251
+ */
2252
+ kind: "padding_region";
2253
+ node_id: NodeId;
2254
+ side: cmath.RectangleSide;
2255
+ } | {
2256
+ /**
2257
+ * Mid-edge drag knob on a padding-overlay side. Pointer-down opens
2258
+ * a `padding_handle` gesture; drag emits `padding_handle` intents
2259
+ * with the current `value` (clamped at 0) and `mirror` flag (alt
2260
+ * latched per frame). Click-no-drag is a no-op (no commit).
2261
+ *
2262
+ * Carries the container `rect` and the dragged `side` so the gesture
2263
+ * can project the cursor onto the axis and derive `value` without
2264
+ * an extra round-trip through the chrome builder.
2265
+ */
2266
+ kind: "padding_handle";
2267
+ node_id: NodeId;
2268
+ side: cmath.RectangleSide; /** Container rect at chrome build time (doc-space). */
2269
+ rect: Rect; /** Initial padding value at chrome build time (doc-space units). */
2270
+ initial_value: number;
2271
+ } | {
2272
+ /**
2273
+ * Body interior of a transform-box. Click + drag emits the
2274
+ * `translate` op for the bound transform; pointer-down without
2275
+ * drag is a no-op (no commit).
2276
+ *
2277
+ * `id` is host-defined and echoes back on the intent — see
2278
+ * `TransformBoxInput.id`.
2279
+ */
2280
+ kind: "transform_box_body";
2281
+ id: string;
2282
+ } | {
2283
+ /**
2284
+ * Side mid-edge of a transform-box. Drag emits the `scale_side`
2285
+ * op. Hit AABB is 12px-thick × side-length, Fitts'-
2286
+ * reach over the visible 1px stroke (D3 — asymmetric outputs).
2287
+ *
2288
+ * `base_angle` is the box's effective screen-space rotation in
2289
+ * RADIANS (CCW positive), i.e. `container.rotation +
2290
+ * decompose(transform).rotation`. The cursor branch in
2291
+ * `decideIdleCursor` passes it through to the `resize` cursor so
2292
+ * the arrow tilts with the visual axis instead of staying
2293
+ * axis-aligned. Mirrors `resize_handle.initial_shape` → cursor
2294
+ * baseAngle.
2295
+ */
2296
+ kind: "transform_box_side";
2297
+ id: string;
2298
+ side: cmath.RectangleSide;
2299
+ base_angle: number;
2300
+ } | {
2301
+ /**
2302
+ * Corner knob of a transform-box. Drag emits the `rotate` op.
2303
+ * Hit AABB is 16×16 screen-px, Fitts'-reach over the invisible
2304
+ * corner point. `base_angle` — see `transform_box_side`.
2305
+ */
2306
+ kind: "transform_box_corner";
2307
+ id: string;
2308
+ corner: cmath.IntercardinalDirection;
2309
+ base_angle: number;
2310
+ } | {
2311
+ /**
2312
+ * Parametric handle knob — the user-facing element of a
2313
+ * `surface.setParametricHandles(...)` input. ONE region per
2314
+ * coincidence group (per `parametricHandleLayoutGroups`); when
2315
+ * the group has 2+ members the state machine resolves which
2316
+ * handle the pointer meant from drag direction.
2317
+ *
2318
+ * Carries everything the gesture needs to project the cursor
2319
+ * each frame without consulting the input table again:
2320
+ *
2321
+ * - `pos` — doc-space anchor of the knob at chrome build time.
2322
+ * Used as the screen-space hit anchor too (the gesture
2323
+ * computes a fresh `value` from `point_doc → track`).
2324
+ * - `candidates` — the layout entries this region stands in
2325
+ * for. Single-handle regions have one entry; coincident
2326
+ * regions have N. Each entry carries its handle id, doc-space
2327
+ * track (curve OR point set), and effective domain so the
2328
+ * gesture doesn't need to re-derive them.
2329
+ */
2330
+ kind: "parametric_knob";
2331
+ node_id: NodeId;
2332
+ pos: readonly [number, number];
2333
+ candidates: readonly {
2334
+ handle_id: string;
2335
+ track_doc: cmath.ui.Curve | cmath.ui.PointSet;
2336
+ domain: {
2337
+ min: number;
2338
+ max: number;
2339
+ step?: number;
2340
+ };
2341
+ }[];
2342
+ };
2343
+ //#endregion
2344
+ //#region event/state.d.ts
2345
+ /**
2346
+ * Vector edit sub-selection — what vertices / segments / tangents of a path
2347
+ * are selected during content-edit. Host pushes this via
2348
+ * `Surface.setVectorSelection(...)` whenever its authoritative state changes;
2349
+ * the chrome reads it each frame.
2350
+ */
2351
+ interface VectorSubSelection {
2352
+ /** Path node id under content-edit. */
2353
+ node_id: NodeId;
2354
+ vertices: readonly number[];
2355
+ segments: readonly number[];
2356
+ /** `[vertex_idx, 0]` for ta on segment whose a === vertex_idx;
2357
+ * `[vertex_idx, 1]` for tb where b === vertex_idx. */
2358
+ tangents: readonly (readonly [number, 0 | 1])[];
2359
+ /**
2360
+ * Selected region indices (= closed-loop ids in
2361
+ * `VectorOverlay.regions`). Optional for backward compat — hosts that
2362
+ * don't enumerate regions (or don't push a region mirror) leave it
2363
+ * undefined; the chrome treats `undefined` and `[]` identically.
2364
+ *
2365
+ * Drives the region's `selected` visual state. The chrome reads it
2366
+ * each frame.
2367
+ */
2368
+ regions?: readonly number[];
2369
+ }
2370
+ /**
2371
+ * Which vector chrome control (if any) is currently under the pointer.
2372
+ * Derived from `hit_regions.hitTest(...)` on every idle pointer_move; the
2373
+ * vector chrome reads it to render hover affordances (segment highlight,
2374
+ * vertex/tangent stroke variant).
2375
+ *
2376
+ * Owned by the HUD — mirrors the "HUD owns hover, host owns selection"
2377
+ * boundary in the README.
2378
+ */
2379
+ type VectorHover = {
2380
+ kind: "vertex";
2381
+ node_id: NodeId;
2382
+ index: number;
2383
+ } | {
2384
+ kind: "tangent";
2385
+ node_id: NodeId;
2386
+ tangent: readonly [number, 0 | 1];
2387
+ }
2388
+ /**
2389
+ * Cursor is over the segment body, OFF the ghost insertion knob. Drives
2390
+ * the segment's hovered outline + emits the ghost in DEFAULT (idle)
2391
+ * render state at evaluate(t) so the user can see "the insertion point
2392
+ * lives HERE; reach for it."
2393
+ */
2394
+ | {
2395
+ kind: "segment";
2396
+ node_id: NodeId;
2397
+ segment: number;
2398
+ t: number;
2399
+ }
2400
+ /**
2401
+ * Cursor is over the ghost insertion knob itself (a higher-priority hit
2402
+ * region that sits on top of the segment-strip — see chrome). Drives
2403
+ * the ghost render in HOVER state and gates the split-on-click: only
2404
+ * this hover variant deferred-clicks to `split_segment`.
2405
+ */
2406
+ | {
2407
+ kind: "ghost";
2408
+ node_id: NodeId;
2409
+ segment: number;
2410
+ t: number;
2411
+ }
2412
+ /**
2413
+ * Cursor is inside a closed-loop "region" (a body hit, not a control
2414
+ * knob). Drives the region's hover paint (diagonal stripes). Loses
2415
+ * priority to vertex / tangent / ghost / segment-strip controls
2416
+ * within the same loop's bbox — those win on overlap so the user can
2417
+ * always reach a specific control without the region claiming the
2418
+ * pixel.
2419
+ */
2420
+ | {
2421
+ kind: "region";
2422
+ node_id: NodeId;
2423
+ region: number;
2424
+ };
2425
+ //#endregion
2426
+ //#region event/overlay.d.ts
2427
+ /**
2428
+ * Minimum hit-target size in screen-px.
2429
+ *
2430
+ * Visual knobs are typically 8px, but the hit region is 16px so users don't
2431
+ * need pixel-perfect aim. Matches `MIN_HIT_SIZE` in the Rust overlay.
2432
+ */
2433
+ declare const MIN_HIT_SIZE = 16;
2434
+ /**
2435
+ * Below this selection size (in screen-px on either axis), chrome is
2436
+ * suppressed — both the visual handles AND the hit regions. Matches
2437
+ * `MIN_HANDLES_VISIBLE_SIZE` in the Rust overlay.
2438
+ */
2439
+ declare const MIN_CHROME_VISIBLE_SIZE = 12;
2440
+ /**
2441
+ * A hit region for one overlay element. Always screen-space; either anchored
2442
+ * to a doc-space point (so the hit region tracks the document under camera
2443
+ * changes) or expressed as a pre-projected screen-space AABB (for things
2444
+ * like edge regions whose layout is fundamentally screen-space).
2445
+ */
2446
+ type HitShape =
2447
+ /**
2448
+ * Fixed screen-space rectangle, centered (or otherwise anchored) on a
2449
+ * doc-space point. The surface projects `anchor_doc` through the current
2450
+ * transform each frame; rect dimensions stay constant in CSS px.
2451
+ */
2452
+ {
2453
+ kind: "screen_rect_at_doc";
2454
+ anchor_doc: cmath.Vector2; /** Screen-space size in CSS px. */
2455
+ width: number;
2456
+ height: number; /** Which point of the rect sits on the anchor. Default: "center". */
2457
+ placement?: "center" | "tl" | "tr" | "bl" | "br";
2458
+ }
2459
+ /**
2460
+ * Screen-space AABB at pre-projected screen coordinates. Used for elements
2461
+ * whose layout the chrome builder already projected (e.g. edge strips).
2462
+ */
2463
+ | {
2464
+ kind: "screen_aabb";
2465
+ rect: Rect;
2466
+ }
2467
+ /**
2468
+ * Oriented bounding-box hit shape — an axis-aligned `rect` in a *shadow*
2469
+ * coordinate space, plus the affine that maps a screen-space pointer
2470
+ * INTO that space. The hit-test pipeline applies `inverse_transform` to
2471
+ * the pointer and then tests against `rect` with normal AABB containment.
2472
+ *
2473
+ * Used by transformed-chrome zones: the 9-slice runs in an axis-aligned
2474
+ * shadow rect centered at the chrome's screen center, and a rotation
2475
+ * (typically around the same center) maps shadow → screen. Storing the
2476
+ * inverse here lets hit-test stay exact at any rotation/skew without
2477
+ * inflating to an AABB-of-rotated-corners.
2478
+ */
2479
+ | {
2480
+ kind: "screen_obb";
2481
+ rect: Rect; /** screen → shadow. Applied to the pointer before AABB containment. */
2482
+ inverse_transform: cmath.Transform;
2483
+ };
2484
+ /**
2485
+ * Visual representation for one overlay element. Maps directly to the
2486
+ * primitive layer's `HUDDraw` entries — the surface fans these out into the
2487
+ * one merged `HUDDraw` it hands to `HUDCanvas.draw()` each frame.
2488
+ *
2489
+ * Virtual elements (rotation, side resize) omit `render`.
2490
+ */
2491
+ type RenderShape = /** Screen-space sized rect at doc anchor — e.g. resize knob. */{
2492
+ kind: "screen_rect";
2493
+ anchor_doc: cmath.Vector2;
2494
+ width: number;
2495
+ height: number;
2496
+ placement?: "center" | "tl" | "tr" | "bl" | "br";
2497
+ fill?: boolean;
2498
+ stroke?: boolean;
2499
+ fillColor?: string;
2500
+ strokeColor?: string;
2501
+ /**
2502
+ * Rotation around the rect's screen-space center, in radians (CCW).
2503
+ * Defaults to 0. Used for knobs/badges rendered for a transformed
2504
+ * selection so they rotate with the parent.
2505
+ */
2506
+ angle?: number;
2507
+ /**
2508
+ * Render shape — `"rect"` (default) or `"circle"` (ellipse inscribed
2509
+ * in the same bbox). Hit shape is unaffected; this only changes what
2510
+ * the canvas paints. Used by vector chrome for round vertex knobs.
2511
+ */
2512
+ shape?: "rect" | "circle";
2513
+ } /** Doc-space rect — e.g. selection outline, marquee. */ | {
2514
+ kind: "doc_rect";
2515
+ x: number;
2516
+ y: number;
2517
+ width: number;
2518
+ height: number;
2519
+ stroke?: boolean;
2520
+ fill?: boolean;
2521
+ fillOpacity?: number;
2522
+ dashed?: boolean;
2523
+ } /** Doc-space line — e.g. line-shape selection outline. */ | {
2524
+ kind: "doc_line";
2525
+ x1: number;
2526
+ y1: number;
2527
+ x2: number;
2528
+ y2: number;
2529
+ dashed?: boolean; /** Stroke width in screen-space CSS px. */
2530
+ strokeWidth?: number; /** Override the canvas color for this line. */
2531
+ color?: string;
2532
+ }
2533
+ /** Doc-space polyline — used to draw an open curve as a sequence of
2534
+ * flattened samples, or a closed polygon when `points[last] === points[0]`.
2535
+ * Stroke width is in screen-px (the renderer divides by zoom). Used by
2536
+ * vector chrome to outline each segment of a path under content-edit,
2537
+ * and to fill closed-loop regions with `HUDPaint` (stripes / solid). */
2538
+ | {
2539
+ kind: "doc_polyline";
2540
+ points: ReadonlyArray<readonly [number, number]>;
2541
+ stroke?: boolean;
2542
+ fill?: boolean;
2543
+ fillOpacity?: number;
2544
+ strokeOpacity?: number;
2545
+ strokeWidth?: number;
2546
+ dashed?: boolean;
2547
+ color?: string;
2548
+ /**
2549
+ * Paint applied to the fill. When set, takes precedence over
2550
+ * `color` + `fillOpacity` for the fill. Used by closed-loop
2551
+ * region overlays to apply `HUDPaintStripes` for hover / selected
2552
+ * affordance.
2553
+ *
2554
+ * @unstable
2555
+ */
2556
+ fillPaint?: HUDPaint;
2557
+ };
2558
+ /**
2559
+ * One interactable element of overlay UI.
2560
+ *
2561
+ * Pairs (visual, event, action) into a single struct so the discipline of
2562
+ * "render box ≠ event box" is explicit. The chrome builder emits a list of
2563
+ * these per frame; the surface fans them into render commands and hit
2564
+ * regions.
2565
+ *
2566
+ * - **Virtual elements** (e.g. rotation handles, edge resize strips) omit
2567
+ * `render` — they exist only as hit regions.
2568
+ * - **Padded elements** have `hit` larger than the corresponding `render`
2569
+ * shape (e.g. 16px hit AABB around an 8px visual knob).
2570
+ *
2571
+ * TODO(rotation): when the first base-rotated overlay lands (e.g. an
2572
+ * in-canvas rotation pip), add an `orientation: "screen" | "base"` field.
2573
+ * Existing call sites default to `"screen"` and don't change.
2574
+ */
2575
+ interface OverlayElement {
2576
+ /** Stable semantic identifier. Format: `"<kind>[:<param>]"`. Examples:
2577
+ * `"translate"`, `"resize_handle:nw"`, `"resize_edge:n"`,
2578
+ * `"rotate:ne"`, `"endpoint:p1"`. Used by tests and debug tooling. */
2579
+ label: string;
2580
+ /** Semantic owner for group-level visibility policy. */
2581
+ group?: HUDSemanticGroup;
2582
+ action: OverlayAction;
2583
+ hit: HitShape;
2584
+ render?: RenderShape;
2585
+ /** Lower wins. See `HUDHitPriority` in `selection-controls.ts`. */
2586
+ priority: number;
2587
+ cursor?: CursorIcon;
2588
+ /**
2589
+ * Optional refinement layered on top of the AABB hit check. Receives the
2590
+ * screen-space pointer (or shadow-space, when `hit.kind === "screen_obb"`
2591
+ * and the registry has applied `inverse_transform`). Returns false to
2592
+ * reject the hit even though the AABB matched.
2593
+ *
2594
+ * Used by curve-shaped hit regions (e.g. path segments) — the AABB is
2595
+ * the bezier's bbox, the refinement asks "are you actually near the
2596
+ * curve in screen-px?"
2597
+ */
2598
+ customHitTest?: (point: cmath.Vector2) => boolean;
2599
+ }
2600
+ //#endregion
2601
+ //#region surface/chrome.d.ts
2602
+ interface SurfaceChromeGroups {
2603
+ hover?: HUDSemanticGroup;
2604
+ selection?: HUDSemanticGroup;
2605
+ selectionControls?: HUDSemanticGroup;
2606
+ marquee?: HUDSemanticGroup;
2607
+ /** Lasso polygon (sibling of `marquee`). */
2608
+ lasso?: HUDSemanticGroup;
2609
+ transformPreview?: HUDSemanticGroup;
2610
+ }
2611
+ //#endregion
2612
+ //#region classes/vector-path/input.d.ts
2613
+ /**
2614
+ * Doc-space POJO returned by the host's `vectorOf` callback. The HUD never
2615
+ * imports `@grida/vn` — this minimal shape carries everything chrome needs.
2616
+ *
2617
+ * All coordinates are in doc-space (the HUD's container CSS-px frame). The
2618
+ * host is responsible for projecting from its local frame (e.g. SVG viewBox)
2619
+ * through the camera CTM before handing the data over.
2620
+ */
2621
+ interface VectorOverlay {
2622
+ /** Vertex positions in doc-space. Index === VertexId. */
2623
+ vertices: ReadonlyArray<readonly [number, number]>;
2624
+ /** Optional — present when the host wants segment chrome, tangent
2625
+ * handles, and segment hit-strips. Each segment carries the four
2626
+ * cubic control points in doc-space (already projected). */
2627
+ segments?: ReadonlyArray<{
2628
+ a: number;
2629
+ b: number;
2630
+ /** Absolute doc-space position of the first cubic control point
2631
+ * (= vertices[a] + ta_local, projected through the host's CTM). */
2632
+ a_control: readonly [number, number];
2633
+ /** Absolute doc-space position of the second cubic control point
2634
+ * (= vertices[b] + tb_local, projected). */
2635
+ b_control: readonly [number, number];
2636
+ }>;
2637
+ /** Vertices whose tangent handles should render. The host computes
2638
+ * this — selected vertices ∪ their 1-hop neighbours (see
2639
+ * `PathModel.neighbouringVertices`). Empty list = no tangent handles
2640
+ * rendered. Spelled `neighbours` (not `neighbouring_vertices`) for
2641
+ * brevity and so it doesn't collide with the main canvas editor's
2642
+ * `selection_neighbouring_vertices` state field — if/when the main
2643
+ * editor adopts this overlay shape, no field-name friction. */
2644
+ neighbours?: ReadonlyArray<number>;
2645
+ /**
2646
+ * Optional — closed-loop "regions" of the path. Each entry names the
2647
+ * segment indices forming one closed loop, in traversal order. The
2648
+ * loop must close (each segment's `b` must match the next segment's
2649
+ * `a`, and the last segment's `b` must match the first's `a`); the
2650
+ * HUD does not validate.
2651
+ *
2652
+ * Schema-level feature flag: absence of this field = backend doesn't
2653
+ * enumerate loops, no region chrome renders. Hosts that can derive
2654
+ * loops (e.g. via `vn.VectorNetworkEditor.getLoops()`) populate it
2655
+ * and pick up the chrome + intent + selection mirror for free.
2656
+ */
2657
+ regions?: ReadonlyArray<{
2658
+ segments: ReadonlyArray<number>;
2659
+ }>;
2660
+ /** Doc-space offset to add to local vertex coords before rendering.
2661
+ * For hosts that already project to doc-space (most), pass `[0, 0]`. */
2662
+ origin?: readonly [number, number];
2663
+ }
2664
+ //#endregion
2665
+ //#region classes/padding/priority.d.ts
2666
+ /**
2667
+ * Padding drag-handle priority slot. Lower wins.
2668
+ *
2669
+ * 12 — wins over corner-radius (15), every resize control (≥30), translate
2670
+ * body (40), rotate (50). The handle sits at the inner edge of the padding
2671
+ * rect; in practice it doesn't overlap perimeter resize/rotate but the
2672
+ * priority is insurance.
2673
+ */
2674
+ declare const PADDING_HANDLE_PRIORITY = 12;
2675
+ /**
2676
+ * Padding hover-region priority slot. Lower wins.
2677
+ *
2678
+ * 35 — wins over translate body (40), loses to all resize controls (30/31).
2679
+ * Clicking inside the padding zone of a selected flex container fires
2680
+ * padding hover instead of translate; clicking a corner still resizes.
2681
+ * The padding overlay paints over the selection chrome but loses to
2682
+ * the corner resize handles.
2683
+ */
2684
+ declare const PADDING_REGION_PRIORITY = 35;
2685
+ /**
2686
+ * Screen-px length of the drag-handle's long axis. The short axis is
2687
+ * `PADDING_HANDLE_THICKNESS`. The 16×2 pill is sized for Fitts'-reach
2688
+ * without dominating the inset side rect visually.
2689
+ */
2690
+ declare const PADDING_HANDLE_LENGTH = 16;
2691
+ /** Screen-px thickness of the drag-handle's short axis. */
2692
+ declare const PADDING_HANDLE_THICKNESS = 2;
2693
+ //#endregion
2694
+ //#region classes/padding/surface.d.ts
2695
+ /**
2696
+ * Build the per-frame padding overlay primitives.
2697
+ *
2698
+ * Emits one `OverlayElement` per side rect (hover-only, no intent) and
2699
+ * one per drag handle (per-side `padding_handle` action). The two are
2700
+ * keyed by side and share the same semantic `group`.
2701
+ *
2702
+ * Paint resolution:
2703
+ * - idle → no render (transparent body; hit still active)
2704
+ * - hover → `style.paddingHoverPaint`
2705
+ * - selected (= side === active_side, OR alt and side === opposite of
2706
+ * active_side) → outline stroke (no stripe)
2707
+ *
2708
+ * The drag handles are suppressed while any padding drag is in flight —
2709
+ * don't paint knobs the user can't grab.
2710
+ *
2711
+ * The region's hit shape is pre-projected to **screen-space AABB** here
2712
+ * — `transform` is needed so the hit area scales with zoom.
2713
+ */
2714
+ declare function buildPaddingOverlay(input: {
2715
+ overlay: PaddingOverlayInput;
2716
+ style: HUDStyle;
2717
+ hover: PaddingHover | null;
2718
+ alt_held: boolean;
2719
+ transform: cmath.Transform;
2720
+ active_side?: cmath.RectangleSide;
2721
+ }): OverlayElement[];
2722
+ //#endregion
2723
+ //#region classes/transform-box/priority.d.ts
2724
+ /**
2725
+ * 13 — wins over corner-radius (15) and everything below; loses to
2726
+ * padding-handle (12). Rotate grab should win over corner-radius if
2727
+ * both models overlap on the same node (free-transform case).
2728
+ */
2729
+ declare const TRANSFORM_BOX_CORNER_PRIORITY = 13;
2730
+ /** 14 — peer to corners on this model; wins over corner-radius (15). */
2731
+ declare const TRANSFORM_BOX_SIDE_PRIORITY = 14;
2732
+ /**
2733
+ * 38 — body translate beats marquee start (40) and selection-translate
2734
+ * body (40); loses to padding-region (35), every resize control (30/31),
2735
+ * and every other handle.
2736
+ */
2737
+ declare const TRANSFORM_BOX_BODY_PRIORITY = 38;
2738
+ /** Screen-px size of a corner knob (the rotate hit AABB). */
2739
+ declare const TRANSFORM_BOX_CORNER_HIT_SIZE = 16;
2740
+ /** Screen-px thickness of a side mid-edge hit strip. */
2741
+ declare const TRANSFORM_BOX_SIDE_HIT_THICKNESS = 12;
2742
+ //#endregion
2743
+ //#region classes/transform-box/surface.d.ts
2744
+ /**
2745
+ * Build the per-frame transform-box overlay primitives.
2746
+ *
2747
+ * Emits:
2748
+ * - 1 quad outline (visible — `HUDPolyline` stroke).
2749
+ * - 1 body hit polygon (invisible, fill transparent; intercepts events).
2750
+ * - 4 corner hits (invisible 16×16 rects centered on each corner).
2751
+ * - 4 side hit strips (invisible 12px-thick rotated rects along each side).
2752
+ */
2753
+ declare function buildTransformBox(input: {
2754
+ overlay: TransformBoxInput;
2755
+ style: HUDStyle;
2756
+ /** Reserved — handles are invisible today, so hover does not drive
2757
+ * render. Kept on the contract for the eventual visible-handle
2758
+ * variant (debug overlay, themed knobs). */
2759
+ hover: TransformBoxHover | null;
2760
+ transform: cmath.Transform; /** Reserved — see `hover`. */
2761
+ active_op?: TransformBoxActiveOp;
2762
+ }): OverlayElement[];
2763
+ //#endregion
2764
+ //#region surface/surface.d.ts
2765
+ interface SurfaceVisibilityContext {
2766
+ gesture: SurfaceGesture;
2767
+ }
2768
+ interface SurfaceVisibility {
2769
+ hidden?: Iterable<HUDSemanticGroup>;
2770
+ }
2771
+ type SurfaceVisibilityPolicy = (context: SurfaceVisibilityContext) => SurfaceVisibility | undefined;
2772
+ interface SurfaceOptions {
2773
+ /**
2774
+ * Content pick. Given a doc-space point, return the topmost node id under
2775
+ * the pointer, or `null`. Host wraps its scene query however it wants
2776
+ * (e.g. `elementFromPoint` + `data-id` for SVG-DOM hosts).
2777
+ */
2778
+ pick: (point_doc: [number, number]) => NodeId | null;
2779
+ /**
2780
+ * Selection shape for a node — what the chrome should wrap. Most nodes
2781
+ * return `{ kind: "rect", rect }`; vector lines return
2782
+ * `{ kind: "line", p1, p2 }`.
2783
+ */
2784
+ shapeOf: (id: NodeId) => SelectionShape | null;
2785
+ /**
2786
+ * Optional — vector geometry for a node under content-edit. When set
2787
+ * AND `setVectorSelection` has been called with a non-null mirror, the
2788
+ * surface renders vertex chrome (knobs, hit regions) for the named node.
2789
+ *
2790
+ * Hosts that never enter vector-edit mode (or that don't have path-aware
2791
+ * content-edit) can omit this. The chrome simply won't render.
2792
+ */
2793
+ vectorOf?: (id: NodeId) => VectorOverlay | null;
2794
+ /** Surface emits intents the host commits. */
2795
+ onIntent: IntentHandler;
2796
+ /** Initial style (partial; merged with defaults). */
2797
+ style?: Partial<HUDStyle>;
2798
+ /** Initial readonly flag. Default `false`. */
2799
+ readonly?: boolean;
2800
+ /** Optional HUDCanvas color override. */
2801
+ color?: string;
2802
+ /**
2803
+ * Optional pixel-grid configuration. Drawn back-most in the HUD canvas
2804
+ * when `enabled` and the current zoom exceeds `zoomThreshold`. Hosts can
2805
+ * also call `surface.setPixelGrid(...)` later.
2806
+ */
2807
+ pixelGrid?: PixelGridConfig | null;
2808
+ /**
2809
+ * Optional ruler configuration. Paints a top + left ruler strip (L-shape)
2810
+ * in screen-space, behind every other HUD primitive. Same two-transform
2811
+ * contract as `pixelGrid`. Hosts can also call `surface.setRuler(...)`
2812
+ * later. The corner square is deliberately left blank.
2813
+ */
2814
+ ruler?: RulerConfig | null;
2815
+ /**
2816
+ * Optional semantic groups for surface-owned chrome. The HUD package does
2817
+ * not define a group vocabulary; hosts pass the strings they want to use.
2818
+ */
2819
+ groups?: SurfaceChromeGroups;
2820
+ /**
2821
+ * Host-owned visibility policy. Called per frame with the current surface
2822
+ * gesture; returned groups are filtered from surface chrome and host extras.
2823
+ */
2824
+ visibility?: SurfaceVisibilityPolicy;
2825
+ /**
2826
+ * Where a click on a segment inserts the new vertex.
2827
+ *
2828
+ * - `"midpoint"` (default) — always at `t=0.5`. The ghost preview appears
2829
+ * at the segment's geometric midpoint on hover; the position is
2830
+ * independent of where on the segment the cursor lands. Predictable
2831
+ * and matches the "owning segment hovers → midpoint becomes visible"
2832
+ * mental model.
2833
+ * - `"projected"` — at the nearest point on the curve to the cursor
2834
+ * (`cmath.bezier.project`). The ghost tracks the cursor along the
2835
+ * curve. More expressive but adds a "where exactly will this land?"
2836
+ * cognitive step.
2837
+ *
2838
+ * Both modes share the same hit-test, chrome rendering, and intent
2839
+ * vocabulary — only the projected `t` differs. Hosts that don't set
2840
+ * this get `"midpoint"` (the predictable default).
2841
+ */
2842
+ vectorInsertionMode?: VectorInsertionMode;
2843
+ /**
2844
+ * Which gesture an empty-space drag promotes to:
2845
+ * - `"marquee"` (default) — axis-aligned rect selection.
2846
+ * - `"lasso"` — freeform polygon selection.
2847
+ *
2848
+ * Pushed by the host alongside its own tool toggle (e.g. when the host
2849
+ * swaps cursor ↔ lasso tools) via `setVectorSelectionMode`. The HUD reads
2850
+ * it only at empty-space drag promotion; no other branch consults it.
2851
+ * Symmetric with `vectorInsertionMode`.
2852
+ */
2853
+ vectorSelectionMode?: VectorSelectionMode;
2854
+ /**
2855
+ * Sticky-bend toggle for segment-body drag:
2856
+ * - `"auto"` (default) — Meta-modifier gates the bend gesture.
2857
+ * No Meta → translate; Meta-held → bend.
2858
+ * - `"always"` — segment drag bends regardless of Meta. The host's
2859
+ * bend tool sets this. Released by setting back to `"auto"`.
2860
+ *
2861
+ * Same host-pushed pattern as `vectorSelectionMode`. Affects only the
2862
+ * NEXT segment-drag promotion; in-flight gestures keep their committed
2863
+ * mode.
2864
+ */
2865
+ vectorBendMode?: VectorBendMode;
2866
+ }
2867
+ /** See {@link SurfaceOptions.vectorInsertionMode}. */
2868
+ type VectorInsertionMode = "midpoint" | "projected";
2869
+ /** See {@link SurfaceOptions.vectorSelectionMode}. */
2870
+ type VectorSelectionMode = "marquee" | "lasso";
2871
+ /** See {@link SurfaceOptions.vectorBendMode}. */
2872
+ type VectorBendMode = "auto" | "always";
2873
+ /**
2874
+ * Top-level wired surface.
2875
+ *
2876
+ * Owns an internal `HUDCanvas`, a `SurfaceState` (gesture/hover/...) and
2877
+ * the host providers. On every `dispatch`, the state machine runs;
2878
+ * `draw` composes surface chrome + host-fed extras into a single canvas
2879
+ * paint.
2880
+ */
2881
+ declare class Surface {
2882
+ private hudCanvas;
2883
+ private state;
2884
+ private style;
2885
+ private opts;
2886
+ /**
2887
+ * Current corner-radius input, or `null`. Owned here (not in
2888
+ * `SurfaceState`) because the chrome builder needs it AND the
2889
+ * pointer_down handler needs it AND the registry rebuild needs
2890
+ * it — placing it on `SurfaceState` would pull a primitive type
2891
+ * into the event-layer's dependencies, which the README's
2892
+ * dependency arrow forbids. Surface is the wired class; the
2893
+ * setter passes the input to both the canvas (for paint) and
2894
+ * the state (via hit-region overlays each frame).
2895
+ */
2896
+ private cornerRadius;
2897
+ private parametricHandles;
2898
+ /**
2899
+ * Padding-overlay input. `null` = no chrome drawn. Hosts push on
2900
+ * flex-parent selection; clear on deselect / mode exit. See
2901
+ * `setPaddingOverlay`.
2902
+ */
2903
+ private paddingOverlay;
2904
+ private colorOverride;
2905
+ private width;
2906
+ private height;
2907
+ private cursor_renderer;
2908
+ constructor(canvas: HTMLCanvasElement, options: SurfaceOptions);
2909
+ /** Switch the vector insertion mode at runtime. Affects both the
2910
+ * hover preview position and the split/bend `t` for the NEXT
2911
+ * pointer event. See {@link SurfaceOptions.vectorInsertionMode}. */
2912
+ setVectorInsertionMode(mode: VectorInsertionMode): void;
2913
+ /** Switch the empty-space-drag selection gesture at runtime
2914
+ * (`marquee` vs `lasso`). Affects only the NEXT pending → drag
2915
+ * promotion; any in-flight gesture keeps running. See
2916
+ * {@link SurfaceOptions.vectorSelectionMode}. */
2917
+ setVectorSelectionMode(mode: VectorSelectionMode): void;
2918
+ /** Switch the sticky-bend toggle at runtime (`auto` vs `always`).
2919
+ * `"always"` is the host's bend tool talking — every segment-drag
2920
+ * bends as if Meta were held. Affects only the NEXT segment-drag
2921
+ * promotion. See {@link SurfaceOptions.vectorBendMode}. */
2922
+ setVectorBendMode(mode: VectorBendMode): void;
2923
+ /** Configure / disable the back-most pixel-grid layer. */
2924
+ setPixelGrid(config: PixelGridConfig | null): void;
2925
+ /**
2926
+ * Update just the pixel grid's transform. Cheap to call per camera tick.
2927
+ * No-op when no pixel-grid config is set.
2928
+ */
2929
+ setPixelGridTransform(transform: Transform): void;
2930
+ /** Configure / disable the back-most ruler chrome (top + left strips). */
2931
+ setRuler(config: RulerConfig | null): void;
2932
+ /**
2933
+ * Configure or clear the built-in corner-radius chrome.
2934
+ *
2935
+ * Accepts a single input (one node's corner-radius chrome) OR an
2936
+ * array of inputs (multiple nodes editable at once — typical for
2937
+ * a multi-selection of rect-bearing nodes, or for demos that
2938
+ * compare axis-aligned vs rotated rects in the same viewport).
2939
+ * Pass `null` to remove everything. The chrome for each input is
2940
+ * independent: each input's handles, hit regions, and emitted
2941
+ * intents carry the input's own `node_id`. The host distinguishes
2942
+ * by `node_id` when applying the intent.
2943
+ *
2944
+ * The HUD paints handles along the corner→arc-center diagonal
2945
+ * (rect geometry) or the a→b axis (line geometry); on pointer_down
2946
+ * it owns the hit-test; on drag it emits one of `corner_radius` /
2947
+ * `corner_radius_explicit` / `corner_radius_uniform` per the
2948
+ * geometry + modifier table.
2949
+ *
2950
+ * The handle layout is recomputed every frame from the inputs —
2951
+ * camera changes and radius changes both reflect on the next
2952
+ * `draw()` without a host-side `setCornerRadius` round-trip.
2953
+ *
2954
+ * See `@grida/hud/primitives/corner-radius` for the input shape.
2955
+ */
2956
+ setCornerRadius(input: CornerRadiusInput | readonly CornerRadiusInput[] | null): void;
2957
+ /**
2958
+ * Push one or more parametric-handle inputs — the universal
2959
+ * "scalar value on a 1D manifold" affordance. Each input declares
2960
+ * a node_id, one or more handles (each with curve + value +
2961
+ * optional domain + optional snap-back inset), optional coincidence
2962
+ * groups, and an optional local→doc transform.
2963
+ *
2964
+ * The HUD paints handles along their curves on every `draw()`, owns
2965
+ * the hit-test for the knobs, and emits `parametric_handle` intents
2966
+ * on drag with `{ node_id, handle_id, value, modifiers, phase }`.
2967
+ * Modifier semantics (alt → explicit, etc.) are host-decided —
2968
+ * the producer reports flags only.
2969
+ *
2970
+ * Pass `null` (or `[]`) to remove all parametric chrome. Single
2971
+ * input or array; arrays let one viewport edit multiple nodes
2972
+ * simultaneously. Intents carry their input's `node_id` so the
2973
+ * host routes per-node.
2974
+ *
2975
+ * Hosts typically build inputs through use-case-specific composers
2976
+ * (e.g. the corner-radius primitive builds a 4-handle input over a
2977
+ * rect). The shape itself is generic — anything that can be
2978
+ * expressed as scalars on 1D tracks fits.
2979
+ */
2980
+ setParametricHandles(input: ParametricHandleInput | readonly ParametricHandleInput[] | null): void;
2981
+ /**
2982
+ * Configure or clear the built-in padding-overlay chrome — the
2983
+ * `padding` named class for flex-parent container padding editing.
2984
+ *
2985
+ * Pass an input to enable the chrome (four inset side rects with
2986
+ * diagonal-stripe hover/selected paint + four mid-edge drag handles).
2987
+ * Pass `null` to disable. Schema-level feature flag — absence is the
2988
+ * off-state, no separate boolean to drift.
2989
+ *
2990
+ * The HUD owns hover (including alt-held axis-mirror visual), reads
2991
+ * the `alt` modifier directly for the intent's `mirror` flag, and
2992
+ * emits `padding_handle` intents on drag (preview-stream + commit).
2993
+ * The host applies the value to its node's `layout_padding_{side}`
2994
+ * field and pushes a re-rendered input back via `setPaddingOverlay`
2995
+ * so the chrome reflects the new value.
2996
+ *
2997
+ * Mid-gesture state mirror: hosts SHOULD push `active_side` while a
2998
+ * `padding_handle` gesture is in flight so the dragged side paints
2999
+ * with the "selected" stripe variant (and the opposite side too,
3000
+ * when alt is held). Clear `active_side` on commit.
3001
+ *
3002
+ * See `@grida/hud/classes/padding` for the input shape and
3003
+ * paint/hit/priority details.
3004
+ */
3005
+ setPaddingOverlay(input: PaddingOverlayInput | null): void;
3006
+ /**
3007
+ * Push a transform-box input — the `transform-box` named class for
3008
+ * a 2×3 affine transform manipulated via quad outline + 4 corners +
3009
+ * 4 sides + body. `null` = no chrome drawn (schema-level feature
3010
+ * flag).
3011
+ *
3012
+ * Hosts re-push input whenever the bound transform / size / origin
3013
+ * / rotation changes (driven by the host's reducer applying the
3014
+ * `transform_box` intent). The chrome rebuilds every draw.
3015
+ *
3016
+ * See `@grida/hud/classes/transform-box` for the input shape and
3017
+ * hit/priority/anti-goals details.
3018
+ *
3019
+ * @unstable
3020
+ */
3021
+ setTransformBox(input: TransformBoxInput | null): void;
3022
+ /**
3023
+ * Update just the ruler's transform. Cheap to call per camera tick.
3024
+ * No-op when no ruler config is set.
3025
+ */
3026
+ setRulerTransform(transform: Transform): void;
3027
+ setSize(w: number, h: number): void;
3028
+ setTransform(t: Transform): void;
3029
+ /**
3030
+ * Push a new selection from the host.
3031
+ *
3032
+ * Accepts either:
3033
+ * - `NodeId[]` — each id becomes its own single-member group, shape
3034
+ * resolved via `shapeOf(id)` by the chrome builder.
3035
+ * - `SelectionGroup[]` — pre-computed groups with their union shape.
3036
+ *
3037
+ * See `SurfaceState.setSelection` for details.
3038
+ */
3039
+ setSelection(input: readonly NodeId[] | readonly SelectionGroup[]): void;
3040
+ /**
3041
+ * Push a vector-edit sub-selection. Pass `null` to exit vector chrome.
3042
+ *
3043
+ * Non-null: the surface renders vertex knobs for the named path on every
3044
+ * subsequent `draw()`, with selected vertices highlighted. Requires the
3045
+ * host to have wired `vectorOf` in `SurfaceOptions` — otherwise the
3046
+ * chrome silently skips.
3047
+ *
3048
+ * Host calls this on enter / exit of content-edit AND on every sub-
3049
+ * selection change (click, marquee, etc.). Cheap — just swaps a field.
3050
+ */
3051
+ setVectorSelection(input: VectorSubSelection | null): void;
3052
+ setStyle(partial: Partial<HUDStyle>): void;
3053
+ /**
3054
+ * Set or clear the host color override. `null` clears the override and
3055
+ * lets `style.chromeColor` win on the next paint.
3056
+ */
3057
+ setColor(color: string | null): void;
3058
+ setReadonly(v: boolean): void;
3059
+ /**
3060
+ * Set or clear a host-driven hover override.
3061
+ *
3062
+ * The surface tracks two hover sources:
3063
+ * - **Pointer pick** — what scene content is under the cursor (updated
3064
+ * automatically on `pointer_move`).
3065
+ * - **Host override** — what the host wants to show as hovered, e.g.
3066
+ * from a layers panel row mouseenter.
3067
+ *
3068
+ * The override (when non-null) wins. `hover()` returns the effective
3069
+ * value; chrome renders the effective value. Pass `null` to clear and
3070
+ * fall back to pointer pick.
3071
+ *
3072
+ * Returns the same response shape as `dispatch` so the host can react
3073
+ * to whether anything actually changed.
3074
+ */
3075
+ setHoverOverride(id: NodeId | null): SurfaceResponse;
3076
+ dispose(): void;
3077
+ dispatch(event: SurfaceEvent): SurfaceResponse;
3078
+ draw(extra?: HUDDraw): void;
3079
+ /** Convenience: clear the canvas (e.g. when the host stops the surface). */
3080
+ clear(): void;
3081
+ gesture(): SurfaceGesture;
3082
+ /**
3083
+ * The effective hover: host override (when set) wins over pointer pick.
3084
+ * Use this for chrome decisions and host-side reads.
3085
+ */
3086
+ hover(): NodeId | null;
3087
+ cursor(): CursorIcon;
3088
+ /**
3089
+ * Resolve the current cursor to a CSS `cursor:` value. Runs the
3090
+ * installed renderer (or the built-in `cursorToCss` if none installed).
3091
+ *
3092
+ * Host wires it like:
3093
+ *
3094
+ * const r = surface.dispatch(event);
3095
+ * if (r.cursorChanged) el.style.cursor = surface.cursorCss();
3096
+ *
3097
+ * Saves the host from re-importing `cursorToCss` after every dispatch
3098
+ * and gives one place to change behavior when a renderer is swapped in.
3099
+ */
3100
+ cursorCss(): string;
3101
+ /**
3102
+ * Install (or clear) a custom cursor renderer.
3103
+ *
3104
+ * `null` restores the built-in `cursorToCss` behavior (native CSS
3105
+ * keywords for every variant). Pass `cursors.defaultRenderer()` from
3106
+ * `@grida/hud/cursors` for the bundled SVG cursor set.
3107
+ *
3108
+ * Re-callable mid-session; the next `cursorCss()` reads the new value.
3109
+ */
3110
+ setCursorRenderer(fn: CursorRenderer | null): void;
3111
+ modifiers(): Modifiers;
3112
+ /**
3113
+ * Resolve the current corner-radius layout against `shapeOf` (for
3114
+ * the rect-fallback) and the camera. Always returns an array;
3115
+ * empty when the input is null or when the rect can't be
3116
+ * resolved.
3117
+ */
3118
+ private buildCornerRadiusHandles;
3119
+ /**
3120
+ * Push one hit region per handle onto the state's registry. Called
3121
+ * AFTER `fanOverlays` so the regions survive the per-frame clear.
3122
+ */
3123
+ private registerCornerRadiusHitRegions;
3124
+ /**
3125
+ * Resolve the current parametric-handle layout for one input. The
3126
+ * snap-back floor is lifted while this specific input is being
3127
+ * dragged — match by `node_id` so other inputs in the same canvas
3128
+ * keep their resting positions.
3129
+ */
3130
+ private buildParametricHandleLayout;
3131
+ /**
3132
+ * Push one hit region per coincidence group onto the state's
3133
+ * registry. Coincidence is geometric — a declared group only
3134
+ * collapses when its members actually overlap in doc-space. Open
3135
+ * groups produce one region per handle.
3136
+ */
3137
+ private registerParametricHandleHitRegions;
3138
+ }
3139
+ //#endregion
3140
+ export { computeStripesTileGeometry as $, DEFAULT_RULER_STRIP as $t, TransformBoxHover as A, resolveCenterDragAnchor as At, PointerButton as B, compose as Bt, MIN_CHROME_VISIBLE_SIZE as C, DrawCornerRadiusParams as Ct, VectorHover as D, cornerRadiusHandlePosRect as Dt, RenderShape as E, cornerRadiusHandlePosLine as Et, Intent as F, SelectionShape as Ft, measurementToHUDDraw as G, DEFAULT_RULER_ACCENT_BACKGROUND as Gt, SurfaceResponse as H, decompose as Ht, IntentPhase as I, AffineTransform as It, DEFAULT_STRIPES_ANGLE_DEG as J, DEFAULT_RULER_COLOR as Jt, snapGuideToHUDDraw as K, DEFAULT_RULER_ACCENT_COLOR as Kt, SelectMode as L, TransformBoxAction as Lt, PaddingHover as M, Rect as Mt, PaddingOverlayInput as N, SurfaceGesture as Nt, VectorSubSelection as O, cornerRadiusLayoutGroups as Ot, HUDStyle as P, SelectionGroup as Pt, buildStripesTile as Q, DEFAULT_RULER_STEPS as Qt, Modifiers as R, TransformBoxCorners as Rt, HitShape as S, DEFAULT_CORNER_RADIUS_HIT_SIZE as St, OverlayElement as T, cornerRadiusAnchorSign as Tt, lassoToHUDDraw as U, getTransformBoxCorners as Ut, SurfaceEvent as V, cornersToBoxTransform as Vt, marqueeToHUDDraw as W, reduceTransformBox as Wt, DEFAULT_STRIPES_THICKNESS_PX as X, DEFAULT_RULER_FONT as Xt, DEFAULT_STRIPES_SPACING_PX as Y, DEFAULT_RULER_DRAG_THRESHOLD as Yt, ResolvedPaint as Z, DEFAULT_RULER_OVERLAP_THRESHOLD as Zt, PADDING_HANDLE_PRIORITY as _, CornerRadiusHandleLayout as _t, SurfaceVisibilityPolicy as a, RulerMark as an, DEFAULT_PARAMETRIC_HIT_SIZE as at, VectorOverlay as b, DEFAULT_CORNER_RADIUS_HANDLE_INSET as bt, VectorSelectionMode as c, DEFAULT_PIXEL_GRID_COLOR as cn, ParametricHandleGroup as ct, TRANSFORM_BOX_CORNER_HIT_SIZE as d, PixelGridConfig as dn, computeParametricHandleLayout as dt, DEFAULT_RULER_TEXT_SIDE_OFFSET as en, resolvePaint as et, TRANSFORM_BOX_CORNER_PRIORITY as f, drawPixelGrid as fn, drawParametricHandles as ft, PADDING_HANDLE_LENGTH as g, CornerRadiusAnchor as gt, buildPaddingOverlay as h, resolveParametricHandleByDirection as ht, SurfaceVisibilityContext as i, RulerConfig as in, DEFAULT_PARAMETRIC_HANDLE_SIZE as it, TransformBoxInput as j, resolveCornerDragAnchor as jt, TransformBoxActiveOp as k, drawCornerRadius as kt, buildTransformBox as l, DEFAULT_PIXEL_GRID_STEPS as ln, ParametricHandleInput as lt, TRANSFORM_BOX_SIDE_PRIORITY as m, projectParametricHandleValue as mt, SurfaceOptions as n, DrawRulerParams as nn, HUDCanvasOptions as nt, VectorBendMode as o, RulerRange as on, DrawParametricHandlesParams as ot, TRANSFORM_BOX_SIDE_HIT_THICKNESS as p, parametricHandleLayoutGroups as pt, filterHUDDrawByGroup as q, DEFAULT_RULER_BACKGROUND as qt, SurfaceVisibility as r, RulerAxis as rn, DEFAULT_PARAMETRIC_HANDLE_INSET as rt, VectorInsertionMode as s, drawRuler as sn, ParametricHandle as st, Surface as t, DEFAULT_RULER_TICK_HEIGHT as tn, HUDCanvas as tt, TRANSFORM_BOX_BODY_PRIORITY as u, DrawPixelGridParams as un, ParametricHandleLayout as ut, PADDING_HANDLE_THICKNESS as v, CornerRadiusInput as vt, MIN_HIT_SIZE as w, computeCornerRadiusLayout as wt, SurfaceChromeGroups as x, DEFAULT_CORNER_RADIUS_HANDLE_SIZE as xt, PADDING_REGION_PRIORITY as y, CornerRadiusRectangular as yt, NO_MODS as z, TransformBoxOptions as zt };