@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,291 @@
1
+ //#region core/event.ts
2
+ const NO_MODS = {
3
+ shift: false,
4
+ alt: false,
5
+ meta: false,
6
+ ctrl: false
7
+ };
8
+ //#endregion
9
+ //#region core/click-tracker.ts
10
+ var ClickTracker = class {
11
+ constructor(opts = {}) {
12
+ this.last_time = 0;
13
+ this.last_x = 0;
14
+ this.last_y = 0;
15
+ this.count = 0;
16
+ this.window_ms = opts.windowMs ?? 250;
17
+ this.distance_px = opts.distancePx ?? 5;
18
+ }
19
+ /**
20
+ * Register a click at `(x, y)` and return the current consecutive-
21
+ * click count (1 = single, 2 = double, etc.). `now` is in ms.
22
+ */
23
+ register(x, y, now = nowMs()) {
24
+ const dt = now - this.last_time;
25
+ const dx = x - this.last_x;
26
+ const dy = y - this.last_y;
27
+ const dist2 = dx * dx + dy * dy;
28
+ const max2 = this.distance_px * this.distance_px;
29
+ if (dt <= this.window_ms && dist2 <= max2 && this.count > 0) this.count += 1;
30
+ else this.count = 1;
31
+ this.last_time = now;
32
+ this.last_x = x;
33
+ this.last_y = y;
34
+ return this.count;
35
+ }
36
+ reset() {
37
+ this.count = 0;
38
+ this.last_time = 0;
39
+ }
40
+ };
41
+ function nowMs() {
42
+ if (typeof performance !== "undefined" && performance.now) return performance.now();
43
+ return Date.now();
44
+ }
45
+ //#endregion
46
+ //#region core/transform.ts
47
+ const IDENTITY = [[
48
+ 1,
49
+ 0,
50
+ 0
51
+ ], [
52
+ 0,
53
+ 1,
54
+ 0
55
+ ]];
56
+ /** Project a screen-space point into document-space. */
57
+ function screenToDoc(t, x, y) {
58
+ const [[sx, , tx], [, sy, ty]] = t;
59
+ return [(x - tx) / (sx || 1), (y - ty) / (sy || 1)];
60
+ }
61
+ /** Project a document-space point into screen-space. */
62
+ function docToScreen(t, x, y) {
63
+ const [[sx, , tx], [, sy, ty]] = t;
64
+ return [sx * x + tx, sy * y + ty];
65
+ }
66
+ /** Current uniform zoom. Reads `sx`. */
67
+ function zoomOf(t) {
68
+ return t[0][0];
69
+ }
70
+ //#endregion
71
+ //#region core/registry.ts
72
+ /**
73
+ * Thrown by `register` when an entity with the same `name` is already
74
+ * registered. Consumers should `unregister` the existing one first.
75
+ */
76
+ var RegistrationError = class extends Error {
77
+ constructor(message) {
78
+ super(message);
79
+ this.name = "RegistrationError";
80
+ }
81
+ };
82
+ /**
83
+ * Generic name-keyed registry.
84
+ *
85
+ * @typeParam K — key type, must extend `string`.
86
+ * @typeParam T — entity type. Must expose `readonly name: K` and an
87
+ * optional `detach()` method.
88
+ */
89
+ var NamedRegistry = class {
90
+ constructor(label) {
91
+ this.label = label;
92
+ this.byName = /* @__PURE__ */ new Map();
93
+ this.order = [];
94
+ }
95
+ register(e) {
96
+ if (this.byName.has(e.name)) throw new RegistrationError(`${this.label} "${e.name}" already registered; unregister it first.`);
97
+ this.byName.set(e.name, e);
98
+ this.order.push(e);
99
+ }
100
+ unregister(e) {
101
+ if (this.byName.get(e.name) !== e) return;
102
+ this.byName.delete(e.name);
103
+ const i = this.order.indexOf(e);
104
+ if (i >= 0) this.order.splice(i, 1);
105
+ try {
106
+ e.detach?.();
107
+ } catch (err) {
108
+ console.error(`[hud] ${this.label} "${e.name}" detach() threw:`, err);
109
+ }
110
+ }
111
+ has(name) {
112
+ return this.byName.has(name);
113
+ }
114
+ get(name) {
115
+ return this.byName.get(name);
116
+ }
117
+ *entries() {
118
+ for (const e of this.order) yield e;
119
+ }
120
+ size() {
121
+ return this.order.length;
122
+ }
123
+ clear() {
124
+ for (let i = this.order.length - 1; i >= 0; i--) {
125
+ const e = this.order[i];
126
+ try {
127
+ e.detach?.();
128
+ } catch (err) {
129
+ console.error(`[hud] ${this.label} "${e.name}" detach() threw:`, err);
130
+ }
131
+ }
132
+ this.order = [];
133
+ this.byName.clear();
134
+ }
135
+ };
136
+ //#endregion
137
+ //#region core/hit-registry.ts
138
+ /**
139
+ * Generic hit registry. One per frame (or persistent — bedrock is
140
+ * agnostic). Add objects with `add`, clear with `clear`, query with
141
+ * `queryPoint` / `queryAll`.
142
+ */
143
+ var HitRegistry = class {
144
+ constructor() {
145
+ this.items = [];
146
+ }
147
+ /**
148
+ * Add an object. Objects without `hit` are accepted (they may carry
149
+ * paint information the consumer still wants in the registry for
150
+ * uniform iteration) but are filtered from point queries.
151
+ */
152
+ add(obj) {
153
+ this.items.push(obj);
154
+ }
155
+ /** Drop all entries. */
156
+ clear() {
157
+ this.items = [];
158
+ }
159
+ /** Total number of stored objects (paint-only + hit-testable). */
160
+ size() {
161
+ return this.items.length;
162
+ }
163
+ /** Iterate every stored object in insertion order. */
164
+ *entries() {
165
+ for (const o of this.items) yield o;
166
+ }
167
+ /**
168
+ * Return the hit-testable object whose hit shape contains the
169
+ * given screen-space point with the lowest `priority` value
170
+ * (lower wins). Paint-only objects (no `hit`) are skipped.
171
+ */
172
+ queryPoint(point_screen, transform) {
173
+ let best = null;
174
+ let best_priority = Number.POSITIVE_INFINITY;
175
+ for (const obj of this.items) {
176
+ if (!obj.hit) continue;
177
+ if (obj.priority > best_priority) continue;
178
+ if (!shapeContains(obj.hit, point_screen, transform)) continue;
179
+ if (obj.refine && !obj.refine(point_screen)) continue;
180
+ best = obj;
181
+ best_priority = obj.priority;
182
+ }
183
+ return best;
184
+ }
185
+ /**
186
+ * Return all hit-testable objects whose hit shape contains the point,
187
+ * winner first. Ordering is identical to `queryPoint`'s arbitration:
188
+ * priority ascending (lower wins), and on EQUAL priority the
189
+ * later-added object comes first ("later push wins on tie"). This makes
190
+ * `queryAll(p)[0]` always equal `queryPoint(p)` — the two query paths
191
+ * never disagree on the winner.
192
+ */
193
+ queryAll(point_screen, transform) {
194
+ const matches = [];
195
+ for (let i = 0; i < this.items.length; i++) {
196
+ const obj = this.items[i];
197
+ if (!obj.hit) continue;
198
+ if (!shapeContains(obj.hit, point_screen, transform)) continue;
199
+ if (obj.refine && !obj.refine(point_screen)) continue;
200
+ matches.push({
201
+ obj,
202
+ i
203
+ });
204
+ }
205
+ matches.sort((a, b) => a.obj.priority - b.obj.priority || b.i - a.i);
206
+ return matches.map((m) => m.obj);
207
+ }
208
+ };
209
+ /**
210
+ * Test whether a screen-space point lies inside a `HitShape`.
211
+ *
212
+ * The camera `transform` is needed for doc-anchored shapes
213
+ * (`screen_rect_at_doc`, `screen_circle_at_doc`); pre-projected
214
+ * shapes (`screen_aabb`, `screen_polygon`) ignore it. `screen_obb`
215
+ * applies its own `inverse_transform`, which maps screen → shadow
216
+ * (independent of the camera).
217
+ */
218
+ function shapeContains(shape, point_screen, transform) {
219
+ const [px, py] = point_screen;
220
+ switch (shape.kind) {
221
+ case "screen_rect_at_doc": {
222
+ const [ax, ay] = docToScreen(transform, shape.anchor_doc[0], shape.anchor_doc[1]);
223
+ const { x, y } = anchorOrigin(ax, ay, shape.width, shape.height, shape.placement ?? "center");
224
+ return px >= x && px <= x + shape.width && py >= y && py <= y + shape.height;
225
+ }
226
+ case "screen_aabb": {
227
+ const r = shape.rect;
228
+ return px >= r.x && px <= r.x + r.width && py >= r.y && py <= r.y + r.height;
229
+ }
230
+ case "screen_obb": {
231
+ const [[a, b, e], [c, d, f]] = shape.inverse_transform;
232
+ const sx = a * px + b * py + e;
233
+ const sy = c * px + d * py + f;
234
+ const r = shape.rect;
235
+ return sx >= r.x && sx <= r.x + r.width && sy >= r.y && sy <= r.y + r.height;
236
+ }
237
+ case "screen_circle_at_doc": {
238
+ const [ax, ay] = docToScreen(transform, shape.anchor_doc[0], shape.anchor_doc[1]);
239
+ const dx = px - ax;
240
+ const dy = py - ay;
241
+ return dx * dx + dy * dy <= shape.radius * shape.radius;
242
+ }
243
+ case "screen_polygon": return pointInPolygon(point_screen, shape.points);
244
+ }
245
+ }
246
+ function anchorOrigin(ax, ay, w, h, placement) {
247
+ switch (placement) {
248
+ case "center": return {
249
+ x: ax - w / 2,
250
+ y: ay - h / 2
251
+ };
252
+ case "tl": return {
253
+ x: ax,
254
+ y: ay
255
+ };
256
+ case "tr": return {
257
+ x: ax - w,
258
+ y: ay
259
+ };
260
+ case "bl": return {
261
+ x: ax,
262
+ y: ay - h
263
+ };
264
+ case "br": return {
265
+ x: ax - w,
266
+ y: ay - h
267
+ };
268
+ }
269
+ }
270
+ /**
271
+ * Standard even-odd ray-cast point-in-polygon.
272
+ *
273
+ * Horizontal edges (`yi === yj`) are skipped — the standard idiom for
274
+ * the even-odd algorithm. (The previous form used `(yj - yi ||
275
+ * Number.EPSILON)` as a divide-by-zero guard, but the guard was dead
276
+ * code: the `yi > py !== yj > py` clause already short-circuits to
277
+ * `false` when the edge is horizontal, so the division never runs.)
278
+ */
279
+ function pointInPolygon(point, poly) {
280
+ const [px, py] = point;
281
+ let inside = false;
282
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
283
+ const [xi, yi] = poly[i];
284
+ const [xj, yj] = poly[j];
285
+ if (yi === yj) continue;
286
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) inside = !inside;
287
+ }
288
+ return inside;
289
+ }
290
+ //#endregion
291
+ export { ClickTracker, HitRegistry, IDENTITY, NO_MODS, NamedRegistry, RegistrationError, docToScreen, screenToDoc, shapeContains, zoomOf };
@@ -0,0 +1,64 @@
1
+ //#region event/cursor.d.ts
2
+ /** 8 cardinal/diagonal resize directions. */
3
+ type ResizeDirection = "n" | "ne" | "e" | "se" | "s" | "sw" | "w" | "nw";
4
+ /** 4 corner positions for rotation handles. */
5
+ type RotationCorner = "nw" | "ne" | "se" | "sw";
6
+ /**
7
+ * Logical cursor icon names — the host maps these to CSS `cursor` values.
8
+ *
9
+ * `baseAngle` (radians) tilts the rendered arrow by an additional amount
10
+ * on top of the variant's canonical orientation. Set automatically by the
11
+ * surface for both variants:
12
+ *
13
+ * - **rotate**: `decideIdleCursor` reads the action's `initial_shape`
14
+ * and sets `baseAngle` to the matrix's screen-space rotation; the
15
+ * in-gesture `pointer_move` arm composes `initial_cursor_angle + delta`
16
+ * so the cursor tracks the live rotation, starting from the selection's
17
+ * pre-gesture orientation rather than 0.
18
+ * - **resize**: `decideIdleCursor` reads the action's `initial_shape`
19
+ * and sets `baseAngle` to the matrix's screen-space rotation, so the
20
+ * resize cursor on a rotated selection tilts to align with the rotated
21
+ * edge / corner.
22
+ *
23
+ * For `kind: "rect"` selections (the common case) `baseAngle` resolves
24
+ * to 0 and the cursor behaves identically to the pre-rotation builds.
25
+ */
26
+ type CursorIcon = "default" | "pointer" | "move" | "crosshair" | "grab" | "grabbing" | "text" | {
27
+ kind: "resize";
28
+ direction: ResizeDirection;
29
+ baseAngle?: number;
30
+ } | {
31
+ kind: "rotate";
32
+ corner: RotationCorner;
33
+ baseAngle?: number;
34
+ };
35
+ /**
36
+ * Pluggable cursor renderer.
37
+ *
38
+ * Maps a logical `CursorIcon` to a complete CSS `cursor:` value
39
+ * (e.g. a native keyword like `"crosshair"` or a data-URL form like
40
+ * `"url(data:...) 12 12, auto"`).
41
+ *
42
+ * The Surface owns one slot. Default = the built-in `cursorToCss` below.
43
+ * Hosts opt into the bundled SVG renderer via
44
+ * `surface.setCursorRenderer(cursors.defaultRenderer())` from
45
+ * `@grida/hud/cursors`, or supply their own function for full control.
46
+ */
47
+ type CursorRenderer = (icon: CursorIcon) => string;
48
+ /**
49
+ * Map a `CursorIcon` to the standard CSS `cursor` value. Hosts can override
50
+ * for custom cursors.
51
+ */
52
+ declare function cursorToCss(c: CursorIcon): string;
53
+ /**
54
+ * Cursor-equality used to detect changes without re-emitting `cursorChanged`.
55
+ *
56
+ * Two rotate/resize icons compare equal only when both the
57
+ * discriminator (corner / direction) AND the bucketed `baseAngle` match.
58
+ * This is what lets the state machine call `setCursor` every frame
59
+ * during a rotate gesture and have `cursorChanged` fire only on real
60
+ * angle changes (≥ `CURSOR_ANGLE_BUCKET_RAD` of motion).
61
+ */
62
+ declare function cursorEquals(a: CursorIcon, b: CursorIcon): boolean;
63
+ //#endregion
64
+ export { cursorEquals as a, RotationCorner as i, CursorRenderer as n, cursorToCss as o, ResizeDirection as r, CursorIcon as t };
@@ -0,0 +1,64 @@
1
+ //#region event/cursor.d.ts
2
+ /** 8 cardinal/diagonal resize directions. */
3
+ type ResizeDirection = "n" | "ne" | "e" | "se" | "s" | "sw" | "w" | "nw";
4
+ /** 4 corner positions for rotation handles. */
5
+ type RotationCorner = "nw" | "ne" | "se" | "sw";
6
+ /**
7
+ * Logical cursor icon names — the host maps these to CSS `cursor` values.
8
+ *
9
+ * `baseAngle` (radians) tilts the rendered arrow by an additional amount
10
+ * on top of the variant's canonical orientation. Set automatically by the
11
+ * surface for both variants:
12
+ *
13
+ * - **rotate**: `decideIdleCursor` reads the action's `initial_shape`
14
+ * and sets `baseAngle` to the matrix's screen-space rotation; the
15
+ * in-gesture `pointer_move` arm composes `initial_cursor_angle + delta`
16
+ * so the cursor tracks the live rotation, starting from the selection's
17
+ * pre-gesture orientation rather than 0.
18
+ * - **resize**: `decideIdleCursor` reads the action's `initial_shape`
19
+ * and sets `baseAngle` to the matrix's screen-space rotation, so the
20
+ * resize cursor on a rotated selection tilts to align with the rotated
21
+ * edge / corner.
22
+ *
23
+ * For `kind: "rect"` selections (the common case) `baseAngle` resolves
24
+ * to 0 and the cursor behaves identically to the pre-rotation builds.
25
+ */
26
+ type CursorIcon = "default" | "pointer" | "move" | "crosshair" | "grab" | "grabbing" | "text" | {
27
+ kind: "resize";
28
+ direction: ResizeDirection;
29
+ baseAngle?: number;
30
+ } | {
31
+ kind: "rotate";
32
+ corner: RotationCorner;
33
+ baseAngle?: number;
34
+ };
35
+ /**
36
+ * Pluggable cursor renderer.
37
+ *
38
+ * Maps a logical `CursorIcon` to a complete CSS `cursor:` value
39
+ * (e.g. a native keyword like `"crosshair"` or a data-URL form like
40
+ * `"url(data:...) 12 12, auto"`).
41
+ *
42
+ * The Surface owns one slot. Default = the built-in `cursorToCss` below.
43
+ * Hosts opt into the bundled SVG renderer via
44
+ * `surface.setCursorRenderer(cursors.defaultRenderer())` from
45
+ * `@grida/hud/cursors`, or supply their own function for full control.
46
+ */
47
+ type CursorRenderer = (icon: CursorIcon) => string;
48
+ /**
49
+ * Map a `CursorIcon` to the standard CSS `cursor` value. Hosts can override
50
+ * for custom cursors.
51
+ */
52
+ declare function cursorToCss(c: CursorIcon): string;
53
+ /**
54
+ * Cursor-equality used to detect changes without re-emitting `cursorChanged`.
55
+ *
56
+ * Two rotate/resize icons compare equal only when both the
57
+ * discriminator (corner / direction) AND the bucketed `baseAngle` match.
58
+ * This is what lets the state machine call `setCursor` every frame
59
+ * during a rotate gesture and have `cursorChanged` fire only on real
60
+ * angle changes (≥ `CURSOR_ANGLE_BUCKET_RAD` of motion).
61
+ */
62
+ declare function cursorEquals(a: CursorIcon, b: CursorIcon): boolean;
63
+ //#endregion
64
+ export { cursorEquals as a, RotationCorner as i, CursorRenderer as n, cursorToCss as o, ResizeDirection as r, CursorIcon as t };
@@ -0,0 +1,57 @@
1
+ //#region event/cursor.ts
2
+ /**
3
+ * Angle-comparison bucket — radians. 0.5° in radians. Two icons are
4
+ * considered equal when their `baseAngle`s round to the same bucket;
5
+ * this is what prevents unnecessary `cursorChanged` re-emit during a
6
+ * smooth rotate gesture, so the host repaints only on real motion.
7
+ *
8
+ * 0.5° is below human perception for cursor orientation, so the
9
+ * quantization is visually free.
10
+ */
11
+ const CURSOR_ANGLE_BUCKET_RAD = Math.PI / 360;
12
+ /**
13
+ * Quantize an angle (radians) to its `CURSOR_ANGLE_BUCKET_RAD` bucket.
14
+ * Used by `cursorEquals` to decide whether two cursors with the same
15
+ * variant differ visibly.
16
+ */
17
+ function angleBucket(rad) {
18
+ if (rad === void 0 || rad === 0) return 0;
19
+ return Math.round(rad / CURSOR_ANGLE_BUCKET_RAD);
20
+ }
21
+ /**
22
+ * Map a `CursorIcon` to the standard CSS `cursor` value. Hosts can override
23
+ * for custom cursors.
24
+ */
25
+ function cursorToCss(c) {
26
+ if (typeof c === "string") switch (c) {
27
+ case "default": return "default";
28
+ case "pointer": return "pointer";
29
+ case "move": return "move";
30
+ case "crosshair": return "crosshair";
31
+ case "grab": return "grab";
32
+ case "grabbing": return "grabbing";
33
+ case "text": return "text";
34
+ }
35
+ if (c.kind === "resize") return `${c.direction}-resize`;
36
+ return "crosshair";
37
+ }
38
+ /**
39
+ * Cursor-equality used to detect changes without re-emitting `cursorChanged`.
40
+ *
41
+ * Two rotate/resize icons compare equal only when both the
42
+ * discriminator (corner / direction) AND the bucketed `baseAngle` match.
43
+ * This is what lets the state machine call `setCursor` every frame
44
+ * during a rotate gesture and have `cursorChanged` fire only on real
45
+ * angle changes (≥ `CURSOR_ANGLE_BUCKET_RAD` of motion).
46
+ */
47
+ function cursorEquals(a, b) {
48
+ if (typeof a === "string" && typeof b === "string") return a === b;
49
+ if (typeof a !== "string" && typeof b !== "string") {
50
+ if (a.kind === "resize" && b.kind === "resize") return a.direction === b.direction && angleBucket(a.baseAngle) === angleBucket(b.baseAngle);
51
+ if (a.kind === "rotate" && b.kind === "rotate") return a.corner === b.corner && angleBucket(a.baseAngle) === angleBucket(b.baseAngle);
52
+ return false;
53
+ }
54
+ return false;
55
+ }
56
+ //#endregion
57
+ export { cursorToCss as i, angleBucket as n, cursorEquals as r, CURSOR_ANGLE_BUCKET_RAD as t };
@@ -0,0 +1,80 @@
1
+ //#region event/cursor.ts
2
+ /**
3
+ * Angle-comparison bucket — radians. 0.5° in radians. Two icons are
4
+ * considered equal when their `baseAngle`s round to the same bucket;
5
+ * this is what prevents unnecessary `cursorChanged` re-emit during a
6
+ * smooth rotate gesture, so the host repaints only on real motion.
7
+ *
8
+ * 0.5° is below human perception for cursor orientation, so the
9
+ * quantization is visually free.
10
+ */
11
+ const CURSOR_ANGLE_BUCKET_RAD = Math.PI / 360;
12
+ /**
13
+ * Quantize an angle (radians) to its `CURSOR_ANGLE_BUCKET_RAD` bucket.
14
+ * Used by `cursorEquals` to decide whether two cursors with the same
15
+ * variant differ visibly.
16
+ */
17
+ function angleBucket(rad) {
18
+ if (rad === void 0 || rad === 0) return 0;
19
+ return Math.round(rad / CURSOR_ANGLE_BUCKET_RAD);
20
+ }
21
+ /**
22
+ * Map a `CursorIcon` to the standard CSS `cursor` value. Hosts can override
23
+ * for custom cursors.
24
+ */
25
+ function cursorToCss(c) {
26
+ if (typeof c === "string") switch (c) {
27
+ case "default": return "default";
28
+ case "pointer": return "pointer";
29
+ case "move": return "move";
30
+ case "crosshair": return "crosshair";
31
+ case "grab": return "grab";
32
+ case "grabbing": return "grabbing";
33
+ case "text": return "text";
34
+ }
35
+ if (c.kind === "resize") return `${c.direction}-resize`;
36
+ return "crosshair";
37
+ }
38
+ /**
39
+ * Cursor-equality used to detect changes without re-emitting `cursorChanged`.
40
+ *
41
+ * Two rotate/resize icons compare equal only when both the
42
+ * discriminator (corner / direction) AND the bucketed `baseAngle` match.
43
+ * This is what lets the state machine call `setCursor` every frame
44
+ * during a rotate gesture and have `cursorChanged` fire only on real
45
+ * angle changes (≥ `CURSOR_ANGLE_BUCKET_RAD` of motion).
46
+ */
47
+ function cursorEquals(a, b) {
48
+ if (typeof a === "string" && typeof b === "string") return a === b;
49
+ if (typeof a !== "string" && typeof b !== "string") {
50
+ if (a.kind === "resize" && b.kind === "resize") return a.direction === b.direction && angleBucket(a.baseAngle) === angleBucket(b.baseAngle);
51
+ if (a.kind === "rotate" && b.kind === "rotate") return a.corner === b.corner && angleBucket(a.baseAngle) === angleBucket(b.baseAngle);
52
+ return false;
53
+ }
54
+ return false;
55
+ }
56
+ //#endregion
57
+ Object.defineProperty(exports, "CURSOR_ANGLE_BUCKET_RAD", {
58
+ enumerable: true,
59
+ get: function() {
60
+ return CURSOR_ANGLE_BUCKET_RAD;
61
+ }
62
+ });
63
+ Object.defineProperty(exports, "angleBucket", {
64
+ enumerable: true,
65
+ get: function() {
66
+ return angleBucket;
67
+ }
68
+ });
69
+ Object.defineProperty(exports, "cursorEquals", {
70
+ enumerable: true,
71
+ get: function() {
72
+ return cursorEquals;
73
+ }
74
+ });
75
+ Object.defineProperty(exports, "cursorToCss", {
76
+ enumerable: true,
77
+ get: function() {
78
+ return cursorToCss;
79
+ }
80
+ });
@@ -0,0 +1,98 @@
1
+ import { n as CursorRenderer } from "../cursor-CxS8EMvm.mjs";
2
+
3
+ //#region cursors/renderer.d.ts
4
+ /**
5
+ * Build the default cursor renderer.
6
+ *
7
+ * Stateless — every call regenerates the data URL from scratch. This is
8
+ * cheap (`template_*` is a string-concat, `btoa` of ~600 B is
9
+ * sub-millisecond) AND the upstream `SurfaceState.setCursor` already
10
+ * gates re-emit via `cursorEquals`, which buckets `baseAngle` to
11
+ * `CURSOR_ANGLE_BUCKET_RAD`. Net result: this function is invoked at
12
+ * most once per real bucket change — caching here would be redundant
13
+ * and, sized wrong (the previous 64-entry LRU thrashed after ~32° of
14
+ * drag), actively harmful.
15
+ *
16
+ * Hosts install via `surface.setCursorRenderer(...)`.
17
+ *
18
+ * @example
19
+ * import { cursors } from "@grida/hud/cursors";
20
+ * surface.setCursorRenderer(cursors.defaultRenderer());
21
+ */
22
+ declare function defaultRenderer(): CursorRenderer;
23
+ //#endregion
24
+ //#region cursors/encode.d.ts
25
+ /**
26
+ * SVG → `data:` URL encoding for CSS `cursor:` values.
27
+ *
28
+ * Uses base64 (`btoa`) for two reasons:
29
+ * - Cross-browser support is universal; URL-encoded SVG has historically had
30
+ * parser inconsistencies on `#` and `<`.
31
+ * - It's what the main editor's `cursor-data.ts` uses, so cursor visuals
32
+ * compare byte-identical when we A/B during the eventual migration.
33
+ *
34
+ * Browser-only — `btoa` is part of the WHATWG HTML spec, available on
35
+ * Worker / Window. The package's `platform: "neutral"` tsdown config means
36
+ * we don't pull a Node polyfill; consumers using SSR must avoid evaluating
37
+ * the renderer on the server (the Surface itself is client-side).
38
+ */
39
+ /** Encode an SVG string as `data:image/svg+xml;base64,...`. */
40
+ declare function svgDataUrl(svg: string): string;
41
+ //#endregion
42
+ //#region cursors/templates.d.ts
43
+ /**
44
+ * SVG cursor templates — pure string templates parameterized by angle.
45
+ *
46
+ * Two families:
47
+ *
48
+ * 1. **Rotate arrow** — a curved double-arrow. Built from one template
49
+ * (`template_rotate`) rotated by `angle_deg` around the SVG's
50
+ * hotspot. Per-corner orientation comes from caller-supplied
51
+ * initial offsets (NW: −45°, NE: +45°, SW: −135°, SE: +135°), in
52
+ * Phase A. In Phase B, the host's selection rotation is added on
53
+ * top of that.
54
+ *
55
+ * 2. **Resize arrow** — a straight double-arrow, also built from one
56
+ * template (`template_resize`) rotated per cardinal direction. The
57
+ * 8 standard directions (N/NE/E/SE/S/SW/W/NW) map to multiples of
58
+ * 45° from the canonical horizontal arrow.
59
+ *
60
+ * **Fixed Grida palette** — black fill, white stroke. Cursors are not
61
+ * color-themed; hosts wanting custom colors install a custom
62
+ * `CursorRenderer` (see `renderer.ts`).
63
+ *
64
+ * **Hotspots** are documented per template — the click point inside
65
+ * the SVG that the OS aligns with the actual pointer position. Hotspots
66
+ * are fixed (SVG center) because the SVG rotates around the same point;
67
+ * the cursor stays anchored to the pointer regardless of angle.
68
+ */
69
+ /** Rotate cursor SVG. `angle_deg` rotates the arrow around (13, 12). */
70
+ declare function template_rotate(angle_deg: number): string;
71
+ /**
72
+ * Resize cursor SVG. `angle_deg` rotates the arrow around (16, 16).
73
+ *
74
+ * For the canonical horizontal arrow pass `0`. For the 8 standard
75
+ * cardinal directions, see `DIRECTION_ANGLE_DEG` in `renderer.ts`.
76
+ */
77
+ declare function template_resize(angle_deg: number): string;
78
+ //#endregion
79
+ //#region cursors/index.d.ts
80
+ /**
81
+ * Convenience namespace export. All cursor utilities are also available
82
+ * as named imports — but `cursors.defaultRenderer()` reads more cleanly
83
+ * at host call sites, mirroring the main editor's `cursors.*` style.
84
+ */
85
+ declare const cursors: {
86
+ readonly defaultRenderer: typeof defaultRenderer;
87
+ readonly svgDataUrl: typeof svgDataUrl;
88
+ readonly templates: {
89
+ readonly rotate: typeof template_rotate;
90
+ readonly resize: typeof template_resize;
91
+ };
92
+ readonly hotspots: {
93
+ readonly rotate: readonly [number, number];
94
+ readonly resize: readonly [number, number];
95
+ };
96
+ };
97
+ //#endregion
98
+ export { type CursorRenderer, cursors, defaultRenderer, svgDataUrl };