@grida/hud 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,734 @@
1
+ import { Measurement } from "@grida/cmath/_measurement";
2
+ import cmath from "@grida/cmath";
3
+ import { guide } from "@grida/cmath/_snap";
4
+
5
+ //#region primitives/types.d.ts
6
+ /**
7
+ * A line segment in document space, extending `cmath.ui.Line` with
8
+ * an optional `dashed` style.
9
+ */
10
+ interface HUDLine extends cmath.ui.Line {
11
+ dashed?: boolean;
12
+ /** Stroke width in screen-space CSS px. Defaults to the canvas default. */
13
+ strokeWidth?: number;
14
+ /**
15
+ * Override the canvas color for this line's stroke and label pill. Falls
16
+ * back to the canvas's current color when absent.
17
+ */
18
+ color?: string;
19
+ }
20
+ /**
21
+ * A full-viewport axis-aligned line (infinite extent) at a given offset.
22
+ *
23
+ * `axis` indicates which axis the `offset` lives on:
24
+ * - `"x"` → vertical line at x=offset
25
+ * - `"y"` → horizontal line at y=offset
26
+ *
27
+ * Offset is in document space; the renderer projects it to screen.
28
+ */
29
+ interface HUDRule {
30
+ axis: "x" | "y";
31
+ offset: number;
32
+ }
33
+ /**
34
+ * A rectangle in document space.
35
+ *
36
+ * Stroke uses the canvas color by default. Fill is opt-in.
37
+ */
38
+ interface HUDRect {
39
+ x: number;
40
+ y: number;
41
+ width: number;
42
+ height: number;
43
+ /** Whether to stroke the outline (default: true). */
44
+ stroke?: boolean;
45
+ /** Whether to fill the interior (default: false). */
46
+ fill?: boolean;
47
+ /** Opacity of the fill color, 0–1 (default: 1). Ignored when `fill` is falsy. */
48
+ fillOpacity?: number;
49
+ /** Draw the outline with a dash pattern. */
50
+ dashed?: boolean;
51
+ /** Stroke width in screen-space CSS px. Defaults to the canvas default. */
52
+ strokeWidth?: number;
53
+ /**
54
+ * Override the canvas color for this rect's stroke and fill. Falls back
55
+ * to the canvas's current color when absent.
56
+ */
57
+ color?: string;
58
+ }
59
+ /**
60
+ * A polyline (or closed polygon) in document space.
61
+ *
62
+ * Stroke uses the canvas color by default. Fill is opt-in.
63
+ */
64
+ interface HUDPolyline {
65
+ points: cmath.Vector2[];
66
+ /** Whether to stroke the path (default: true). */
67
+ stroke?: boolean;
68
+ /** Whether to fill the interior (default: false). */
69
+ fill?: boolean;
70
+ /** Opacity of the fill color, 0–1 (default: 1). Ignored when `fill` is falsy. */
71
+ fillOpacity?: number;
72
+ /** Draw the stroke with a dash pattern. */
73
+ dashed?: boolean;
74
+ /**
75
+ * Override the canvas color for this polyline's stroke and fill. Falls
76
+ * back to the canvas's current color when absent.
77
+ */
78
+ color?: string;
79
+ }
80
+ /**
81
+ * A rectangle whose **size is fixed in screen-space** but whose **anchor lives
82
+ * in document space**. Used for handles and other chrome that must not scale
83
+ * with viewport zoom.
84
+ *
85
+ * The anchor `(x, y)` is the document-space point that maps to one corner (or
86
+ * center) of the resulting screen-space rect — controlled by `anchor`.
87
+ *
88
+ * Default anchor is `"center"` (the doc-space point becomes the rect's center).
89
+ */
90
+ interface HUDScreenRect {
91
+ /** Document-space anchor point. */
92
+ x: number;
93
+ y: number;
94
+ /** Screen-space size in CSS px. */
95
+ width: number;
96
+ height: number;
97
+ /** Which point of the rect sits at (x, y). Default: "center". */
98
+ anchor?: "center" | "tl" | "tr" | "bl" | "br";
99
+ fill?: boolean;
100
+ stroke?: boolean;
101
+ /** Override the canvas color for fill. */
102
+ fillColor?: string;
103
+ /** Override the canvas color for stroke. */
104
+ strokeColor?: string;
105
+ }
106
+ /**
107
+ * A complete set of draw commands for one frame.
108
+ *
109
+ * The HUD clears the canvas and draws everything in this struct each frame.
110
+ * Callers build a `HUDDraw` from their domain data (snap result, measurement,
111
+ * guide state, etc.) and hand it to `HUDCanvas.draw()`.
112
+ */
113
+ interface HUDDraw {
114
+ lines?: HUDLine[];
115
+ rules?: HUDRule[];
116
+ points?: cmath.Vector2[];
117
+ rects?: HUDRect[];
118
+ polylines?: HUDPolyline[];
119
+ /** Screen-space-sized rects anchored to document-space points (handles). */
120
+ screenRects?: HUDScreenRect[];
121
+ }
122
+ //#endregion
123
+ //#region primitives/canvas.d.ts
124
+ interface HUDCanvasOptions {
125
+ color?: string;
126
+ }
127
+ /**
128
+ * Imperative Canvas 2D renderer for the HUD overlay.
129
+ *
130
+ * Owns a single `<canvas>` element and draws {@link HUDDraw} command lists
131
+ * each frame. All drawing is immediate-mode: the canvas is cleared and
132
+ * fully redrawn on every `draw()` call.
133
+ *
134
+ * The viewport transform is assumed to be axis-aligned (scale + translate only,
135
+ * no rotation/shear). The off-diagonal components of the transform matrix are
136
+ * ignored.
137
+ */
138
+ declare class HUDCanvas {
139
+ private canvas;
140
+ private ctx;
141
+ private dpr;
142
+ private transform;
143
+ private color;
144
+ private width;
145
+ private height;
146
+ constructor(canvas: HTMLCanvasElement, options?: HUDCanvasOptions);
147
+ setColor(color?: string): void;
148
+ setSize(w: number, h: number): void;
149
+ setTransform(transform: cmath.Transform): void;
150
+ /**
151
+ * Clear the canvas and draw all primitives in `commands`.
152
+ * Pass `undefined` to clear without drawing (e.g. when no overlay is active).
153
+ */
154
+ draw(commands: HUDDraw | undefined): void;
155
+ private applyViewTransform;
156
+ private applyScreenTransform;
157
+ /** Project a scalar offset on `axis` to screen-space. */
158
+ private deltaToScreen;
159
+ private drawRules;
160
+ private drawLines;
161
+ private drawRects;
162
+ private drawPolylines;
163
+ private drawPoints;
164
+ /**
165
+ * Draw rects whose **size is in screen-space** but whose **anchor is in
166
+ * document-space**. The doc-space point is projected via the current
167
+ * transform; the rect is then drawn at fixed CSS-pixel dimensions.
168
+ *
169
+ * This is the primitive used to draw resize / rotate handles — they must
170
+ * remain a constant visual size regardless of viewport zoom.
171
+ */
172
+ private drawScreenRects;
173
+ }
174
+ //#endregion
175
+ //#region primitives/snap-guide.d.ts
176
+ /**
177
+ * Convert a `guide.SnapGuide` (the output of `guide.plot()`) into a
178
+ * generic {@link HUDDraw} command list.
179
+ *
180
+ * Lines pass through directly (HUDLine extends cmath.ui.Line).
181
+ * Points pass through directly (both are cmath.Vector2).
182
+ * Rules are destructured from tuples to objects.
183
+ */
184
+ declare function snapGuideToHUDDraw(sg: guide.SnapGuide | undefined): HUDDraw | undefined;
185
+ //#endregion
186
+ //#region primitives/measurement-guide.d.ts
187
+ /**
188
+ * Convert a {@link Measurement} (the output of `measure()`) into a
189
+ * generic {@link HUDDraw} command list.
190
+ *
191
+ * All coordinates are in **document space** — the HUD canvas applies
192
+ * the viewport transform.
193
+ *
194
+ * Produces:
195
+ * - Two stroke-only rects for the A and B bounding boxes
196
+ * - One labelled guide line per non-zero distance (solid)
197
+ * - One auxiliary line per non-zero side connecting the guide to B (dashed)
198
+ *
199
+ * If `color` is provided, every emitted line and rect carries that color so
200
+ * the guides render distinctly from the canvas's chrome color. When used via
201
+ * `surface.draw(extra)` (the host-fed-extras channel) this is required to
202
+ * separate measurement from selection chrome on a shared canvas.
203
+ */
204
+ declare function measurementToHUDDraw(m: Measurement, color?: string): HUDDraw;
205
+ //#endregion
206
+ //#region primitives/marquee.d.ts
207
+ /**
208
+ * Convert two marquee corner points into a {@link HUDDraw} command list.
209
+ *
210
+ * All coordinates are in **document space**.
211
+ *
212
+ * Produces a single rectangle with a stroke outline and a semi-transparent fill.
213
+ */
214
+ declare function marqueeToHUDDraw(a: cmath.Vector2, b: cmath.Vector2): HUDDraw;
215
+ //#endregion
216
+ //#region primitives/lasso.d.ts
217
+ /**
218
+ * Convert a lasso point sequence into a {@link HUDDraw} command list.
219
+ *
220
+ * All coordinates are in **document space**.
221
+ *
222
+ * Produces a single polyline with a dashed stroke and a semi-transparent fill.
223
+ */
224
+ declare function lassoToHUDDraw(points: cmath.Vector2[]): HUDDraw | undefined;
225
+ //#endregion
226
+ //#region event/event.d.ts
227
+ /** Modifier-key snapshot at the moment an event was produced. */
228
+ interface Modifiers {
229
+ shift: boolean;
230
+ alt: boolean;
231
+ meta: boolean;
232
+ ctrl: boolean;
233
+ }
234
+ declare const NO_MODS: Modifiers;
235
+ type PointerButton = "primary" | "secondary" | "middle";
236
+ /**
237
+ * Input event consumed by `Surface.dispatch`.
238
+ *
239
+ * All coordinates are **screen-space CSS pixels relative to the canvas**.
240
+ * The surface owns the camera and converts to document-space internally.
241
+ */
242
+ type SurfaceEvent = {
243
+ kind: "pointer_move";
244
+ x: number;
245
+ y: number;
246
+ mods: Modifiers;
247
+ } | {
248
+ kind: "pointer_down";
249
+ x: number;
250
+ y: number;
251
+ button: PointerButton;
252
+ mods: Modifiers;
253
+ } | {
254
+ kind: "pointer_up";
255
+ x: number;
256
+ y: number;
257
+ button: PointerButton;
258
+ mods: Modifiers;
259
+ } | {
260
+ kind: "modifiers";
261
+ mods: Modifiers;
262
+ } | {
263
+ kind: "wheel";
264
+ x: number;
265
+ y: number;
266
+ dx: number;
267
+ dy: number;
268
+ mods: Modifiers;
269
+ } | {
270
+ kind: "key";
271
+ phase: "down" | "up";
272
+ code: string;
273
+ mods: Modifiers;
274
+ } | {
275
+ kind: "blur";
276
+ };
277
+ /** Result of a `Surface.dispatch` call. */
278
+ interface SurfaceResponse {
279
+ needsRedraw: boolean;
280
+ cursorChanged: boolean;
281
+ hoverChanged: boolean;
282
+ }
283
+ //#endregion
284
+ //#region event/cursor.d.ts
285
+ /** 8 cardinal/diagonal resize directions. */
286
+ type ResizeDirection = "n" | "ne" | "e" | "se" | "s" | "sw" | "w" | "nw";
287
+ /** 4 corner positions for rotation handles. */
288
+ type RotationCorner = "nw" | "ne" | "se" | "sw";
289
+ /** Logical cursor icon names — the host maps these to CSS `cursor` values. */
290
+ type CursorIcon = "default" | "pointer" | "move" | "crosshair" | "grab" | "grabbing" | "text" | {
291
+ kind: "resize";
292
+ direction: ResizeDirection;
293
+ } | {
294
+ kind: "rotate";
295
+ corner: RotationCorner;
296
+ };
297
+ //#endregion
298
+ //#region event/gesture.d.ts
299
+ /**
300
+ * Rect type local to the event/ layer.
301
+ *
302
+ * Mirrors `cmath.Rectangle` shape; declared here to keep `event/` free of
303
+ * imports beyond `cmath` types.
304
+ */
305
+ interface Rect {
306
+ x: number;
307
+ y: number;
308
+ width: number;
309
+ height: number;
310
+ }
311
+ type NodeId = string;
312
+ /**
313
+ * Active interaction state for the surface.
314
+ *
315
+ * Coordinates inside each variant are documented per-field. The surface
316
+ * stores anchor points in document-space (so they survive camera pans during
317
+ * a gesture); incremental deltas are computed against the anchor each move.
318
+ */
319
+ type SurfaceGesture = {
320
+ kind: "idle";
321
+ } | {
322
+ kind: "pan"; /** Last screen-space pointer position. */
323
+ prev_screen: cmath.Vector2;
324
+ } | {
325
+ kind: "marquee"; /** Anchor (pointer-down) in document-space. */
326
+ anchor_doc: cmath.Vector2; /** Current pointer in document-space. */
327
+ current_doc: cmath.Vector2;
328
+ } | {
329
+ kind: "translate"; /** Selected ids at the start of the gesture. */
330
+ ids: NodeId[]; /** Anchor (pointer-down) in document-space. */
331
+ anchor_doc: cmath.Vector2; /** Last reported pointer in document-space. */
332
+ last_doc: cmath.Vector2;
333
+ } | {
334
+ kind: "resize"; /** Member ids of the group being resized (1 or more). */
335
+ ids: NodeId[]; /** Which handle the user grabbed. */
336
+ direction: ResizeDirection; /** Bounding rect of the group at gesture start, in document-space. */
337
+ initial_rect: Rect; /** Anchor (pointer-down) in document-space. */
338
+ anchor_doc: cmath.Vector2; /** Current rect during the gesture, in document-space. */
339
+ current_rect: Rect;
340
+ } | {
341
+ kind: "rotate";
342
+ ids: NodeId[];
343
+ corner: RotationCorner; /** Subject center in document-space. */
344
+ center_doc: cmath.Vector2; /** Angle at gesture start (radians). */
345
+ anchor_angle: number; /** Current angle (radians). */
346
+ current_angle: number;
347
+ } | {
348
+ kind: "endpoint";
349
+ id: NodeId;
350
+ endpoint: "p1" | "p2"; /** Current endpoint position in document-space. */
351
+ pos_doc: cmath.Vector2;
352
+ };
353
+ //#endregion
354
+ //#region event/intent.d.ts
355
+ /** "preview" is emitted on every gesture move; "commit" once on release. */
356
+ type IntentPhase = "preview" | "commit";
357
+ /**
358
+ * Selection mode for `select` intents.
359
+ *
360
+ * - `replace` — clear selection, then select the given ids
361
+ * - `add` — union into the current selection
362
+ * - `toggle` — flip each given id's membership
363
+ */
364
+ type SelectMode = "replace" | "add" | "toggle";
365
+ /**
366
+ * Actionable change emitted by the surface. The host commits the intent
367
+ * (wrapping in `history.preview` for `phase: "preview"`, finalizing for
368
+ * `phase: "commit"`).
369
+ *
370
+ * The surface itself never mutates the document.
371
+ */
372
+ type Intent = {
373
+ kind: "select";
374
+ ids: NodeId[];
375
+ mode: SelectMode;
376
+ } | {
377
+ kind: "deselect_all";
378
+ } | {
379
+ kind: "translate";
380
+ ids: NodeId[]; /** Total delta in document-space, from gesture start. */
381
+ dx: number;
382
+ dy: number;
383
+ phase: IntentPhase;
384
+ } | {
385
+ kind: "resize"; /** Member ids of the group being resized (1 or more). */
386
+ ids: NodeId[];
387
+ anchor: ResizeDirection; /** Target rect in document-space. */
388
+ rect: Rect;
389
+ phase: IntentPhase;
390
+ } | {
391
+ kind: "rotate"; /** Member ids of the group being rotated (typically 1). */
392
+ ids: NodeId[]; /** Target angle delta in radians (relative to gesture start). */
393
+ angle: number;
394
+ phase: IntentPhase;
395
+ } | {
396
+ kind: "marquee_select"; /** Marquee rect in document-space (normalized). */
397
+ rect: Rect;
398
+ additive: boolean;
399
+ phase: IntentPhase;
400
+ } | {
401
+ kind: "set_endpoint"; /** Subject node id (line-shape selection). */
402
+ id: NodeId; /** Which endpoint is being moved. */
403
+ endpoint: "p1" | "p2"; /** Target position in document-space. */
404
+ pos: cmath.Vector2;
405
+ phase: IntentPhase;
406
+ } | {
407
+ kind: "enter_content_edit";
408
+ id: NodeId;
409
+ } | {
410
+ kind: "cancel_gesture";
411
+ };
412
+ /** Callback the host implements to receive intents. */
413
+ type IntentHandler = (intent: Intent) => void;
414
+ //#endregion
415
+ //#region event/transform.d.ts
416
+ /**
417
+ * Surface camera transform: axis-aligned (scale + translate only).
418
+ *
419
+ * Stored as a `cmath.Transform`:
420
+ * `[[sx, 0, tx], [0, sy, ty]]`
421
+ *
422
+ * Off-diagonal components are ignored; the surface does not support rotation
423
+ * or shear at the camera level.
424
+ */
425
+ type Transform = cmath.Transform;
426
+ //#endregion
427
+ //#region event/shape.d.ts
428
+ /**
429
+ * Selection shape — what the chrome wraps.
430
+ *
431
+ * Hosts return a `SelectionShape` from `shapeOf(id)` so the HUD can lay out
432
+ * the right kind of chrome:
433
+ *
434
+ * - **`rect`** — standard bounding box. Renders selection outline + 4 corner
435
+ * knobs, with virtual edge / rotation hit regions.
436
+ * - **`line`** — two-endpoint primitive (e.g. SVG `<line>`). Renders the
437
+ * line segment + 2 endpoint knobs. No edge / rotation regions.
438
+ * - **`unresolved`** — internal-only. Used inside `SelectionGroup` when the
439
+ * host gave a flat `NodeId[]` to `setSelection` — the chrome builder
440
+ * resolves the real shape by calling `shapeOf(id)` at frame build time.
441
+ * Hosts never emit this; treating it as host-facing would be a bug.
442
+ *
443
+ * Future kinds (e.g. `polyline`, `ellipse_oriented`) can be added without
444
+ * breaking the existing kinds — the chrome builder branches on `kind`.
445
+ */
446
+ type SelectionShape = {
447
+ kind: "rect";
448
+ rect: Rect;
449
+ } | {
450
+ kind: "line";
451
+ p1: cmath.Vector2;
452
+ p2: cmath.Vector2;
453
+ } | {
454
+ kind: "unresolved";
455
+ id: string;
456
+ };
457
+ /**
458
+ * A logical selection group. The HUD renders one chrome instance per group.
459
+ *
460
+ * The host pre-computes `shape` (typically the union of member bounds for
461
+ * multi-node groups). Member `ids` are the gesture target — they all
462
+ * translate/resize together as a unit.
463
+ */
464
+ interface SelectionGroup {
465
+ ids: readonly NodeId[];
466
+ shape: SelectionShape;
467
+ }
468
+ //#endregion
469
+ //#region surface/style.d.ts
470
+ /**
471
+ * HUD style — colors, sizes, and offsets for the surface-owned chrome.
472
+ *
473
+ * All fields are optional; defaults follow.
474
+ */
475
+ interface HUDStyle {
476
+ /** Primary chrome color (selection outline, handle border). */
477
+ chromeColor: string;
478
+ /** Secondary color used for hover outline (lighter than chrome). */
479
+ hoverColor: string;
480
+ /** Handle visual size in screen-px. */
481
+ handleSize: number;
482
+ /** Handle fill color. */
483
+ handleFill: string;
484
+ /** Handle stroke color. */
485
+ handleStroke: string;
486
+ /** Selection outline stroke width (in screen-px). */
487
+ selectionOutlineWidth: number;
488
+ /** Hover outline stroke width (in screen-px). Typically thicker than selection. */
489
+ hoverOutlineWidth: number;
490
+ /** Whether to render rotation handles in addition to resize handles. */
491
+ showRotationHandles: boolean;
492
+ }
493
+ //#endregion
494
+ //#region surface/surface.d.ts
495
+ interface SurfaceOptions {
496
+ /**
497
+ * Content pick. Given a doc-space point, return the topmost node id under
498
+ * the pointer, or `null`. Host wraps its scene query however it wants
499
+ * (e.g. `elementFromPoint` + `data-id` for SVG-DOM hosts).
500
+ */
501
+ pick: (point_doc: [number, number]) => NodeId | null;
502
+ /**
503
+ * Selection shape for a node — what the chrome should wrap. Most nodes
504
+ * return `{ kind: "rect", rect }`; vector lines return
505
+ * `{ kind: "line", p1, p2 }`.
506
+ */
507
+ shapeOf: (id: NodeId) => SelectionShape | null;
508
+ /** Surface emits intents the host commits. */
509
+ onIntent: IntentHandler;
510
+ /** Initial style (partial; merged with defaults). */
511
+ style?: Partial<HUDStyle>;
512
+ /** Initial readonly flag. Default `false`. */
513
+ readonly?: boolean;
514
+ /** Optional HUDCanvas color override. */
515
+ color?: string;
516
+ }
517
+ /**
518
+ * Top-level wired surface.
519
+ *
520
+ * Owns an internal `HUDCanvas`, a `SurfaceState` (gesture/hover/...) and
521
+ * the host providers. On every `dispatch`, the state machine runs;
522
+ * `draw` composes surface chrome + host-fed extras into a single canvas
523
+ * paint.
524
+ */
525
+ declare class Surface {
526
+ private hudCanvas;
527
+ private state;
528
+ private style;
529
+ private opts;
530
+ private width;
531
+ private height;
532
+ constructor(canvas: HTMLCanvasElement, options: SurfaceOptions);
533
+ setSize(w: number, h: number): void;
534
+ setTransform(t: Transform): void;
535
+ /**
536
+ * Push a new selection from the host.
537
+ *
538
+ * Accepts either:
539
+ * - `NodeId[]` — each id becomes its own single-member group, shape
540
+ * resolved via `shapeOf(id)` by the chrome builder.
541
+ * - `SelectionGroup[]` — pre-computed groups with their union shape.
542
+ *
543
+ * See `SurfaceState.setSelection` for details.
544
+ */
545
+ setSelection(input: readonly NodeId[] | readonly SelectionGroup[]): void;
546
+ setStyle(partial: Partial<HUDStyle>): void;
547
+ setReadonly(v: boolean): void;
548
+ /**
549
+ * Set or clear a host-driven hover override.
550
+ *
551
+ * The surface tracks two hover sources:
552
+ * - **Pointer pick** — what scene content is under the cursor (updated
553
+ * automatically on `pointer_move`).
554
+ * - **Host override** — what the host wants to show as hovered, e.g.
555
+ * from a layers panel row mouseenter.
556
+ *
557
+ * The override (when non-null) wins. `hover()` returns the effective
558
+ * value; chrome renders the effective value. Pass `null` to clear and
559
+ * fall back to pointer pick.
560
+ *
561
+ * Returns the same response shape as `dispatch` so the host can react
562
+ * to whether anything actually changed.
563
+ */
564
+ setHoverOverride(id: NodeId | null): SurfaceResponse;
565
+ dispose(): void;
566
+ dispatch(event: SurfaceEvent): SurfaceResponse;
567
+ draw(extra?: HUDDraw): void;
568
+ /** Convenience: clear the canvas (e.g. when the host stops the surface). */
569
+ clear(): void;
570
+ gesture(): SurfaceGesture;
571
+ /**
572
+ * The effective hover: host override (when set) wins over pointer pick.
573
+ * Use this for chrome decisions and host-side reads.
574
+ */
575
+ hover(): NodeId | null;
576
+ cursor(): CursorIcon;
577
+ modifiers(): Modifiers;
578
+ }
579
+ //#endregion
580
+ //#region event/hit-regions.d.ts
581
+ /**
582
+ * Action a UI hit-region triggers when clicked.
583
+ *
584
+ * Each variant carries a snapshot of the relevant shape state at the time
585
+ * the chrome was built — so the surface can start a gesture without an
586
+ * extra round-trip to host providers. The chrome builder is the single
587
+ * source of truth for "what does this hit region act on?"
588
+ *
589
+ * - `select_node` — user clicked on a node-representative UI region.
590
+ * - `resize_handle` — one of 8 resize regions (4 corner knobs + 4 virtual
591
+ * edges). Carries the group's member ids and the group's initial rect.
592
+ * - `rotate_handle` — one of 4 virtual rotation regions outside the group's
593
+ * corners. Carries the group's initial rect for center math.
594
+ * - `endpoint_handle` — endpoint of a line-shape selection. Carries the
595
+ * line's current p1/p2 so dragging is relative to a stable snapshot.
596
+ * - `translate_handle` — body region covering a selection group's bbox.
597
+ * Pushed under the corner / edge / rotation regions so resize wins on
598
+ * overlap. Lets the user grab any part of the selection chrome — including
599
+ * transparent corners of a circle's bbox — to start a translate. Hud is
600
+ * the event source once present.
601
+ */
602
+ type OverlayAction = {
603
+ kind: "select_node";
604
+ id: NodeId;
605
+ } | {
606
+ kind: "resize_handle";
607
+ direction: ResizeDirection;
608
+ ids: readonly NodeId[];
609
+ initial_rect: {
610
+ x: number;
611
+ y: number;
612
+ width: number;
613
+ height: number;
614
+ };
615
+ } | {
616
+ kind: "rotate_handle";
617
+ corner: RotationCorner;
618
+ ids: readonly NodeId[];
619
+ initial_rect: {
620
+ x: number;
621
+ y: number;
622
+ width: number;
623
+ height: number;
624
+ };
625
+ } | {
626
+ kind: "endpoint_handle";
627
+ endpoint: "p1" | "p2";
628
+ id: NodeId; /** Snapshot of the line endpoints in doc-space at chrome build time. */
629
+ p1: [number, number];
630
+ p2: [number, number];
631
+ } | {
632
+ kind: "translate_handle";
633
+ ids: readonly NodeId[];
634
+ };
635
+ //#endregion
636
+ //#region event/overlay.d.ts
637
+ /**
638
+ * Minimum hit-target size in screen-px.
639
+ *
640
+ * Visual knobs are typically 8px, but the hit region is 16px so users don't
641
+ * need pixel-perfect aim. Matches `MIN_HIT_SIZE` in the Rust overlay.
642
+ */
643
+ declare const MIN_HIT_SIZE = 16;
644
+ /**
645
+ * Below this selection size (in screen-px on either axis), chrome is
646
+ * suppressed — both the visual handles AND the hit regions. Matches
647
+ * `MIN_HANDLES_VISIBLE_SIZE` in the Rust overlay.
648
+ */
649
+ declare const MIN_CHROME_VISIBLE_SIZE = 12;
650
+ /**
651
+ * A hit region for one overlay element. Always screen-space; either anchored
652
+ * to a doc-space point (so the hit region tracks the document under camera
653
+ * changes) or expressed as a pre-projected screen-space AABB (for things
654
+ * like edge regions whose layout is fundamentally screen-space).
655
+ */
656
+ type HitShape =
657
+ /**
658
+ * Fixed screen-space rectangle, centered (or otherwise anchored) on a
659
+ * doc-space point. The surface projects `anchor_doc` through the current
660
+ * transform each frame; rect dimensions stay constant in CSS px.
661
+ */
662
+ {
663
+ kind: "screen_rect_at_doc";
664
+ anchor_doc: cmath.Vector2; /** Screen-space size in CSS px. */
665
+ width: number;
666
+ height: number; /** Which point of the rect sits on the anchor. Default: "center". */
667
+ placement?: "center" | "tl" | "tr" | "bl" | "br";
668
+ }
669
+ /**
670
+ * Screen-space AABB at pre-projected screen coordinates. Used for elements
671
+ * whose layout the chrome builder already projected (e.g. edge strips).
672
+ */
673
+ | {
674
+ kind: "screen_aabb";
675
+ rect: Rect;
676
+ };
677
+ /**
678
+ * Visual representation for one overlay element. Maps directly to the
679
+ * primitive layer's `HUDDraw` entries — the surface fans these out into the
680
+ * one merged `HUDDraw` it hands to `HUDCanvas.draw()` each frame.
681
+ *
682
+ * Virtual elements (rotation, side resize) omit `render`.
683
+ */
684
+ type RenderShape = /** Screen-space sized rect at doc anchor — e.g. resize knob. */{
685
+ kind: "screen_rect";
686
+ anchor_doc: cmath.Vector2;
687
+ width: number;
688
+ height: number;
689
+ placement?: "center" | "tl" | "tr" | "bl" | "br";
690
+ fill?: boolean;
691
+ stroke?: boolean;
692
+ fillColor?: string;
693
+ strokeColor?: string;
694
+ } /** Doc-space rect — e.g. selection outline, marquee. */ | {
695
+ kind: "doc_rect";
696
+ x: number;
697
+ y: number;
698
+ width: number;
699
+ height: number;
700
+ stroke?: boolean;
701
+ fill?: boolean;
702
+ fillOpacity?: number;
703
+ dashed?: boolean;
704
+ } /** Doc-space line — e.g. line-shape selection outline. */ | {
705
+ kind: "doc_line";
706
+ x1: number;
707
+ y1: number;
708
+ x2: number;
709
+ y2: number;
710
+ dashed?: boolean;
711
+ };
712
+ /**
713
+ * One interactable element of overlay UI.
714
+ *
715
+ * Pairs (visual, event, action) into a single struct so the discipline of
716
+ * "render box ≠ event box" is explicit. The chrome builder emits a list of
717
+ * these per frame; the surface fans them into render commands and hit
718
+ * regions.
719
+ *
720
+ * - **Virtual elements** (e.g. rotation handles, edge resize strips) omit
721
+ * `render` — they exist only as hit regions.
722
+ * - **Padded elements** have `hit` larger than the corresponding `render`
723
+ * shape (e.g. 16px hit AABB around an 8px visual knob).
724
+ * - **Cursor hover** is driven by the element's `cursor` field when the
725
+ * pointer enters its hit region.
726
+ */
727
+ interface OverlayElement {
728
+ action: OverlayAction;
729
+ hit: HitShape;
730
+ render?: RenderShape;
731
+ cursor?: CursorIcon;
732
+ }
733
+ //#endregion
734
+ export { HUDPolyline as A, marqueeToHUDDraw as C, HUDCanvasOptions as D, HUDCanvas as E, HUDRule as M, HUDScreenRect as N, HUDDraw as O, lassoToHUDDraw as S, snapGuideToHUDDraw as T, Modifiers as _, RenderShape as a, SurfaceEvent as b, HUDStyle as c, Intent as d, IntentPhase as f, RotationCorner as g, ResizeDirection as h, OverlayElement as i, HUDRect as j, HUDLine as k, SelectionGroup as l, CursorIcon as m, MIN_CHROME_VISIBLE_SIZE as n, Surface as o, SurfaceGesture as p, MIN_HIT_SIZE as r, SurfaceOptions as s, HitShape as t, SelectionShape as u, NO_MODS as v, measurementToHUDDraw as w, SurfaceResponse as x, PointerButton as y };