@grida/hud 0.1.0 → 0.2.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.
@@ -1,13 +1,19 @@
1
- import { Measurement } from "@grida/cmath/_measurement";
1
+ import { i as RotationCorner, n as CursorRenderer, r as ResizeDirection, t as CursorIcon } from "./cursor-CIYvFshz.js";
2
2
  import cmath from "@grida/cmath";
3
3
  import { guide } from "@grida/cmath/_snap";
4
+ import { Measurement } from "@grida/cmath/_measurement";
4
5
 
5
6
  //#region primitives/types.d.ts
7
+ type HUDSemanticGroup = string;
8
+ interface HUDSemantic {
9
+ /** Semantic owner of this primitive, used for group-level visibility policy. */
10
+ group?: HUDSemanticGroup;
11
+ }
6
12
  /**
7
13
  * A line segment in document space, extending `cmath.ui.Line` with
8
14
  * an optional `dashed` style.
9
15
  */
10
- interface HUDLine extends cmath.ui.Line {
16
+ interface HUDLine extends cmath.ui.Line, HUDSemantic {
11
17
  dashed?: boolean;
12
18
  /** Stroke width in screen-space CSS px. Defaults to the canvas default. */
13
19
  strokeWidth?: number;
@@ -16,6 +22,13 @@ interface HUDLine extends cmath.ui.Line {
16
22
  * back to the canvas's current color when absent.
17
23
  */
18
24
  color?: string;
25
+ /**
26
+ * Label rotation in radians (CCW) around the label pill's screen-space
27
+ * center. Defaults to `0`. Used to make the size meter pill rotate with
28
+ * a `SelectionShape.transformed` parent. Only affects the pill +
29
+ * text; the underlying line stroke is unaffected.
30
+ */
31
+ labelAngle?: number;
19
32
  }
20
33
  /**
21
34
  * A full-viewport axis-aligned line (infinite extent) at a given offset.
@@ -26,16 +39,37 @@ interface HUDLine extends cmath.ui.Line {
26
39
  *
27
40
  * Offset is in document space; the renderer projects it to screen.
28
41
  */
29
- interface HUDRule {
42
+ interface HUDRule extends HUDSemantic {
30
43
  axis: "x" | "y";
31
44
  offset: number;
45
+ /**
46
+ * Override the canvas color for this rule's stroke. Falls back to
47
+ * the canvas's current color when absent.
48
+ */
49
+ color?: string;
50
+ }
51
+ /**
52
+ * A single document-space point rendered as a small crosshair "X" on
53
+ * the canvas. The size of the crosshair is fixed in CSS pixels; the
54
+ * anchor (x, y) is in document space.
55
+ *
56
+ * Stroke uses the canvas color by default. Per-point override is opt-in.
57
+ */
58
+ interface HUDPoint extends HUDSemantic {
59
+ x: number;
60
+ y: number;
61
+ /**
62
+ * Override the canvas color for this point's crosshair stroke.
63
+ * Falls back to the canvas's current color when absent.
64
+ */
65
+ color?: string;
32
66
  }
33
67
  /**
34
68
  * A rectangle in document space.
35
69
  *
36
70
  * Stroke uses the canvas color by default. Fill is opt-in.
37
71
  */
38
- interface HUDRect {
72
+ interface HUDRect extends HUDSemantic {
39
73
  x: number;
40
74
  y: number;
41
75
  width: number;
@@ -61,7 +95,7 @@ interface HUDRect {
61
95
  *
62
96
  * Stroke uses the canvas color by default. Fill is opt-in.
63
97
  */
64
- interface HUDPolyline {
98
+ interface HUDPolyline extends HUDSemantic {
65
99
  points: cmath.Vector2[];
66
100
  /** Whether to stroke the path (default: true). */
67
101
  stroke?: boolean;
@@ -87,7 +121,7 @@ interface HUDPolyline {
87
121
  *
88
122
  * Default anchor is `"center"` (the doc-space point becomes the rect's center).
89
123
  */
90
- interface HUDScreenRect {
124
+ interface HUDScreenRect extends HUDSemantic {
91
125
  /** Document-space anchor point. */
92
126
  x: number;
93
127
  y: number;
@@ -102,6 +136,14 @@ interface HUDScreenRect {
102
136
  fillColor?: string;
103
137
  /** Override the canvas color for stroke. */
104
138
  strokeColor?: string;
139
+ /**
140
+ * Rotation in radians (CCW) around the rect's screen-space center.
141
+ * Defaults to `0`. Used to make handle knobs / size badges rotate
142
+ * together with a `SelectionShape.transformed` parent. Hit-testing is
143
+ * unaffected — render and hit live on independent shapes per the
144
+ * package's render/hit-test split (see README).
145
+ */
146
+ angle?: number;
105
147
  }
106
148
  /**
107
149
  * A complete set of draw commands for one frame.
@@ -113,13 +155,43 @@ interface HUDScreenRect {
113
155
  interface HUDDraw {
114
156
  lines?: HUDLine[];
115
157
  rules?: HUDRule[];
116
- points?: cmath.Vector2[];
158
+ points?: HUDPoint[];
117
159
  rects?: HUDRect[];
118
160
  polylines?: HUDPolyline[];
119
161
  /** Screen-space-sized rects anchored to document-space points (handles). */
120
162
  screenRects?: HUDScreenRect[];
121
163
  }
122
164
  //#endregion
165
+ //#region primitives/pixel-grid.d.ts
166
+ declare const DEFAULT_PIXEL_GRID_COLOR = "rgba(150, 150, 150, 0.15)";
167
+ declare const DEFAULT_PIXEL_GRID_STEPS: [number, number];
168
+ interface PixelGridConfig {
169
+ enabled: boolean;
170
+ /** Minimum `transform[0][0]` (uniform scale) at which the grid renders. */
171
+ zoomThreshold: number;
172
+ /**
173
+ * Optional camera transform used to space the grid. Hosts that drive the
174
+ * HUD canvas's own `setTransform` can omit this — the pixel grid falls
175
+ * back to the canvas's chrome transform. Hosts that keep the HUD at
176
+ * identity (e.g. `@grida/svg-editor`, which applies the camera as a CSS
177
+ * transform on the `<svg>` element) must supply this explicitly and
178
+ * update it on every camera change via `setPixelGridTransform`.
179
+ */
180
+ transform?: cmath.Transform;
181
+ color?: string;
182
+ steps?: [number, number];
183
+ }
184
+ interface DrawPixelGridParams {
185
+ ctx: CanvasRenderingContext2D;
186
+ transform: cmath.Transform;
187
+ width: number;
188
+ height: number;
189
+ dpr: number;
190
+ color?: string;
191
+ steps?: [number, number];
192
+ }
193
+ declare function drawPixelGrid(p: DrawPixelGridParams): void;
194
+ //#endregion
123
195
  //#region primitives/canvas.d.ts
124
196
  interface HUDCanvasOptions {
125
197
  color?: string;
@@ -143,10 +215,22 @@ declare class HUDCanvas {
143
215
  private color;
144
216
  private width;
145
217
  private height;
218
+ private pixelGrid;
146
219
  constructor(canvas: HTMLCanvasElement, options?: HUDCanvasOptions);
147
220
  setColor(color?: string): void;
148
221
  setSize(w: number, h: number): void;
149
222
  setTransform(transform: cmath.Transform): void;
223
+ /**
224
+ * Configure the back-most pixel-grid layer. Pass `null` to disable.
225
+ * Drawn before any HUD primitive, gated by `zoomThreshold`. See
226
+ * `PixelGridConfig.transform` for the two-transform contract.
227
+ */
228
+ setPixelGrid(config: PixelGridConfig | null): void;
229
+ /**
230
+ * Update only the pixel grid's transform, without replacing the rest of
231
+ * the config. Cheap to call per camera tick.
232
+ */
233
+ setPixelGridTransform(transform: cmath.Transform): void;
150
234
  /**
151
235
  * Clear the canvas and draw all primitives in `commands`.
152
236
  * Pass `undefined` to clear without drawing (e.g. when no overlay is active).
@@ -172,16 +256,29 @@ declare class HUDCanvas {
172
256
  private drawScreenRects;
173
257
  }
174
258
  //#endregion
259
+ //#region primitives/draw.d.ts
260
+ interface HUDGroupFilter {
261
+ hidden?: Iterable<HUDSemanticGroup>;
262
+ }
263
+ /**
264
+ * Filter a draw command list by semantic group.
265
+ *
266
+ * Ungrouped primitives are always kept. The function is intentionally shallow:
267
+ * primitives are immutable command objects on the hot draw path, so preserving
268
+ * object identity keeps this as a visibility pass rather than a rewrite.
269
+ */
270
+ declare function filterHUDDrawByGroup(draw: HUDDraw | undefined, filter: HUDGroupFilter): HUDDraw | undefined;
271
+ //#endregion
175
272
  //#region primitives/snap-guide.d.ts
176
273
  /**
177
274
  * Convert a `guide.SnapGuide` (the output of `guide.plot()`) into a
178
275
  * generic {@link HUDDraw} command list.
179
276
  *
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.
277
+ * `color`, when supplied, is applied as the per-item stroke override
278
+ * for every emitted line, rule, and point. When absent, the HUD
279
+ * canvas's current color is used.
183
280
  */
184
- declare function snapGuideToHUDDraw(sg: guide.SnapGuide | undefined): HUDDraw | undefined;
281
+ declare function snapGuideToHUDDraw(sg: guide.SnapGuide | undefined, color?: string): HUDDraw | undefined;
185
282
  //#endregion
186
283
  //#region primitives/measurement-guide.d.ts
187
284
  /**
@@ -281,19 +378,57 @@ interface SurfaceResponse {
281
378
  hoverChanged: boolean;
282
379
  }
283
380
  //#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;
381
+ //#region event/shape.d.ts
382
+ /**
383
+ * Selection shape what the chrome wraps.
384
+ *
385
+ * Hosts return a `SelectionShape` from `shapeOf(id)` so the HUD can lay out
386
+ * the right kind of chrome:
387
+ *
388
+ * - **`rect`** — standard bounding box. Renders selection outline + 4 corner
389
+ * knobs, with virtual edge / rotation hit regions.
390
+ * - **`transformed`** — an axis-aligned local bbox PLUS a 2×3 affine matrix
391
+ * mapping local-frame points into doc-space. Covers rotation, skew,
392
+ * non-uniform scale, and mirror with one code path. `local.width × local.height`
393
+ * are the artwork's true dims (read by the size badge and by `applyResize`
394
+ * in local space). Identity matrix here is byte-equivalent to
395
+ * `{ kind: "rect", rect: local }`.
396
+ * - **`line`** — two-endpoint primitive (e.g. SVG `<line>`). Renders the
397
+ * line segment + 2 endpoint knobs. No edge / rotation regions.
398
+ * - **`unresolved`** — internal-only. Used inside `SelectionGroup` when the
399
+ * host gave a flat `NodeId[]` to `setSelection` — the chrome builder
400
+ * resolves the real shape by calling `shapeOf(id)` at frame build time.
401
+ * Hosts never emit this; treating it as host-facing would be a bug.
402
+ *
403
+ * Future kinds (e.g. `polyline`, `ellipse_oriented`) can be added without
404
+ * breaking the existing kinds — the chrome builder branches on `kind`.
405
+ */
406
+ type SelectionShape = {
407
+ kind: "rect";
408
+ rect: Rect;
293
409
  } | {
294
- kind: "rotate";
295
- corner: RotationCorner;
410
+ kind: "transformed"; /** Local-frame AABB; width/height are artwork's true dims. */
411
+ local: Rect; /** 2×3 affine mapping local-frame points → doc-space. */
412
+ matrix: cmath.Transform;
413
+ } | {
414
+ kind: "line";
415
+ p1: cmath.Vector2;
416
+ p2: cmath.Vector2;
417
+ } | {
418
+ kind: "unresolved";
419
+ id: string;
296
420
  };
421
+ /**
422
+ * A logical selection group. The HUD renders one chrome instance per group.
423
+ *
424
+ * The host pre-computes `shape` (typically the union of member bounds for
425
+ * multi-node groups). Member `ids` are the gesture target — they all
426
+ * translate/resize together as a unit.
427
+ */
428
+ interface SelectionGroup {
429
+ ids: readonly NodeId[];
430
+ shape: SelectionShape;
431
+ }
297
432
  //#endregion
298
433
  //#region event/gesture.d.ts
299
434
  /**
@@ -333,10 +468,15 @@ type SurfaceGesture = {
333
468
  } | {
334
469
  kind: "resize"; /** Member ids of the group being resized (1 or more). */
335
470
  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;
471
+ direction: ResizeDirection;
472
+ /**
473
+ * Selection shape at gesture start. For `kind: "rect"` this is the
474
+ * doc-space bbox; for `kind: "transformed"` it carries the local-frame
475
+ * AABB + matrix so resize math runs in the rotated/skewed local frame.
476
+ */
477
+ initial_shape: SelectionShape; /** Anchor (pointer-down) in document-space. */
478
+ anchor_doc: cmath.Vector2; /** Current shape during the gesture. Same kind as `initial_shape`. */
479
+ current_shape: SelectionShape;
340
480
  } | {
341
481
  kind: "rotate";
342
482
  ids: NodeId[];
@@ -344,6 +484,14 @@ type SurfaceGesture = {
344
484
  center_doc: cmath.Vector2; /** Angle at gesture start (radians). */
345
485
  anchor_angle: number; /** Current angle (radians). */
346
486
  current_angle: number;
487
+ /**
488
+ * Selection's screen-space rotation at gesture start (radians).
489
+ * Composed with `current_angle - anchor_angle` each frame to set
490
+ * the rotate-cursor's `baseAngle` — so cursors on already-rotated
491
+ * selections continue tilting correctly mid-gesture instead of
492
+ * snapping back to 0 + delta.
493
+ */
494
+ initial_cursor_angle: number;
347
495
  } | {
348
496
  kind: "endpoint";
349
497
  id: NodeId;
@@ -384,8 +532,20 @@ type Intent = {
384
532
  } | {
385
533
  kind: "resize"; /** Member ids of the group being resized (1 or more). */
386
534
  ids: NodeId[];
387
- anchor: ResizeDirection; /** Target rect in document-space. */
535
+ anchor: ResizeDirection;
536
+ /**
537
+ * Target rect in document-space. For `transformed` selections this
538
+ * is the AABB of the new shape — preserved so axis-aligned hosts
539
+ * that ignore `shape` keep working unchanged.
540
+ */
388
541
  rect: Rect;
542
+ /**
543
+ * Full target shape. Present whenever the gesture produced one
544
+ * (which is always, post-Commit 2 of the affine-first plan).
545
+ * Hosts that handle rotated/sheared selections consume this
546
+ * directly; legacy hosts can read `rect` and ignore `shape`.
547
+ */
548
+ shape?: SelectionShape;
389
549
  phase: IntentPhase;
390
550
  } | {
391
551
  kind: "rotate"; /** Member ids of the group being rotated (typically 1). */
@@ -424,48 +584,6 @@ type IntentHandler = (intent: Intent) => void;
424
584
  */
425
585
  type Transform = cmath.Transform;
426
586
  //#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
587
  //#region surface/style.d.ts
470
588
  /**
471
589
  * HUD style — colors, sizes, and offsets for the surface-owned chrome.
@@ -491,92 +609,6 @@ interface HUDStyle {
491
609
  showRotationHandles: boolean;
492
610
  }
493
611
  //#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
612
  //#region event/hit-regions.d.ts
581
613
  /**
582
614
  * Action a UI hit-region triggers when clicked.
@@ -588,9 +620,12 @@ declare class Surface {
588
620
  *
589
621
  * - `select_node` — user clicked on a node-representative UI region.
590
622
  * - `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.
623
+ * edges). Carries the group's member ids and the group's initial
624
+ * `SelectionShape` — `rect` for axis-aligned groups, `transformed` for
625
+ * rotated/sheared groups (so resize math runs in the local frame).
592
626
  * - `rotate_handle` — one of 4 virtual rotation regions outside the group's
593
- * corners. Carries the group's initial rect for center math.
627
+ * corners. Carries the group's initial `SelectionShape` for center math
628
+ * (pivot = center of `shapeBounds(initial_shape)`).
594
629
  * - `endpoint_handle` — endpoint of a line-shape selection. Carries the
595
630
  * line's current p1/p2 so dragging is relative to a stable snapshot.
596
631
  * - `translate_handle` — body region covering a selection group's bbox.
@@ -606,22 +641,12 @@ type OverlayAction = {
606
641
  kind: "resize_handle";
607
642
  direction: ResizeDirection;
608
643
  ids: readonly NodeId[];
609
- initial_rect: {
610
- x: number;
611
- y: number;
612
- width: number;
613
- height: number;
614
- };
644
+ initial_shape: SelectionShape;
615
645
  } | {
616
646
  kind: "rotate_handle";
617
647
  corner: RotationCorner;
618
648
  ids: readonly NodeId[];
619
- initial_rect: {
620
- x: number;
621
- y: number;
622
- width: number;
623
- height: number;
624
- };
649
+ initial_shape: SelectionShape;
625
650
  } | {
626
651
  kind: "endpoint_handle";
627
652
  endpoint: "p1" | "p2";
@@ -673,6 +698,23 @@ type HitShape =
673
698
  | {
674
699
  kind: "screen_aabb";
675
700
  rect: Rect;
701
+ }
702
+ /**
703
+ * Oriented bounding-box hit shape — an axis-aligned `rect` in a *shadow*
704
+ * coordinate space, plus the affine that maps a screen-space pointer
705
+ * INTO that space. The hit-test pipeline applies `inverse_transform` to
706
+ * the pointer and then tests against `rect` with normal AABB containment.
707
+ *
708
+ * Used by transformed-chrome zones: the 9-slice runs in an axis-aligned
709
+ * shadow rect centered at the chrome's screen center, and a rotation
710
+ * (typically around the same center) maps shadow → screen. Storing the
711
+ * inverse here lets hit-test stay exact at any rotation/skew without
712
+ * inflating to an AABB-of-rotated-corners.
713
+ */
714
+ | {
715
+ kind: "screen_obb";
716
+ rect: Rect; /** screen → shadow. Applied to the pointer before AABB containment. */
717
+ inverse_transform: cmath.Transform;
676
718
  };
677
719
  /**
678
720
  * Visual representation for one overlay element. Maps directly to the
@@ -691,6 +733,12 @@ type RenderShape = /** Screen-space sized rect at doc anchor — e.g. resize kno
691
733
  stroke?: boolean;
692
734
  fillColor?: string;
693
735
  strokeColor?: string;
736
+ /**
737
+ * Rotation around the rect's screen-space center, in radians (CCW).
738
+ * Defaults to 0. Used for knobs/badges rendered for a transformed
739
+ * selection so they rotate with the parent.
740
+ */
741
+ angle?: number;
694
742
  } /** Doc-space rect — e.g. selection outline, marquee. */ | {
695
743
  kind: "doc_rect";
696
744
  x: number;
@@ -721,14 +769,179 @@ type RenderShape = /** Screen-space sized rect at doc anchor — e.g. resize kno
721
769
  * `render` — they exist only as hit regions.
722
770
  * - **Padded elements** have `hit` larger than the corresponding `render`
723
771
  * 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.
772
+ *
773
+ * TODO(rotation): when the first base-rotated overlay lands (e.g. an
774
+ * in-canvas rotation pip), add an `orientation: "screen" | "base"` field.
775
+ * Existing call sites default to `"screen"` and don't change.
726
776
  */
727
777
  interface OverlayElement {
778
+ /** Stable semantic identifier. Format: `"<kind>[:<param>]"`. Examples:
779
+ * `"translate"`, `"resize_handle:nw"`, `"resize_edge:n"`,
780
+ * `"rotate:ne"`, `"endpoint:p1"`. Used by tests and debug tooling. */
781
+ label: string;
782
+ /** Semantic owner for group-level visibility policy. */
783
+ group?: HUDSemanticGroup;
728
784
  action: OverlayAction;
729
785
  hit: HitShape;
730
786
  render?: RenderShape;
787
+ /** Lower wins. See `HUDHitPriority` in `selection-controls.ts`. */
788
+ priority: number;
731
789
  cursor?: CursorIcon;
732
790
  }
733
791
  //#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 };
792
+ //#region surface/chrome.d.ts
793
+ interface SurfaceChromeGroups {
794
+ hover?: HUDSemanticGroup;
795
+ selection?: HUDSemanticGroup;
796
+ selectionControls?: HUDSemanticGroup;
797
+ marquee?: HUDSemanticGroup;
798
+ transformPreview?: HUDSemanticGroup;
799
+ }
800
+ //#endregion
801
+ //#region surface/surface.d.ts
802
+ interface SurfaceVisibilityContext {
803
+ gesture: SurfaceGesture;
804
+ }
805
+ interface SurfaceVisibility {
806
+ hidden?: Iterable<HUDSemanticGroup>;
807
+ }
808
+ type SurfaceVisibilityPolicy = (context: SurfaceVisibilityContext) => SurfaceVisibility | undefined;
809
+ interface SurfaceOptions {
810
+ /**
811
+ * Content pick. Given a doc-space point, return the topmost node id under
812
+ * the pointer, or `null`. Host wraps its scene query however it wants
813
+ * (e.g. `elementFromPoint` + `data-id` for SVG-DOM hosts).
814
+ */
815
+ pick: (point_doc: [number, number]) => NodeId | null;
816
+ /**
817
+ * Selection shape for a node — what the chrome should wrap. Most nodes
818
+ * return `{ kind: "rect", rect }`; vector lines return
819
+ * `{ kind: "line", p1, p2 }`.
820
+ */
821
+ shapeOf: (id: NodeId) => SelectionShape | null;
822
+ /** Surface emits intents the host commits. */
823
+ onIntent: IntentHandler;
824
+ /** Initial style (partial; merged with defaults). */
825
+ style?: Partial<HUDStyle>;
826
+ /** Initial readonly flag. Default `false`. */
827
+ readonly?: boolean;
828
+ /** Optional HUDCanvas color override. */
829
+ color?: string;
830
+ /**
831
+ * Optional pixel-grid configuration. Drawn back-most in the HUD canvas
832
+ * when `enabled` and the current zoom exceeds `zoomThreshold`. Hosts can
833
+ * also call `surface.setPixelGrid(...)` later.
834
+ */
835
+ pixelGrid?: PixelGridConfig | null;
836
+ /**
837
+ * Optional semantic groups for surface-owned chrome. The HUD package does
838
+ * not define a group vocabulary; hosts pass the strings they want to use.
839
+ */
840
+ groups?: SurfaceChromeGroups;
841
+ /**
842
+ * Host-owned visibility policy. Called per frame with the current surface
843
+ * gesture; returned groups are filtered from surface chrome and host extras.
844
+ */
845
+ visibility?: SurfaceVisibilityPolicy;
846
+ }
847
+ /**
848
+ * Top-level wired surface.
849
+ *
850
+ * Owns an internal `HUDCanvas`, a `SurfaceState` (gesture/hover/...) and
851
+ * the host providers. On every `dispatch`, the state machine runs;
852
+ * `draw` composes surface chrome + host-fed extras into a single canvas
853
+ * paint.
854
+ */
855
+ declare class Surface {
856
+ private hudCanvas;
857
+ private state;
858
+ private style;
859
+ private opts;
860
+ private colorOverride;
861
+ private width;
862
+ private height;
863
+ private cursor_renderer;
864
+ constructor(canvas: HTMLCanvasElement, options: SurfaceOptions);
865
+ /** Configure / disable the back-most pixel-grid layer. */
866
+ setPixelGrid(config: PixelGridConfig | null): void;
867
+ /**
868
+ * Update just the pixel grid's transform. Cheap to call per camera tick.
869
+ * No-op when no pixel-grid config is set.
870
+ */
871
+ setPixelGridTransform(transform: Transform): void;
872
+ setSize(w: number, h: number): void;
873
+ setTransform(t: Transform): void;
874
+ /**
875
+ * Push a new selection from the host.
876
+ *
877
+ * Accepts either:
878
+ * - `NodeId[]` — each id becomes its own single-member group, shape
879
+ * resolved via `shapeOf(id)` by the chrome builder.
880
+ * - `SelectionGroup[]` — pre-computed groups with their union shape.
881
+ *
882
+ * See `SurfaceState.setSelection` for details.
883
+ */
884
+ setSelection(input: readonly NodeId[] | readonly SelectionGroup[]): void;
885
+ setStyle(partial: Partial<HUDStyle>): void;
886
+ /**
887
+ * Set or clear the host color override. `null` clears the override and
888
+ * lets `style.chromeColor` win on the next paint.
889
+ */
890
+ setColor(color: string | null): void;
891
+ setReadonly(v: boolean): void;
892
+ /**
893
+ * Set or clear a host-driven hover override.
894
+ *
895
+ * The surface tracks two hover sources:
896
+ * - **Pointer pick** — what scene content is under the cursor (updated
897
+ * automatically on `pointer_move`).
898
+ * - **Host override** — what the host wants to show as hovered, e.g.
899
+ * from a layers panel row mouseenter.
900
+ *
901
+ * The override (when non-null) wins. `hover()` returns the effective
902
+ * value; chrome renders the effective value. Pass `null` to clear and
903
+ * fall back to pointer pick.
904
+ *
905
+ * Returns the same response shape as `dispatch` so the host can react
906
+ * to whether anything actually changed.
907
+ */
908
+ setHoverOverride(id: NodeId | null): SurfaceResponse;
909
+ dispose(): void;
910
+ dispatch(event: SurfaceEvent): SurfaceResponse;
911
+ draw(extra?: HUDDraw): void;
912
+ /** Convenience: clear the canvas (e.g. when the host stops the surface). */
913
+ clear(): void;
914
+ gesture(): SurfaceGesture;
915
+ /**
916
+ * The effective hover: host override (when set) wins over pointer pick.
917
+ * Use this for chrome decisions and host-side reads.
918
+ */
919
+ hover(): NodeId | null;
920
+ cursor(): CursorIcon;
921
+ /**
922
+ * Resolve the current cursor to a CSS `cursor:` value. Runs the
923
+ * installed renderer (or the built-in `cursorToCss` if none installed).
924
+ *
925
+ * Host wires it like:
926
+ *
927
+ * const r = surface.dispatch(event);
928
+ * if (r.cursorChanged) el.style.cursor = surface.cursorCss();
929
+ *
930
+ * Saves the host from re-importing `cursorToCss` after every dispatch
931
+ * and gives one place to change behavior when a renderer is swapped in.
932
+ */
933
+ cursorCss(): string;
934
+ /**
935
+ * Install (or clear) a custom cursor renderer.
936
+ *
937
+ * `null` restores the built-in `cursorToCss` behavior (native CSS
938
+ * keywords for every variant). Pass `cursors.defaultRenderer()` from
939
+ * `@grida/hud/cursors` for the bundled SVG cursor set.
940
+ *
941
+ * Re-callable mid-session; the next `cursorCss()` reads the new value.
942
+ */
943
+ setCursorRenderer(fn: CursorRenderer | null): void;
944
+ modifiers(): Modifiers;
945
+ }
946
+ //#endregion
947
+ export { HUDCanvas as A, HUDRect as B, SurfaceEvent as C, measurementToHUDDraw as D, marqueeToHUDDraw as E, PixelGridConfig as F, HUDScreenRect as H, drawPixelGrid as I, HUDDraw as L, DEFAULT_PIXEL_GRID_COLOR as M, DEFAULT_PIXEL_GRID_STEPS as N, snapGuideToHUDDraw as O, DrawPixelGridParams as P, HUDLine as R, PointerButton as S, lassoToHUDDraw as T, HUDSemantic as U, HUDRule as V, HUDSemanticGroup as W, SurfaceGesture as _, SurfaceVisibilityPolicy as a, Modifiers as b, MIN_CHROME_VISIBLE_SIZE as c, RenderShape as d, HUDStyle as f, Rect as g, SelectMode as h, SurfaceVisibilityContext as i, HUDCanvasOptions as j, filterHUDDrawByGroup as k, MIN_HIT_SIZE as l, IntentPhase as m, SurfaceOptions as n, SurfaceChromeGroups as o, Intent as p, SurfaceVisibility as r, HitShape as s, Surface as t, OverlayElement as u, SelectionGroup as v, SurfaceResponse as w, NO_MODS as x, SelectionShape as y, HUDPolyline as z };