@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,181 @@
1
+ import { i as HitShape, t as HUDObject } from "../overlay-dsG32baA.mjs";
2
+ import cmath from "@grida/cmath";
3
+
4
+ //#region core/event.d.ts
5
+ /** Modifier-key snapshot at the moment an event was produced. */
6
+ interface Modifiers {
7
+ readonly shift: boolean;
8
+ readonly alt: boolean;
9
+ readonly meta: boolean;
10
+ readonly ctrl: boolean;
11
+ }
12
+ declare const NO_MODS: Modifiers;
13
+ type PointerButton = "primary" | "secondary" | "middle";
14
+ /**
15
+ * Synthesized input event consumed by HUD bedrock.
16
+ *
17
+ * All coordinates are **screen-space CSS pixels relative to the
18
+ * canvas viewport**. The consumer holds the camera and converts to
19
+ * document-space via {@link Transform}.
20
+ */
21
+ type HUDEvent = {
22
+ readonly kind: "pointer_move";
23
+ readonly x: number;
24
+ readonly y: number;
25
+ readonly mods: Modifiers;
26
+ } | {
27
+ readonly kind: "pointer_down";
28
+ readonly x: number;
29
+ readonly y: number;
30
+ readonly button: PointerButton;
31
+ readonly mods: Modifiers;
32
+ } | {
33
+ readonly kind: "pointer_up";
34
+ readonly x: number;
35
+ readonly y: number;
36
+ readonly button: PointerButton;
37
+ readonly mods: Modifiers;
38
+ } | {
39
+ readonly kind: "modifiers";
40
+ readonly mods: Modifiers;
41
+ } | {
42
+ readonly kind: "wheel";
43
+ readonly x: number;
44
+ readonly y: number;
45
+ readonly dx: number;
46
+ readonly dy: number;
47
+ readonly mods: Modifiers;
48
+ } | {
49
+ readonly kind: "key";
50
+ readonly phase: "down" | "up";
51
+ readonly code: string;
52
+ readonly mods: Modifiers;
53
+ } | {
54
+ readonly kind: "blur";
55
+ };
56
+ /** Convenience re-export. */
57
+ type Vector2 = cmath.Vector2;
58
+ //#endregion
59
+ //#region core/click-tracker.d.ts
60
+ interface ClickTrackerOptions {
61
+ /** Max gap between clicks in ms. Default 250. */
62
+ readonly windowMs?: number;
63
+ /** Max distance between clicks in screen px. Default 5. */
64
+ readonly distancePx?: number;
65
+ }
66
+ declare class ClickTracker {
67
+ private window_ms;
68
+ private distance_px;
69
+ private last_time;
70
+ private last_x;
71
+ private last_y;
72
+ private count;
73
+ constructor(opts?: ClickTrackerOptions);
74
+ /**
75
+ * Register a click at `(x, y)` and return the current consecutive-
76
+ * click count (1 = single, 2 = double, etc.). `now` is in ms.
77
+ */
78
+ register(x: number, y: number, now?: number): number;
79
+ reset(): void;
80
+ }
81
+ //#endregion
82
+ //#region core/transform.d.ts
83
+ /**
84
+ * Axis-aligned 2×3 affine.
85
+ *
86
+ * Stored as a `cmath.Transform`:
87
+ * `[[sx, 0, tx], [0, sy, ty]]`
88
+ *
89
+ * Off-diagonal components are ignored by these helpers; HUD's camera
90
+ * is scale + translate only.
91
+ */
92
+ type Transform = cmath.Transform;
93
+ declare const IDENTITY: Transform;
94
+ /** Project a screen-space point into document-space. */
95
+ declare function screenToDoc(t: Transform, x: number, y: number): cmath.Vector2;
96
+ /** Project a document-space point into screen-space. */
97
+ declare function docToScreen(t: Transform, x: number, y: number): cmath.Vector2;
98
+ /** Current uniform zoom. Reads `sx`. */
99
+ declare function zoomOf(t: Transform): number;
100
+ //#endregion
101
+ //#region core/registry.d.ts
102
+ /**
103
+ * Thrown by `register` when an entity with the same `name` is already
104
+ * registered. Consumers should `unregister` the existing one first.
105
+ */
106
+ declare class RegistrationError extends Error {
107
+ constructor(message: string);
108
+ }
109
+ /**
110
+ * Generic name-keyed registry.
111
+ *
112
+ * @typeParam K — key type, must extend `string`.
113
+ * @typeParam T — entity type. Must expose `readonly name: K` and an
114
+ * optional `detach()` method.
115
+ */
116
+ declare class NamedRegistry<K extends string, T extends {
117
+ readonly name: K;
118
+ detach?(): void;
119
+ }> {
120
+ private label;
121
+ private byName;
122
+ /** Explicit insertion-ordered array for cheap reverse-walk on clear. */
123
+ private order;
124
+ constructor(label: string);
125
+ register(e: T): void;
126
+ unregister(e: T): void;
127
+ has(name: K): boolean;
128
+ get(name: K): T | undefined;
129
+ entries(): IterableIterator<T>;
130
+ size(): number;
131
+ clear(): void;
132
+ }
133
+ //#endregion
134
+ //#region core/hit-registry.d.ts
135
+ /**
136
+ * Generic hit registry. One per frame (or persistent — bedrock is
137
+ * agnostic). Add objects with `add`, clear with `clear`, query with
138
+ * `queryPoint` / `queryAll`.
139
+ */
140
+ declare class HitRegistry<I = unknown> {
141
+ private items;
142
+ /**
143
+ * Add an object. Objects without `hit` are accepted (they may carry
144
+ * paint information the consumer still wants in the registry for
145
+ * uniform iteration) but are filtered from point queries.
146
+ */
147
+ add(obj: HUDObject<I>): void;
148
+ /** Drop all entries. */
149
+ clear(): void;
150
+ /** Total number of stored objects (paint-only + hit-testable). */
151
+ size(): number;
152
+ /** Iterate every stored object in insertion order. */
153
+ entries(): IterableIterator<HUDObject<I>>;
154
+ /**
155
+ * Return the hit-testable object whose hit shape contains the
156
+ * given screen-space point with the lowest `priority` value
157
+ * (lower wins). Paint-only objects (no `hit`) are skipped.
158
+ */
159
+ queryPoint(point_screen: cmath.Vector2, transform: Transform): HUDObject<I> | null;
160
+ /**
161
+ * Return all hit-testable objects whose hit shape contains the point,
162
+ * winner first. Ordering is identical to `queryPoint`'s arbitration:
163
+ * priority ascending (lower wins), and on EQUAL priority the
164
+ * later-added object comes first ("later push wins on tie"). This makes
165
+ * `queryAll(p)[0]` always equal `queryPoint(p)` — the two query paths
166
+ * never disagree on the winner.
167
+ */
168
+ queryAll(point_screen: cmath.Vector2, transform: Transform): HUDObject<I>[];
169
+ }
170
+ /**
171
+ * Test whether a screen-space point lies inside a `HitShape`.
172
+ *
173
+ * The camera `transform` is needed for doc-anchored shapes
174
+ * (`screen_rect_at_doc`, `screen_circle_at_doc`); pre-projected
175
+ * shapes (`screen_aabb`, `screen_polygon`) ignore it. `screen_obb`
176
+ * applies its own `inverse_transform`, which maps screen → shadow
177
+ * (independent of the camera).
178
+ */
179
+ declare function shapeContains(shape: HitShape, point_screen: cmath.Vector2, transform: Transform): boolean;
180
+ //#endregion
181
+ export { ClickTracker, type ClickTrackerOptions, type HUDEvent, HitRegistry, IDENTITY, type Modifiers, NO_MODS, NamedRegistry, type PointerButton, RegistrationError, type Transform, type Vector2, docToScreen, screenToDoc, shapeContains, zoomOf };
@@ -0,0 +1,181 @@
1
+ import { i as HitShape, t as HUDObject } from "../overlay-CVV4s3IL.js";
2
+ import cmath from "@grida/cmath";
3
+
4
+ //#region core/event.d.ts
5
+ /** Modifier-key snapshot at the moment an event was produced. */
6
+ interface Modifiers {
7
+ readonly shift: boolean;
8
+ readonly alt: boolean;
9
+ readonly meta: boolean;
10
+ readonly ctrl: boolean;
11
+ }
12
+ declare const NO_MODS: Modifiers;
13
+ type PointerButton = "primary" | "secondary" | "middle";
14
+ /**
15
+ * Synthesized input event consumed by HUD bedrock.
16
+ *
17
+ * All coordinates are **screen-space CSS pixels relative to the
18
+ * canvas viewport**. The consumer holds the camera and converts to
19
+ * document-space via {@link Transform}.
20
+ */
21
+ type HUDEvent = {
22
+ readonly kind: "pointer_move";
23
+ readonly x: number;
24
+ readonly y: number;
25
+ readonly mods: Modifiers;
26
+ } | {
27
+ readonly kind: "pointer_down";
28
+ readonly x: number;
29
+ readonly y: number;
30
+ readonly button: PointerButton;
31
+ readonly mods: Modifiers;
32
+ } | {
33
+ readonly kind: "pointer_up";
34
+ readonly x: number;
35
+ readonly y: number;
36
+ readonly button: PointerButton;
37
+ readonly mods: Modifiers;
38
+ } | {
39
+ readonly kind: "modifiers";
40
+ readonly mods: Modifiers;
41
+ } | {
42
+ readonly kind: "wheel";
43
+ readonly x: number;
44
+ readonly y: number;
45
+ readonly dx: number;
46
+ readonly dy: number;
47
+ readonly mods: Modifiers;
48
+ } | {
49
+ readonly kind: "key";
50
+ readonly phase: "down" | "up";
51
+ readonly code: string;
52
+ readonly mods: Modifiers;
53
+ } | {
54
+ readonly kind: "blur";
55
+ };
56
+ /** Convenience re-export. */
57
+ type Vector2 = cmath.Vector2;
58
+ //#endregion
59
+ //#region core/click-tracker.d.ts
60
+ interface ClickTrackerOptions {
61
+ /** Max gap between clicks in ms. Default 250. */
62
+ readonly windowMs?: number;
63
+ /** Max distance between clicks in screen px. Default 5. */
64
+ readonly distancePx?: number;
65
+ }
66
+ declare class ClickTracker {
67
+ private window_ms;
68
+ private distance_px;
69
+ private last_time;
70
+ private last_x;
71
+ private last_y;
72
+ private count;
73
+ constructor(opts?: ClickTrackerOptions);
74
+ /**
75
+ * Register a click at `(x, y)` and return the current consecutive-
76
+ * click count (1 = single, 2 = double, etc.). `now` is in ms.
77
+ */
78
+ register(x: number, y: number, now?: number): number;
79
+ reset(): void;
80
+ }
81
+ //#endregion
82
+ //#region core/transform.d.ts
83
+ /**
84
+ * Axis-aligned 2×3 affine.
85
+ *
86
+ * Stored as a `cmath.Transform`:
87
+ * `[[sx, 0, tx], [0, sy, ty]]`
88
+ *
89
+ * Off-diagonal components are ignored by these helpers; HUD's camera
90
+ * is scale + translate only.
91
+ */
92
+ type Transform = cmath.Transform;
93
+ declare const IDENTITY: Transform;
94
+ /** Project a screen-space point into document-space. */
95
+ declare function screenToDoc(t: Transform, x: number, y: number): cmath.Vector2;
96
+ /** Project a document-space point into screen-space. */
97
+ declare function docToScreen(t: Transform, x: number, y: number): cmath.Vector2;
98
+ /** Current uniform zoom. Reads `sx`. */
99
+ declare function zoomOf(t: Transform): number;
100
+ //#endregion
101
+ //#region core/registry.d.ts
102
+ /**
103
+ * Thrown by `register` when an entity with the same `name` is already
104
+ * registered. Consumers should `unregister` the existing one first.
105
+ */
106
+ declare class RegistrationError extends Error {
107
+ constructor(message: string);
108
+ }
109
+ /**
110
+ * Generic name-keyed registry.
111
+ *
112
+ * @typeParam K — key type, must extend `string`.
113
+ * @typeParam T — entity type. Must expose `readonly name: K` and an
114
+ * optional `detach()` method.
115
+ */
116
+ declare class NamedRegistry<K extends string, T extends {
117
+ readonly name: K;
118
+ detach?(): void;
119
+ }> {
120
+ private label;
121
+ private byName;
122
+ /** Explicit insertion-ordered array for cheap reverse-walk on clear. */
123
+ private order;
124
+ constructor(label: string);
125
+ register(e: T): void;
126
+ unregister(e: T): void;
127
+ has(name: K): boolean;
128
+ get(name: K): T | undefined;
129
+ entries(): IterableIterator<T>;
130
+ size(): number;
131
+ clear(): void;
132
+ }
133
+ //#endregion
134
+ //#region core/hit-registry.d.ts
135
+ /**
136
+ * Generic hit registry. One per frame (or persistent — bedrock is
137
+ * agnostic). Add objects with `add`, clear with `clear`, query with
138
+ * `queryPoint` / `queryAll`.
139
+ */
140
+ declare class HitRegistry<I = unknown> {
141
+ private items;
142
+ /**
143
+ * Add an object. Objects without `hit` are accepted (they may carry
144
+ * paint information the consumer still wants in the registry for
145
+ * uniform iteration) but are filtered from point queries.
146
+ */
147
+ add(obj: HUDObject<I>): void;
148
+ /** Drop all entries. */
149
+ clear(): void;
150
+ /** Total number of stored objects (paint-only + hit-testable). */
151
+ size(): number;
152
+ /** Iterate every stored object in insertion order. */
153
+ entries(): IterableIterator<HUDObject<I>>;
154
+ /**
155
+ * Return the hit-testable object whose hit shape contains the
156
+ * given screen-space point with the lowest `priority` value
157
+ * (lower wins). Paint-only objects (no `hit`) are skipped.
158
+ */
159
+ queryPoint(point_screen: cmath.Vector2, transform: Transform): HUDObject<I> | null;
160
+ /**
161
+ * Return all hit-testable objects whose hit shape contains the point,
162
+ * winner first. Ordering is identical to `queryPoint`'s arbitration:
163
+ * priority ascending (lower wins), and on EQUAL priority the
164
+ * later-added object comes first ("later push wins on tie"). This makes
165
+ * `queryAll(p)[0]` always equal `queryPoint(p)` — the two query paths
166
+ * never disagree on the winner.
167
+ */
168
+ queryAll(point_screen: cmath.Vector2, transform: Transform): HUDObject<I>[];
169
+ }
170
+ /**
171
+ * Test whether a screen-space point lies inside a `HitShape`.
172
+ *
173
+ * The camera `transform` is needed for doc-anchored shapes
174
+ * (`screen_rect_at_doc`, `screen_circle_at_doc`); pre-projected
175
+ * shapes (`screen_aabb`, `screen_polygon`) ignore it. `screen_obb`
176
+ * applies its own `inverse_transform`, which maps screen → shadow
177
+ * (independent of the camera).
178
+ */
179
+ declare function shapeContains(shape: HitShape, point_screen: cmath.Vector2, transform: Transform): boolean;
180
+ //#endregion
181
+ export { ClickTracker, type ClickTrackerOptions, type HUDEvent, HitRegistry, IDENTITY, type Modifiers, NO_MODS, NamedRegistry, type PointerButton, RegistrationError, type Transform, type Vector2, docToScreen, screenToDoc, shapeContains, zoomOf };
@@ -0,0 +1,301 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region core/event.ts
3
+ const NO_MODS = {
4
+ shift: false,
5
+ alt: false,
6
+ meta: false,
7
+ ctrl: false
8
+ };
9
+ //#endregion
10
+ //#region core/click-tracker.ts
11
+ var ClickTracker = class {
12
+ constructor(opts = {}) {
13
+ this.last_time = 0;
14
+ this.last_x = 0;
15
+ this.last_y = 0;
16
+ this.count = 0;
17
+ this.window_ms = opts.windowMs ?? 250;
18
+ this.distance_px = opts.distancePx ?? 5;
19
+ }
20
+ /**
21
+ * Register a click at `(x, y)` and return the current consecutive-
22
+ * click count (1 = single, 2 = double, etc.). `now` is in ms.
23
+ */
24
+ register(x, y, now = nowMs()) {
25
+ const dt = now - this.last_time;
26
+ const dx = x - this.last_x;
27
+ const dy = y - this.last_y;
28
+ const dist2 = dx * dx + dy * dy;
29
+ const max2 = this.distance_px * this.distance_px;
30
+ if (dt <= this.window_ms && dist2 <= max2 && this.count > 0) this.count += 1;
31
+ else this.count = 1;
32
+ this.last_time = now;
33
+ this.last_x = x;
34
+ this.last_y = y;
35
+ return this.count;
36
+ }
37
+ reset() {
38
+ this.count = 0;
39
+ this.last_time = 0;
40
+ }
41
+ };
42
+ function nowMs() {
43
+ if (typeof performance !== "undefined" && performance.now) return performance.now();
44
+ return Date.now();
45
+ }
46
+ //#endregion
47
+ //#region core/transform.ts
48
+ const IDENTITY = [[
49
+ 1,
50
+ 0,
51
+ 0
52
+ ], [
53
+ 0,
54
+ 1,
55
+ 0
56
+ ]];
57
+ /** Project a screen-space point into document-space. */
58
+ function screenToDoc(t, x, y) {
59
+ const [[sx, , tx], [, sy, ty]] = t;
60
+ return [(x - tx) / (sx || 1), (y - ty) / (sy || 1)];
61
+ }
62
+ /** Project a document-space point into screen-space. */
63
+ function docToScreen(t, x, y) {
64
+ const [[sx, , tx], [, sy, ty]] = t;
65
+ return [sx * x + tx, sy * y + ty];
66
+ }
67
+ /** Current uniform zoom. Reads `sx`. */
68
+ function zoomOf(t) {
69
+ return t[0][0];
70
+ }
71
+ //#endregion
72
+ //#region core/registry.ts
73
+ /**
74
+ * Thrown by `register` when an entity with the same `name` is already
75
+ * registered. Consumers should `unregister` the existing one first.
76
+ */
77
+ var RegistrationError = class extends Error {
78
+ constructor(message) {
79
+ super(message);
80
+ this.name = "RegistrationError";
81
+ }
82
+ };
83
+ /**
84
+ * Generic name-keyed registry.
85
+ *
86
+ * @typeParam K — key type, must extend `string`.
87
+ * @typeParam T — entity type. Must expose `readonly name: K` and an
88
+ * optional `detach()` method.
89
+ */
90
+ var NamedRegistry = class {
91
+ constructor(label) {
92
+ this.label = label;
93
+ this.byName = /* @__PURE__ */ new Map();
94
+ this.order = [];
95
+ }
96
+ register(e) {
97
+ if (this.byName.has(e.name)) throw new RegistrationError(`${this.label} "${e.name}" already registered; unregister it first.`);
98
+ this.byName.set(e.name, e);
99
+ this.order.push(e);
100
+ }
101
+ unregister(e) {
102
+ if (this.byName.get(e.name) !== e) return;
103
+ this.byName.delete(e.name);
104
+ const i = this.order.indexOf(e);
105
+ if (i >= 0) this.order.splice(i, 1);
106
+ try {
107
+ e.detach?.();
108
+ } catch (err) {
109
+ console.error(`[hud] ${this.label} "${e.name}" detach() threw:`, err);
110
+ }
111
+ }
112
+ has(name) {
113
+ return this.byName.has(name);
114
+ }
115
+ get(name) {
116
+ return this.byName.get(name);
117
+ }
118
+ *entries() {
119
+ for (const e of this.order) yield e;
120
+ }
121
+ size() {
122
+ return this.order.length;
123
+ }
124
+ clear() {
125
+ for (let i = this.order.length - 1; i >= 0; i--) {
126
+ const e = this.order[i];
127
+ try {
128
+ e.detach?.();
129
+ } catch (err) {
130
+ console.error(`[hud] ${this.label} "${e.name}" detach() threw:`, err);
131
+ }
132
+ }
133
+ this.order = [];
134
+ this.byName.clear();
135
+ }
136
+ };
137
+ //#endregion
138
+ //#region core/hit-registry.ts
139
+ /**
140
+ * Generic hit registry. One per frame (or persistent — bedrock is
141
+ * agnostic). Add objects with `add`, clear with `clear`, query with
142
+ * `queryPoint` / `queryAll`.
143
+ */
144
+ var HitRegistry = class {
145
+ constructor() {
146
+ this.items = [];
147
+ }
148
+ /**
149
+ * Add an object. Objects without `hit` are accepted (they may carry
150
+ * paint information the consumer still wants in the registry for
151
+ * uniform iteration) but are filtered from point queries.
152
+ */
153
+ add(obj) {
154
+ this.items.push(obj);
155
+ }
156
+ /** Drop all entries. */
157
+ clear() {
158
+ this.items = [];
159
+ }
160
+ /** Total number of stored objects (paint-only + hit-testable). */
161
+ size() {
162
+ return this.items.length;
163
+ }
164
+ /** Iterate every stored object in insertion order. */
165
+ *entries() {
166
+ for (const o of this.items) yield o;
167
+ }
168
+ /**
169
+ * Return the hit-testable object whose hit shape contains the
170
+ * given screen-space point with the lowest `priority` value
171
+ * (lower wins). Paint-only objects (no `hit`) are skipped.
172
+ */
173
+ queryPoint(point_screen, transform) {
174
+ let best = null;
175
+ let best_priority = Number.POSITIVE_INFINITY;
176
+ for (const obj of this.items) {
177
+ if (!obj.hit) continue;
178
+ if (obj.priority > best_priority) continue;
179
+ if (!shapeContains(obj.hit, point_screen, transform)) continue;
180
+ if (obj.refine && !obj.refine(point_screen)) continue;
181
+ best = obj;
182
+ best_priority = obj.priority;
183
+ }
184
+ return best;
185
+ }
186
+ /**
187
+ * Return all hit-testable objects whose hit shape contains the point,
188
+ * winner first. Ordering is identical to `queryPoint`'s arbitration:
189
+ * priority ascending (lower wins), and on EQUAL priority the
190
+ * later-added object comes first ("later push wins on tie"). This makes
191
+ * `queryAll(p)[0]` always equal `queryPoint(p)` — the two query paths
192
+ * never disagree on the winner.
193
+ */
194
+ queryAll(point_screen, transform) {
195
+ const matches = [];
196
+ for (let i = 0; i < this.items.length; i++) {
197
+ const obj = this.items[i];
198
+ if (!obj.hit) continue;
199
+ if (!shapeContains(obj.hit, point_screen, transform)) continue;
200
+ if (obj.refine && !obj.refine(point_screen)) continue;
201
+ matches.push({
202
+ obj,
203
+ i
204
+ });
205
+ }
206
+ matches.sort((a, b) => a.obj.priority - b.obj.priority || b.i - a.i);
207
+ return matches.map((m) => m.obj);
208
+ }
209
+ };
210
+ /**
211
+ * Test whether a screen-space point lies inside a `HitShape`.
212
+ *
213
+ * The camera `transform` is needed for doc-anchored shapes
214
+ * (`screen_rect_at_doc`, `screen_circle_at_doc`); pre-projected
215
+ * shapes (`screen_aabb`, `screen_polygon`) ignore it. `screen_obb`
216
+ * applies its own `inverse_transform`, which maps screen → shadow
217
+ * (independent of the camera).
218
+ */
219
+ function shapeContains(shape, point_screen, transform) {
220
+ const [px, py] = point_screen;
221
+ switch (shape.kind) {
222
+ case "screen_rect_at_doc": {
223
+ const [ax, ay] = docToScreen(transform, shape.anchor_doc[0], shape.anchor_doc[1]);
224
+ const { x, y } = anchorOrigin(ax, ay, shape.width, shape.height, shape.placement ?? "center");
225
+ return px >= x && px <= x + shape.width && py >= y && py <= y + shape.height;
226
+ }
227
+ case "screen_aabb": {
228
+ const r = shape.rect;
229
+ return px >= r.x && px <= r.x + r.width && py >= r.y && py <= r.y + r.height;
230
+ }
231
+ case "screen_obb": {
232
+ const [[a, b, e], [c, d, f]] = shape.inverse_transform;
233
+ const sx = a * px + b * py + e;
234
+ const sy = c * px + d * py + f;
235
+ const r = shape.rect;
236
+ return sx >= r.x && sx <= r.x + r.width && sy >= r.y && sy <= r.y + r.height;
237
+ }
238
+ case "screen_circle_at_doc": {
239
+ const [ax, ay] = docToScreen(transform, shape.anchor_doc[0], shape.anchor_doc[1]);
240
+ const dx = px - ax;
241
+ const dy = py - ay;
242
+ return dx * dx + dy * dy <= shape.radius * shape.radius;
243
+ }
244
+ case "screen_polygon": return pointInPolygon(point_screen, shape.points);
245
+ }
246
+ }
247
+ function anchorOrigin(ax, ay, w, h, placement) {
248
+ switch (placement) {
249
+ case "center": return {
250
+ x: ax - w / 2,
251
+ y: ay - h / 2
252
+ };
253
+ case "tl": return {
254
+ x: ax,
255
+ y: ay
256
+ };
257
+ case "tr": return {
258
+ x: ax - w,
259
+ y: ay
260
+ };
261
+ case "bl": return {
262
+ x: ax,
263
+ y: ay - h
264
+ };
265
+ case "br": return {
266
+ x: ax - w,
267
+ y: ay - h
268
+ };
269
+ }
270
+ }
271
+ /**
272
+ * Standard even-odd ray-cast point-in-polygon.
273
+ *
274
+ * Horizontal edges (`yi === yj`) are skipped — the standard idiom for
275
+ * the even-odd algorithm. (The previous form used `(yj - yi ||
276
+ * Number.EPSILON)` as a divide-by-zero guard, but the guard was dead
277
+ * code: the `yi > py !== yj > py` clause already short-circuits to
278
+ * `false` when the edge is horizontal, so the division never runs.)
279
+ */
280
+ function pointInPolygon(point, poly) {
281
+ const [px, py] = point;
282
+ let inside = false;
283
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
284
+ const [xi, yi] = poly[i];
285
+ const [xj, yj] = poly[j];
286
+ if (yi === yj) continue;
287
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) inside = !inside;
288
+ }
289
+ return inside;
290
+ }
291
+ //#endregion
292
+ exports.ClickTracker = ClickTracker;
293
+ exports.HitRegistry = HitRegistry;
294
+ exports.IDENTITY = IDENTITY;
295
+ exports.NO_MODS = NO_MODS;
296
+ exports.NamedRegistry = NamedRegistry;
297
+ exports.RegistrationError = RegistrationError;
298
+ exports.docToScreen = docToScreen;
299
+ exports.screenToDoc = screenToDoc;
300
+ exports.shapeContains = shapeContains;
301
+ exports.zoomOf = zoomOf;