@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.
- package/README.md +657 -28
- package/dist/core/index.d.mts +181 -0
- package/dist/core/index.d.ts +181 -0
- package/dist/core/index.js +301 -0
- package/dist/core/index.mjs +291 -0
- package/dist/cursor-CxS8EMvm.d.mts +64 -0
- package/dist/cursor-CxS8EMvm.d.ts +64 -0
- package/dist/cursor-DW-uAPVE.mjs +57 -0
- package/dist/cursor-FGiJBdU-.js +80 -0
- package/dist/cursors/index.d.mts +98 -0
- package/dist/cursors/index.d.ts +98 -0
- package/dist/cursors/index.js +188 -0
- package/dist/cursors/index.mjs +185 -0
- package/dist/index-BrfEdWbQ.d.ts +3140 -0
- package/dist/index-Cmbe2X5b.d.mts +3140 -0
- package/dist/index.d.mts +107 -2
- package/dist/index.d.ts +107 -2
- package/dist/index.js +66 -1
- package/dist/index.mjs +3 -2
- package/dist/overlay-CVV4s3IL.d.ts +241 -0
- package/dist/overlay-dsG32baA.d.mts +241 -0
- package/dist/primitives/bedrock.d.mts +47 -0
- package/dist/primitives/bedrock.d.ts +47 -0
- package/dist/primitives/bedrock.js +71 -0
- package/dist/primitives/bedrock.mjs +65 -0
- package/dist/react.d.mts +3 -2
- package/dist/react.d.ts +2 -1
- package/dist/react.js +8 -3
- package/dist/react.mjs +8 -3
- package/dist/surface-BHQVvRFC.js +7356 -0
- package/dist/surface-NHSzUR8r.mjs +6902 -0
- package/dist/types-3wwFisZs.d.mts +296 -0
- package/dist/types-3wwFisZs.d.ts +296 -0
- package/package.json +18 -2
- package/dist/index-CBqCh-ZM.d.mts +0 -734
- package/dist/index-DRBeSiI2.d.ts +0 -734
- package/dist/surface-CNlBaEXn.js +0 -1890
- package/dist/surface-hUEeEVdL.mjs +0 -1808
|
@@ -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 };
|