@vectojs/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2238 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7; var _class8;
2
+
3
+
4
+
5
+
6
+ var _chunk72WVPMSJjs = require('./chunk-72WVPMSJ.js');
7
+
8
+
9
+
10
+
11
+
12
+
13
+ var _chunkLIX7DJTIjs = require('./chunk-LIX7DJTI.js');
14
+
15
+
16
+
17
+
18
+
19
+
20
+ var _chunk53DAQC3Ujs = require('./chunk-53DAQC3U.js');
21
+
22
+
23
+
24
+
25
+ var _chunkRW6NC4RBjs = require('./chunk-RW6NC4RB.js');
26
+
27
+ // src/tree/ComputeParticleEntity.ts
28
+ var PARTICLE_STRIDE_FLOATS = 8;
29
+ var PARTICLE_OFFSET_POSITION_X = 0;
30
+ var PARTICLE_OFFSET_POSITION_Y = 1;
31
+ var PARTICLE_OFFSET_VELOCITY_X = 2;
32
+ var PARTICLE_OFFSET_VELOCITY_Y = 3;
33
+ var PARTICLE_OFFSET_ORIGIN_X = 4;
34
+ var PARTICLE_OFFSET_ORIGIN_Y = 5;
35
+ var PARTICLE_OFFSET_SIZE = 6;
36
+ var PARTICLE_OFFSET_LIFE = 7;
37
+ var ComputeParticleEntity = (_class = class extends _chunk53DAQC3Ujs.Entity {
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ /** Flat array containing layout of all particles: position, velocity, origin, size, life. */
47
+
48
+ /** Flag indicating whether the particle coordinates need to be initialized. */
49
+ __init() {this.needsInit = true}
50
+ /** Active explosion impulse to apply in the next simulation step. */
51
+ __init2() {this.pendingExplosion = null}
52
+ /** WebGPU storage buffer containing particle states. */
53
+ __init3() {this.gpuStorageBuffer = null}
54
+ /** WebGPU uniform buffer containing simulation parameters. */
55
+ __init4() {this.gpuUniformBuffer = null}
56
+ /** WebGPU bind group for the compute shader pass. */
57
+ __init5() {this.computeBindGroup = null}
58
+ /** WebGPU bind group for the render pass (usually same as compute). */
59
+ __init6() {this.renderBindGroup = null}
60
+ constructor(options = {}) {
61
+ super();_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);;
62
+ this.maxParticles = _nullishCoalesce(options.maxParticles, () => ( 1e4));
63
+ this.springK = _nullishCoalesce(options.springK, () => ( 0.05));
64
+ this.damping = _nullishCoalesce(options.damping, () => ( 0.95));
65
+ this.bounceDamping = _nullishCoalesce(options.bounceDamping, () => ( 0.5));
66
+ this.maxVelocity = _nullishCoalesce(options.maxVelocity, () => ( 500));
67
+ this.size = _nullishCoalesce(options.size, () => ( 4));
68
+ this.baseColor = _nullishCoalesce(options.color, () => ( "#00f0ff"));
69
+ this.pointerEvents = _nullishCoalesce(options.pointerEvents, () => ( false));
70
+ this.particleData = new Float32Array(this.maxParticles * PARTICLE_STRIDE_FLOATS);
71
+ this.interactive = true;
72
+ }
73
+ /**
74
+ * Disperses all particles randomly across the specified screen bounds.
75
+ * Sets initial positions, velocities, origins, and sizes.
76
+ *
77
+ * @param width - Simulation zone width.
78
+ * @param height - Simulation zone height.
79
+ */
80
+ initRandomParticles(width, height) {
81
+ const safeW = Math.max(1, width);
82
+ const safeH = Math.max(1, height);
83
+ for (let i = 0; i < this.maxParticles; i++) {
84
+ const idx = i * PARTICLE_STRIDE_FLOATS;
85
+ const x = Math.random() * safeW;
86
+ const y = Math.random() * safeH;
87
+ this.particleData[idx + PARTICLE_OFFSET_POSITION_X] = x;
88
+ this.particleData[idx + PARTICLE_OFFSET_POSITION_Y] = y;
89
+ this.particleData[idx + PARTICLE_OFFSET_VELOCITY_X] = 0;
90
+ this.particleData[idx + PARTICLE_OFFSET_VELOCITY_Y] = 0;
91
+ this.particleData[idx + PARTICLE_OFFSET_ORIGIN_X] = x;
92
+ this.particleData[idx + PARTICLE_OFFSET_ORIGIN_Y] = y;
93
+ this.particleData[idx + PARTICLE_OFFSET_SIZE] = this.size;
94
+ this.particleData[idx + PARTICLE_OFFSET_LIFE] = -1;
95
+ }
96
+ this.needsInit = true;
97
+ _optionalChain([this, 'access', _ => _.scene, 'optionalAccess', _2 => _2.markDirty, 'call', _3 => _3()]);
98
+ }
99
+ /**
100
+ * Sets the origins (ox, oy) for a subset or all particles.
101
+ * Also sets position to origin if requestPositionReset is true.
102
+ *
103
+ * @param points - Flat Float32Array containing [x0, y0, x1, y1, ...]
104
+ * @param requestPositionReset - Whether to set current positions to the new origins. Defaults to true.
105
+ */
106
+ setOrigins(points, requestPositionReset = true) {
107
+ const len = Math.min(this.maxParticles, Math.floor(points.length / 2));
108
+ for (let i = 0; i < len; i++) {
109
+ const idx = i * PARTICLE_STRIDE_FLOATS;
110
+ const ptIdx = i * 2;
111
+ const ox = points[ptIdx];
112
+ const oy = points[ptIdx + 1];
113
+ this.particleData[idx + PARTICLE_OFFSET_ORIGIN_X] = ox;
114
+ this.particleData[idx + PARTICLE_OFFSET_ORIGIN_Y] = oy;
115
+ if (requestPositionReset) {
116
+ this.particleData[idx + PARTICLE_OFFSET_POSITION_X] = ox;
117
+ this.particleData[idx + PARTICLE_OFFSET_POSITION_Y] = oy;
118
+ this.particleData[idx + PARTICLE_OFFSET_VELOCITY_X] = 0;
119
+ this.particleData[idx + PARTICLE_OFFSET_VELOCITY_Y] = 0;
120
+ }
121
+ }
122
+ this.needsInit = true;
123
+ _optionalChain([this, 'access', _4 => _4.scene, 'optionalAccess', _5 => _5.markDirty, 'call', _6 => _6()]);
124
+ }
125
+ /**
126
+ * Sets the current positions (x, y) for a subset or all particles.
127
+ *
128
+ * @param positions - Flat Float32Array containing [x0, y0, x1, y1, ...]
129
+ */
130
+ setPositions(positions) {
131
+ const len = Math.min(this.maxParticles, Math.floor(positions.length / 2));
132
+ for (let i = 0; i < len; i++) {
133
+ const idx = i * PARTICLE_STRIDE_FLOATS;
134
+ const ptIdx = i * 2;
135
+ this.particleData[idx + PARTICLE_OFFSET_POSITION_X] = positions[ptIdx];
136
+ this.particleData[idx + PARTICLE_OFFSET_POSITION_Y] = positions[ptIdx + 1];
137
+ }
138
+ this.needsInit = true;
139
+ _optionalChain([this, 'access', _7 => _7.scene, 'optionalAccess', _8 => _8.markDirty, 'call', _9 => _9()]);
140
+ }
141
+ /**
142
+ * Sets the current velocities (vx, vy) for a subset or all particles.
143
+ *
144
+ * @param velocities - Flat Float32Array containing [vx0, vy0, vx1, vy1, ...]
145
+ */
146
+ setVelocities(velocities) {
147
+ const len = Math.min(this.maxParticles, Math.floor(velocities.length / 2));
148
+ for (let i = 0; i < len; i++) {
149
+ const idx = i * PARTICLE_STRIDE_FLOATS;
150
+ const ptIdx = i * 2;
151
+ this.particleData[idx + PARTICLE_OFFSET_VELOCITY_X] = velocities[ptIdx];
152
+ this.particleData[idx + PARTICLE_OFFSET_VELOCITY_Y] = velocities[ptIdx + 1];
153
+ }
154
+ this.needsInit = true;
155
+ _optionalChain([this, 'access', _10 => _10.scene, 'optionalAccess', _11 => _11.markDirty, 'call', _12 => _12()]);
156
+ }
157
+ /**
158
+ * Triggers an explosion force center.
159
+ *
160
+ * @param x - Explosion center x-coordinate.
161
+ * @param y - Explosion center y-coordinate.
162
+ * @param force - Magnitude force scalar.
163
+ */
164
+ triggerExplosion(x, y, force) {
165
+ this.pendingExplosion = { x, y, force };
166
+ }
167
+ isPointInside(_x, _y) {
168
+ return this.pointerEvents;
169
+ }
170
+ render(_r) {
171
+ }
172
+ /**
173
+ * Updates particle simulation on the CPU.
174
+ * Handles spring forces, mouse repulsion, explosion impulses, velocity capping, and bounds bouncing/clamping.
175
+ *
176
+ * @param dt - Delta time in seconds.
177
+ * @param mouseX - Mouse x-coordinate, or a value below -9000 if inactive.
178
+ * @param mouseY - Mouse y-coordinate, or a value below -9000 if inactive.
179
+ * @param width - Boundary width.
180
+ * @param height - Boundary height.
181
+ */
182
+ updateCPU(dt, mouseX, mouseY, width, height) {
183
+ const safeDt = isNaN(dt) ? 0.016 : Math.max(0, Math.min(dt, 0.1));
184
+ const explosion = this.pendingExplosion;
185
+ const safeWidth = Math.max(1, width);
186
+ const safeHeight = Math.max(1, height);
187
+ const springK = Math.max(0, Math.min(10, this.springK));
188
+ const damping = Math.max(0, Math.min(1, this.damping));
189
+ const bounceDamping = Math.max(0, Math.min(1, this.bounceDamping));
190
+ const maxVelocity = Math.max(1, this.maxVelocity);
191
+ for (let i = 0; i < this.maxParticles; i++) {
192
+ const offset = i * 8;
193
+ let px = this.particleData[offset];
194
+ let py = this.particleData[offset + 1];
195
+ let vx = this.particleData[offset + 2];
196
+ let vy = this.particleData[offset + 3];
197
+ const ox = this.particleData[offset + 4];
198
+ const oy = this.particleData[offset + 5];
199
+ const life = this.particleData[offset + 7];
200
+ if (isNaN(px)) px = ox;
201
+ if (isNaN(py)) py = oy;
202
+ if (isNaN(vx)) vx = 0;
203
+ if (isNaN(vy)) vy = 0;
204
+ const fx_spring = (ox - px) * springK;
205
+ const fy_spring = (oy - py) * springK;
206
+ let fx_mouse = 0;
207
+ let fy_mouse = 0;
208
+ if (!isNaN(mouseX) && !isNaN(mouseY) && mouseX > -9e3 && mouseY > -9e3) {
209
+ const dx = mouseX - px;
210
+ const dy = mouseY - py;
211
+ const dist = Math.hypot(dx, dy);
212
+ if (dist < 120 && dist > 0.1) {
213
+ const forceMag = (120 - dist) * 2;
214
+ fx_mouse = -(dx / dist) * forceMag;
215
+ fy_mouse = -(dy / dist) * forceMag;
216
+ }
217
+ }
218
+ let fx_expl = 0;
219
+ let fy_expl = 0;
220
+ if (explosion) {
221
+ const ex = explosion.x - px;
222
+ const ey = explosion.y - py;
223
+ const edist = Math.hypot(ex, ey);
224
+ if (edist < 150 && edist > 0.1) {
225
+ const forceMag = (150 - edist) * explosion.force;
226
+ fx_expl = -(ex / edist) * forceMag;
227
+ fy_expl = -(ey / edist) * forceMag;
228
+ }
229
+ }
230
+ const ax = fx_spring + fx_mouse + fx_expl;
231
+ const ay = fy_spring + fy_mouse + fy_expl;
232
+ let nvx = (vx + ax * safeDt) * damping;
233
+ let nvy = (vy + ay * safeDt) * damping;
234
+ const speed = Math.hypot(nvx, nvy);
235
+ if (speed > maxVelocity) {
236
+ nvx = nvx / speed * maxVelocity;
237
+ nvy = nvy / speed * maxVelocity;
238
+ }
239
+ let npx = px + nvx * safeDt;
240
+ let npy = py + nvy * safeDt;
241
+ if (npx <= 0 && nvx < 0) {
242
+ nvx = -nvx * bounceDamping;
243
+ } else if (npx >= safeWidth && nvx > 0) {
244
+ nvx = -nvx * bounceDamping;
245
+ }
246
+ if (npy <= 0 && nvy < 0) {
247
+ nvy = -nvy * bounceDamping;
248
+ } else if (npy >= safeHeight && nvy > 0) {
249
+ nvy = -nvy * bounceDamping;
250
+ }
251
+ npx = Math.max(0, Math.min(safeWidth, npx));
252
+ npy = Math.max(0, Math.min(safeHeight, npy));
253
+ let nlife = life;
254
+ if (life >= 0) {
255
+ nlife = Math.max(0, life - safeDt * 0.5);
256
+ }
257
+ this.particleData[offset] = npx;
258
+ this.particleData[offset + 1] = npy;
259
+ this.particleData[offset + 2] = nvx;
260
+ this.particleData[offset + 3] = nvy;
261
+ this.particleData[offset + 7] = nlife;
262
+ }
263
+ this.pendingExplosion = null;
264
+ }
265
+ destroy() {
266
+ this.destroyGPUResources();
267
+ super.destroy();
268
+ }
269
+ /**
270
+ * Frees all GPU resources allocated for WebGPU simulation.
271
+ */
272
+ destroyGPUResources() {
273
+ if (this.gpuStorageBuffer) {
274
+ if (typeof this.gpuStorageBuffer.destroy === "function") {
275
+ this.gpuStorageBuffer.destroy();
276
+ }
277
+ this.gpuStorageBuffer = null;
278
+ }
279
+ if (this.gpuUniformBuffer) {
280
+ if (typeof this.gpuUniformBuffer.destroy === "function") {
281
+ this.gpuUniformBuffer.destroy();
282
+ }
283
+ this.gpuUniformBuffer = null;
284
+ }
285
+ this.computeBindGroup = null;
286
+ this.renderBindGroup = null;
287
+ }
288
+ }, _class);
289
+
290
+ // src/tree/Scene.ts
291
+ var REDUCED_MOTION_FPS = 30;
292
+ var Scene = (_class2 = class _Scene {
293
+ static __initStatic() {this.webglCreator = null}
294
+ static __initStatic2() {this.webgpuManagerClass = null}
295
+ static registerWebGLPointRendererCreator(creator) {
296
+ _Scene.webglCreator = creator;
297
+ }
298
+ static registerWebGPUParticleSystemManager(managerClass) {
299
+ _Scene.webgpuManagerClass = managerClass;
300
+ }
301
+
302
+
303
+
304
+ __init7() {this.isRunning = false}
305
+ __init8() {this.lastTime = 0}
306
+
307
+ /**
308
+ * Redraw strategy:
309
+ * - `'always'` (default): re-render every animation frame (legacy behavior).
310
+ * - `'onDemand'`: only re-render when the scene is marked dirty (via
311
+ * {@link markDirty}) or while an animation is pending. Ideal for static /
312
+ * event-driven UIs where idle frames should cost ~0.
313
+ */
314
+ __init9() {this.renderMode = "always"}
315
+ __init10() {this.dirty = true}
316
+ /**
317
+ * Frame-rate cap (power saving). `0` = uncapped (native refresh). When set,
318
+ * the loop renders at most `maxFPS` times per second; animations still run,
319
+ * just less often. See {@link SceneOptions.maxFPS}.
320
+ */
321
+ __init11() {this.maxFPS = 60}
322
+ /** Whether the OS prefers-reduced-motion setting auto-caps the loop. */
323
+ __init12() {this.respectReducedMotion = true}
324
+ /** Cached media-query list; `.matches` is read live each frame. */
325
+ __init13() {this.reducedMotionQuery = null}
326
+ /**
327
+ * Throttle interval (ms) for the a11y/automation shadow sync. `0` = every
328
+ * frame. See {@link SceneOptions.a11ySyncInterval}.
329
+ */
330
+ __init14() {this.a11ySyncInterval = 0}
331
+ /** Timestamp of the last a11y sync, for throttling. */
332
+ __init15() {this.lastA11ySync = -Infinity}
333
+ /** True if we skipped an a11y sync during animation and need to sync when at rest. */
334
+ __init16() {this.a11yPendingSyncAfterAnimation = false}
335
+ // A11y / Automation Layer. `null` in non-DOM (SSR/Node) environments — the
336
+ // whole projection degrades to a no-op so the engine's logic stays usable
337
+ // server-side (e.g. headless layout / vector export) without jsdom.
338
+
339
+ __init17() {this.a11yElements = /* @__PURE__ */ new Map()}
340
+
341
+ __init18() {this.focusedA11yElement = null}
342
+ __init19() {this.caretBlinkTimer = null}
343
+ __init20() {this.a11yNeedsReorder = true}
344
+ __init21() {this.portalRoot = null}
345
+ __init22() {this.fullViewportElements = []}
346
+ __init23() {this.normalElements = []}
347
+ __init24() {this.activeIds = /* @__PURE__ */ new Set()}
348
+ __init25() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
349
+ __init26() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
350
+ __init27() {this.portalEntities = /* @__PURE__ */ new Map()}
351
+ __init28() {this.renderOrderCounter = 0}
352
+ // Optional WebGL point-cloud layer (see SceneOptions.pointBackend).
353
+ __init29() {this.pointRenderer = null}
354
+ __init30() {this.glCanvas = null}
355
+
356
+
357
+
358
+ __init31() {this.disableWindowResize = false}
359
+ // WebGPU properties
360
+ __init32() {this.destroyed = false}
361
+ __init33() {this.device = null}
362
+ __init34() {this.deviceLost = false}
363
+ __init35() {this.particleBackend = "auto"}
364
+ __init36() {this._webgpuDisabled = false}
365
+ get webgpuDisabled() {
366
+ return this._webgpuDisabled || this.particleBackend === "cpu";
367
+ }
368
+ set webgpuDisabled(value) {
369
+ this._webgpuDisabled = value;
370
+ }
371
+ __init37() {this.recoveryTimerId = null}
372
+ __init38() {this.manager = null}
373
+ __init39() {this.initializingWebGPU = false}
374
+ __init40() {this.gpuCanvas = null}
375
+ __init41() {this.gpuContext = null}
376
+ __init42() {this.mouseX = -9999}
377
+ __init43() {this.mouseY = -9999}
378
+ __init44() {this.pointerMoveListener = null}
379
+ __init45() {this.pointerLeaveListener = null}
380
+ __init46() {this.hasWarnedZeroSize = false}
381
+ constructor(canvas, options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);
382
+ this.canvas = canvas;
383
+ this.debugA11y = _nullishCoalesce(options.debugA11y, () => ( false));
384
+ this.disableWindowResize = _nullishCoalesce(options.disableWindowResize, () => ( false));
385
+ if (this.disableWindowResize) {
386
+ this.width = canvas.width || canvas.clientWidth || 0;
387
+ this.height = canvas.height || canvas.clientHeight || 0;
388
+ } else {
389
+ this.width = typeof window !== "undefined" ? window.innerWidth : canvas.clientWidth || canvas.width || 800;
390
+ this.height = typeof window !== "undefined" ? window.innerHeight : canvas.clientHeight || canvas.height || 600;
391
+ }
392
+ const globalProcess = typeof globalThis !== "undefined" ? globalThis.process : void 0;
393
+ const isTest = globalProcess && (_optionalChain([globalProcess, 'access', _13 => _13.env, 'optionalAccess', _14 => _14.NODE_ENV]) === "test" || _optionalChain([globalProcess, 'access', _15 => _15.env, 'optionalAccess', _16 => _16.VITEST]) === "true");
394
+ this.maxFPS = _nullishCoalesce(options.maxFPS, () => ( (isTest ? 0 : 60)));
395
+ this.respectReducedMotion = _nullishCoalesce(options.respectReducedMotion, () => ( true));
396
+ this.particleBackend = _nullishCoalesce(options.particleBackend, () => ( "auto"));
397
+ this.a11ySyncInterval = _nullishCoalesce(options.a11ySyncInterval, () => ( 0));
398
+ this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
399
+ this.root = new class RootEntity extends _chunk53DAQC3Ujs.Entity {
400
+ isPointInside() {
401
+ return false;
402
+ }
403
+ // Root renders nothing itself — renderNode() handles all child traversal.
404
+ render(_r) {
405
+ }
406
+ }("root");
407
+ this.root._scene = this;
408
+ this.overlayRoot = new class OverlayRoot extends _chunk53DAQC3Ujs.Entity {
409
+ isPointInside() {
410
+ return false;
411
+ }
412
+ render() {
413
+ }
414
+ }("overlayRoot");
415
+ this.overlayRoot._scene = this;
416
+ if (options.renderer) {
417
+ this.renderer = options.renderer;
418
+ } else {
419
+ this.renderer = new (0, _chunkLIX7DJTIjs.CanvasRenderer)(canvas);
420
+ }
421
+ if (typeof document !== "undefined") {
422
+ this.a11yRoot = document.createElement("div");
423
+ this.a11yRoot.style.position = "absolute";
424
+ this.a11yRoot.style.top = "0";
425
+ this.a11yRoot.style.left = "0";
426
+ this.a11yRoot.style.width = "100vw";
427
+ this.a11yRoot.style.height = "100vh";
428
+ this.a11yRoot.style.pointerEvents = "none";
429
+ this.a11yRoot.style.overflow = "hidden";
430
+ this.a11yRoot.style.zIndex = "10";
431
+ if (canvas.parentElement) {
432
+ canvas.parentElement.appendChild(this.a11yRoot);
433
+ }
434
+ this.portalRoot = document.createElement("div");
435
+ this.portalRoot.style.position = "absolute";
436
+ this.portalRoot.style.top = "0";
437
+ this.portalRoot.style.left = "0";
438
+ this.portalRoot.style.width = "100vw";
439
+ this.portalRoot.style.height = "100vh";
440
+ this.portalRoot.style.pointerEvents = "none";
441
+ this.portalRoot.style.overflow = "hidden";
442
+ this.portalRoot.style.zIndex = "9";
443
+ if (canvas.parentElement) {
444
+ canvas.parentElement.appendChild(this.portalRoot);
445
+ }
446
+ } else {
447
+ this.a11yRoot = null;
448
+ this.portalRoot = null;
449
+ }
450
+ if (options.pointBackend === "webgl" && typeof document !== "undefined") {
451
+ const gl = document.createElement("canvas");
452
+ gl.style.position = "absolute";
453
+ gl.style.top = "0";
454
+ gl.style.left = "0";
455
+ gl.style.pointerEvents = "none";
456
+ gl.style.zIndex = "5";
457
+ if (canvas.parentElement) canvas.parentElement.appendChild(gl);
458
+ const pr = _Scene.webglCreator ? _Scene.webglCreator(gl) : null;
459
+ if (pr) {
460
+ pr.resize(this.width, this.height);
461
+ this.glCanvas = gl;
462
+ this.pointRenderer = pr;
463
+ } else {
464
+ gl.remove();
465
+ }
466
+ }
467
+ this.resizeHandler = () => {
468
+ this.resize(window.innerWidth, window.innerHeight);
469
+ };
470
+ this.setupEvents();
471
+ }
472
+ /**
473
+ * Expose the underlying {@link IRenderer} for advanced direct-draw operations.
474
+ *
475
+ * @returns The active renderer instance.
476
+ */
477
+ getRenderer() {
478
+ return this.renderer;
479
+ }
480
+ /**
481
+ * Add a top-level entity to the scene graph.
482
+ *
483
+ * @param entity - The entity to attach to the scene root.
484
+ * @returns `this` for method chaining.
485
+ * @example scene.add(new CircleEntity());
486
+ */
487
+ add(entity) {
488
+ this.root.add(entity);
489
+ return this;
490
+ }
491
+ removeA11yRecursively(node) {
492
+ if (node.isDOMPortal) {
493
+ node.domElement.remove();
494
+ this.portalEntities.delete(node.id);
495
+ this.activePortalsThisFrame.delete(node.id);
496
+ this.activePortalsPrevFrame.delete(node.id);
497
+ }
498
+ const el = this.a11yElements.get(node.id);
499
+ if (el) {
500
+ if (el === this.focusedA11yElement) {
501
+ this.focusedA11yElement = null;
502
+ if (this.caretBlinkTimer) {
503
+ clearInterval(this.caretBlinkTimer);
504
+ this.caretBlinkTimer = null;
505
+ }
506
+ }
507
+ el.remove();
508
+ this.a11yElements.delete(node.id);
509
+ this.a11yNeedsReorder = true;
510
+ }
511
+ for (const child of node.children) {
512
+ this.removeA11yRecursively(child);
513
+ }
514
+ }
515
+ /**
516
+ * Remove a top-level entity from the scene graph and clean up its
517
+ * accessibility shadow elements recursively.
518
+ *
519
+ * @param entity - The entity to detach from the scene root.
520
+ * @returns `this` for method chaining.
521
+ */
522
+ remove(entity) {
523
+ this.root.remove(entity);
524
+ this.removeA11yRecursively(entity);
525
+ return this;
526
+ }
527
+ /**
528
+ * Tear down the a11y/automation shadow nodes for `entity` and its descendants
529
+ * without removing it from the scene graph. Components that manage dynamic
530
+ * interactive *child* entities (e.g. a {@link Entity}'s per-link hotspots) call
531
+ * this before discarding those children so their shadow `<a>`/controls don't
532
+ * leak (the per-frame `syncA11y` only creates/updates, it never prunes).
533
+ *
534
+ * @param entity - The subtree whose shadow nodes should be removed.
535
+ */
536
+ detachA11y(entity) {
537
+ this.removeA11yRecursively(entity);
538
+ }
539
+ /**
540
+ * Add an overlay entity to the overlay root, bypassing main tree clipping bounds.
541
+ */
542
+ showOverlay(overlay) {
543
+ this.overlayRoot.add(overlay);
544
+ this.markDirty();
545
+ }
546
+ /**
547
+ * Remove an overlay entity from the overlay root.
548
+ */
549
+ hideOverlay(overlay) {
550
+ this.overlayRoot.remove(overlay);
551
+ this.removeA11yRecursively(overlay);
552
+ this.markDirty();
553
+ }
554
+ /**
555
+ * Tear down the Scene, halt the loop, and clean up event listeners and DOM elements.
556
+ */
557
+ destroy() {
558
+ this.destroyed = true;
559
+ this.stop();
560
+ if (typeof window !== "undefined" && !this.disableWindowResize) {
561
+ window.removeEventListener("resize", this.resizeHandler);
562
+ }
563
+ if (typeof window !== "undefined" && this.canvas && typeof this.canvas.removeEventListener === "function") {
564
+ if (this.pointerMoveListener) {
565
+ this.canvas.removeEventListener("pointermove", this.pointerMoveListener);
566
+ }
567
+ if (this.pointerLeaveListener) {
568
+ this.canvas.removeEventListener("pointerleave", this.pointerLeaveListener);
569
+ }
570
+ }
571
+ _optionalChain([this, 'access', _17 => _17.a11yRoot, 'optionalAccess', _18 => _18.remove, 'call', _19 => _19()]);
572
+ _optionalChain([this, 'access', _20 => _20.portalRoot, 'optionalAccess', _21 => _21.remove, 'call', _22 => _22()]);
573
+ this.a11yElements.clear();
574
+ _optionalChain([this, 'access', _23 => _23.pointRenderer, 'optionalAccess', _24 => _24.destroy, 'call', _25 => _25()]);
575
+ _optionalChain([this, 'access', _26 => _26.glCanvas, 'optionalAccess', _27 => _27.remove, 'call', _28 => _28()]);
576
+ _optionalChain([this, 'access', _29 => _29.gpuCanvas, 'optionalAccess', _30 => _30.remove, 'call', _31 => _31()]);
577
+ this.gpuCanvas = null;
578
+ this.gpuContext = null;
579
+ if (this.recoveryTimerId) {
580
+ clearTimeout(this.recoveryTimerId);
581
+ this.recoveryTimerId = null;
582
+ }
583
+ if (this.manager) {
584
+ this.manager.destroy();
585
+ this.manager = null;
586
+ }
587
+ }
588
+ setupEvents() {
589
+ if (typeof window !== "undefined" && !this.disableWindowResize) {
590
+ window.addEventListener("resize", this.resizeHandler);
591
+ }
592
+ if (typeof window !== "undefined" && this.canvas && typeof this.canvas.addEventListener === "function") {
593
+ this.pointerMoveListener = (e) => {
594
+ const rect = this.canvas.getBoundingClientRect();
595
+ this.mouseX = e.clientX - rect.left;
596
+ this.mouseY = e.clientY - rect.top;
597
+ };
598
+ this.pointerLeaveListener = () => {
599
+ this.mouseX = -9999;
600
+ this.mouseY = -9999;
601
+ };
602
+ this.canvas.addEventListener("pointermove", this.pointerMoveListener);
603
+ this.canvas.addEventListener("pointerleave", this.pointerLeaveListener);
604
+ }
605
+ }
606
+ /**
607
+ * Begin the `requestAnimationFrame` render loop.
608
+ *
609
+ * Idempotent — calling `start()` on an already-running scene is a no-op.
610
+ */
611
+ start() {
612
+ if (this.isRunning) return;
613
+ if ((this.width === 0 || this.height === 0) && !this.hasWarnedZeroSize) {
614
+ console.warn(
615
+ `[VectoJS] Scene started with width or height set to 0 (width: ${this.width}, height: ${this.height}). Entities may not render or simulate correctly. Please call scene.resize(width, height) to set valid dimensions.`
616
+ );
617
+ this.hasWarnedZeroSize = true;
618
+ }
619
+ this.isRunning = true;
620
+ this.lastTime = typeof performance !== "undefined" ? performance.now() : 0;
621
+ this.scheduleFrame();
622
+ const isTextFocused = this.focusedA11yElement instanceof HTMLInputElement || this.focusedA11yElement instanceof HTMLTextAreaElement;
623
+ if (isTextFocused && this.renderMode === "onDemand" && !this.caretBlinkTimer) {
624
+ this.caretBlinkTimer = setInterval(() => {
625
+ this.markDirty();
626
+ }, 500);
627
+ }
628
+ }
629
+ /** Schedule the next frame, or no-op where `requestAnimationFrame` is absent (SSR). */
630
+ scheduleFrame() {
631
+ if (typeof requestAnimationFrame !== "undefined") {
632
+ requestAnimationFrame((t) => this.loop(t));
633
+ }
634
+ }
635
+ /**
636
+ * Halt the render loop after the current frame completes.
637
+ *
638
+ * Call {@link start} again to resume rendering.
639
+ */
640
+ stop() {
641
+ this.isRunning = false;
642
+ if (this.caretBlinkTimer) {
643
+ clearInterval(this.caretBlinkTimer);
644
+ this.caretBlinkTimer = null;
645
+ }
646
+ }
647
+ /**
648
+ * Mark the scene as needing a redraw on the next frame.
649
+ *
650
+ * Only meaningful in `onDemand` {@link renderMode}: call it after mutating
651
+ * entity state outside of {@link Entity.animate} so the change is rendered.
652
+ */
653
+ markDirty() {
654
+ this.dirty = true;
655
+ }
656
+ /** True when any node in the subtree has a pending animation. */
657
+ hasAnyPendingAnimation(node) {
658
+ if (node.hasPendingAnimations()) return true;
659
+ for (const child of node.children) {
660
+ if (this.hasAnyPendingAnimation(child)) return true;
661
+ }
662
+ return false;
663
+ }
664
+ /** True when any node in the subtree is interactive (drives a11y sync). */
665
+ hasAnyInteractive(node) {
666
+ if (node.interactive) return true;
667
+ for (const child of node.children) {
668
+ if (this.hasAnyInteractive(child)) return true;
669
+ }
670
+ return false;
671
+ }
672
+ syncA11y(node) {
673
+ if (!this.a11yRoot) return;
674
+ if (node.isDOMPortal) {
675
+ return;
676
+ }
677
+ if (node.interactive && (node.width > 0 || node.a11yFullViewport)) {
678
+ let el = this.a11yElements.get(node.id);
679
+ const attrs = node.getA11yAttributes();
680
+ const expectedTag = attrs.tag || "div";
681
+ if (el && el.tagName.toLowerCase() !== expectedTag.toLowerCase()) {
682
+ if (el === this.focusedA11yElement) {
683
+ this.focusedA11yElement = null;
684
+ if (this.caretBlinkTimer) {
685
+ clearInterval(this.caretBlinkTimer);
686
+ this.caretBlinkTimer = null;
687
+ }
688
+ }
689
+ if (el.parentNode === this.a11yRoot) {
690
+ this.a11yRoot.removeChild(el);
691
+ }
692
+ this.a11yElements.delete(node.id);
693
+ el = void 0;
694
+ this.a11yNeedsReorder = true;
695
+ }
696
+ if (!el) {
697
+ el = document.createElement(expectedTag);
698
+ el.id = node.id;
699
+ el.setAttribute("data-vecto-id", node.id);
700
+ el.style.position = "absolute";
701
+ el.style.pointerEvents = "auto";
702
+ el.style.touchAction = "pinch-zoom";
703
+ el.style.margin = "0";
704
+ el.style.padding = "0";
705
+ el.style.outline = "none";
706
+ el.style.cursor = node.a11yFullViewport ? "default" : "pointer";
707
+ if (this.debugA11y) {
708
+ el.style.backgroundColor = "rgba(56, 189, 248, 0.05)";
709
+ el.style.border = "1px dashed rgba(56, 189, 248, 0.4)";
710
+ } else {
711
+ el.style.opacity = "0";
712
+ el.style.border = "none";
713
+ el.style.background = "transparent";
714
+ }
715
+ el.addEventListener("click", (e) => {
716
+ node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("click", node, e));
717
+ });
718
+ el.addEventListener("mouseenter", (e) => {
719
+ if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.2)";
720
+ node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("hover", node, e, false));
721
+ });
722
+ el.addEventListener("mouseleave", (e) => {
723
+ if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.05)";
724
+ node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("pointerleave", node, e, false));
725
+ });
726
+ const capEl = el;
727
+ el.addEventListener("pointerdown", (e) => {
728
+ if (typeof capEl.setPointerCapture === "function") capEl.setPointerCapture(e.pointerId);
729
+ node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("pointerdown", node, e));
730
+ });
731
+ el.addEventListener("pointerup", (e) => {
732
+ if (typeof capEl.releasePointerCapture === "function")
733
+ capEl.releasePointerCapture(e.pointerId);
734
+ node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("pointerup", node, e));
735
+ });
736
+ el.addEventListener(
737
+ "pointermove",
738
+ (e) => node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("pointermove", node, e))
739
+ );
740
+ el.addEventListener(
741
+ "wheel",
742
+ (e) => {
743
+ node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("wheel", node, e));
744
+ },
745
+ { passive: false }
746
+ );
747
+ el.addEventListener("keydown", (e) => {
748
+ node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("keydown", node, e));
749
+ });
750
+ el.addEventListener("keyup", (e) => {
751
+ node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("keyup", node, e));
752
+ });
753
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
754
+ const input = el;
755
+ let composition = null;
756
+ const forward = () => {
757
+ input._lastSyncedValue = input.value;
758
+ node.emit("change", {
759
+ value: input.value,
760
+ checked: input instanceof HTMLInputElement ? input.checked : void 0,
761
+ selectionStart: _nullishCoalesce(input.selectionStart, () => ( input.value.length)),
762
+ selectionEnd: _nullishCoalesce(input.selectionEnd, () => ( input.value.length)),
763
+ composition
764
+ });
765
+ };
766
+ el.addEventListener("input", forward);
767
+ el.addEventListener("change", forward);
768
+ el.addEventListener("keyup", forward);
769
+ el.addEventListener("click", forward);
770
+ el.addEventListener("select", forward);
771
+ el.addEventListener("compositionstart", () => {
772
+ composition = { start: _nullishCoalesce(input.selectionStart, () => ( input.value.length)), length: 0 };
773
+ forward();
774
+ });
775
+ el.addEventListener("compositionupdate", (e) => {
776
+ const data = _nullishCoalesce(e.data, () => ( ""));
777
+ composition = { start: _nullishCoalesce(_optionalChain([composition, 'optionalAccess', _32 => _32.start]), () => ( 0)), length: data.length };
778
+ forward();
779
+ });
780
+ el.addEventListener("compositionend", () => {
781
+ composition = null;
782
+ forward();
783
+ });
784
+ }
785
+ const isTextInput = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement;
786
+ el.addEventListener("focus", () => {
787
+ this.focusedA11yElement = el;
788
+ node.emit("focus", {});
789
+ if (isTextInput && this.renderMode === "onDemand" && this.isRunning && !this.caretBlinkTimer) {
790
+ this.caretBlinkTimer = setInterval(() => {
791
+ this.markDirty();
792
+ }, 500);
793
+ }
794
+ });
795
+ el.addEventListener("blur", () => {
796
+ if (this.focusedA11yElement === el) {
797
+ this.focusedA11yElement = null;
798
+ }
799
+ const isTextFocused = this.focusedA11yElement instanceof HTMLInputElement || this.focusedA11yElement instanceof HTMLTextAreaElement;
800
+ if (!isTextFocused && this.caretBlinkTimer) {
801
+ clearInterval(this.caretBlinkTimer);
802
+ this.caretBlinkTimer = null;
803
+ }
804
+ node.emit("blur", {});
805
+ });
806
+ const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
807
+ "button",
808
+ "switch",
809
+ "checkbox",
810
+ "radio",
811
+ "link",
812
+ "tab",
813
+ "menuitem",
814
+ "slider",
815
+ "combobox"
816
+ ]);
817
+ const nativelyFocusable = el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement || el instanceof HTMLAnchorElement && el.hasAttribute("href");
818
+ if (!nativelyFocusable && attrs.role && INTERACTIVE_ROLES.has(attrs.role)) {
819
+ el.setAttribute("tabindex", "0");
820
+ el.addEventListener("keydown", (e) => {
821
+ if (e.key === "Enter" || e.key === " ") {
822
+ e.preventDefault();
823
+ node.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)("click", node, e));
824
+ }
825
+ });
826
+ }
827
+ if (node.a11yFullViewport) {
828
+ this.a11yRoot.insertBefore(el, this.a11yRoot.firstChild);
829
+ } else {
830
+ this.a11yRoot.appendChild(el);
831
+ }
832
+ this.a11yElements.set(node.id, el);
833
+ this.a11yNeedsReorder = true;
834
+ }
835
+ if (attrs.role !== void 0 && el.getAttribute("role") !== attrs.role) {
836
+ el.setAttribute("role", attrs.role);
837
+ }
838
+ if (attrs.label !== void 0 && el.getAttribute("aria-label") !== attrs.label) {
839
+ el.setAttribute("aria-label", attrs.label);
840
+ }
841
+ if (attrs.inputType !== void 0 && el.getAttribute("type") !== attrs.inputType) {
842
+ el.setAttribute("type", attrs.inputType);
843
+ }
844
+ if (attrs.placeholder !== void 0 && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
845
+ if (el.placeholder !== attrs.placeholder) el.placeholder = attrs.placeholder;
846
+ }
847
+ if (attrs.href !== void 0 && el instanceof HTMLAnchorElement) {
848
+ if (el.getAttribute("href") !== attrs.href) el.setAttribute("href", attrs.href);
849
+ }
850
+ if (el instanceof HTMLImageElement) {
851
+ if (attrs.src !== void 0 && el.src !== attrs.src) el.src = attrs.src;
852
+ if (attrs.alt !== void 0 && el.alt !== attrs.alt) el.alt = attrs.alt;
853
+ }
854
+ if (attrs.checked !== void 0) {
855
+ if (el instanceof HTMLInputElement) {
856
+ if (el.checked !== attrs.checked) el.checked = attrs.checked;
857
+ } else if (el.getAttribute("aria-checked") !== String(attrs.checked)) {
858
+ el.setAttribute("aria-checked", String(attrs.checked));
859
+ }
860
+ }
861
+ if (attrs.disabled !== void 0) {
862
+ if ("disabled" in el) {
863
+ if (el.disabled !== attrs.disabled) el.disabled = attrs.disabled;
864
+ } else if (el.getAttribute("aria-disabled") !== String(attrs.disabled)) {
865
+ el.setAttribute("aria-disabled", String(attrs.disabled));
866
+ }
867
+ }
868
+ if (attrs.expanded !== void 0 && el.getAttribute("aria-expanded") !== String(attrs.expanded)) {
869
+ el.setAttribute("aria-expanded", String(attrs.expanded));
870
+ }
871
+ if (attrs.controls !== void 0 && el.getAttribute("aria-controls") !== attrs.controls) {
872
+ el.setAttribute("aria-controls", attrs.controls);
873
+ }
874
+ if (attrs.haspopup !== void 0 && el.getAttribute("aria-haspopup") !== attrs.haspopup) {
875
+ el.setAttribute("aria-haspopup", attrs.haspopup);
876
+ }
877
+ if (attrs.selected !== void 0 && el.getAttribute("aria-selected") !== String(attrs.selected)) {
878
+ el.setAttribute("aria-selected", String(attrs.selected));
879
+ }
880
+ if (attrs.activedescendant !== void 0 && el.getAttribute("aria-activedescendant") !== attrs.activedescendant) {
881
+ el.setAttribute("aria-activedescendant", attrs.activedescendant);
882
+ }
883
+ if (attrs.valuemin !== void 0 && el.getAttribute("aria-valuemin") !== attrs.valuemin) {
884
+ el.setAttribute("aria-valuemin", attrs.valuemin);
885
+ }
886
+ if (attrs.valuemax !== void 0 && el.getAttribute("aria-valuemax") !== attrs.valuemax) {
887
+ el.setAttribute("aria-valuemax", attrs.valuemax);
888
+ }
889
+ if (attrs.value !== void 0) {
890
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
891
+ if (el.value !== attrs.value) {
892
+ const userTyped = el._lastSyncedValue;
893
+ if (attrs.value !== userTyped || document.activeElement !== el) {
894
+ el.value = attrs.value;
895
+ el._lastSyncedValue = attrs.value;
896
+ }
897
+ }
898
+ } else if (el.getAttribute("aria-valuenow") !== attrs.value) {
899
+ el.setAttribute("aria-valuenow", attrs.value);
900
+ }
901
+ }
902
+ if (node.a11yFullViewport) {
903
+ el.style.left = "0px";
904
+ el.style.top = "0px";
905
+ el.style.width = `${this.width}px`;
906
+ el.style.height = `${this.height}px`;
907
+ el.style.transform = "";
908
+ } else {
909
+ const pos = node.getGlobalPosition();
910
+ el.style.left = `${pos.x + node.a11yOffsetX}px`;
911
+ el.style.top = `${pos.y + node.a11yOffsetY}px`;
912
+ el.style.width = `${node.width * node.scaleX}px`;
913
+ el.style.height = `${node.height * node.scaleY}px`;
914
+ el.style.transform = `rotate(${node.rotation}rad)`;
915
+ }
916
+ }
917
+ for (const child of node.children) this.syncA11y(child);
918
+ if (node === this.root) {
919
+ for (const overlay of this.overlayRoot.children) this.syncA11y(overlay);
920
+ }
921
+ }
922
+ enforceA11yDomOrder() {
923
+ if (!this.a11yRoot) return;
924
+ this.fullViewportElements.length = 0;
925
+ this.normalElements.length = 0;
926
+ this.activeIds.clear();
927
+ const collect = (node) => {
928
+ if (node.isDOMPortal) return;
929
+ if (node.interactive && (node.width > 0 || node.a11yFullViewport)) {
930
+ const el = this.a11yElements.get(node.id);
931
+ if (el) {
932
+ this.activeIds.add(node.id);
933
+ if (node.a11yFullViewport) this.fullViewportElements.push(el);
934
+ else this.normalElements.push(el);
935
+ }
936
+ }
937
+ for (const child of node.children) collect(child);
938
+ if (node === this.root) {
939
+ for (const overlay of this.overlayRoot.children) collect(overlay);
940
+ }
941
+ };
942
+ collect(this.root);
943
+ let elementsPruned = false;
944
+ for (const [id, el] of this.a11yElements.entries()) {
945
+ if (!this.activeIds.has(id)) {
946
+ elementsPruned = true;
947
+ if (el === this.focusedA11yElement) {
948
+ this.focusedA11yElement = null;
949
+ if (this.caretBlinkTimer) {
950
+ clearInterval(this.caretBlinkTimer);
951
+ this.caretBlinkTimer = null;
952
+ }
953
+ }
954
+ if (el.parentNode === this.a11yRoot) {
955
+ this.a11yRoot.removeChild(el);
956
+ }
957
+ this.a11yElements.delete(id);
958
+ }
959
+ }
960
+ if (elementsPruned) {
961
+ this.a11yNeedsReorder = true;
962
+ }
963
+ if (!this.a11yNeedsReorder) return;
964
+ const fullLen = this.fullViewportElements.length;
965
+ const normalLen = this.normalElements.length;
966
+ const totalLen = fullLen + normalLen;
967
+ for (let i = 0; i < totalLen; i++) {
968
+ const expected = i < fullLen ? this.fullViewportElements[i] : this.normalElements[i - fullLen];
969
+ const current = this.a11yRoot.childNodes[i];
970
+ if (current !== expected) {
971
+ this.a11yRoot.insertBefore(expected, current || null);
972
+ }
973
+ }
974
+ this.a11yNeedsReorder = false;
975
+ }
976
+ getA11yTree() {
977
+ const map = /* @__PURE__ */ new Map();
978
+ const roots = [];
979
+ const traverse = (node, parentNode) => {
980
+ if (node.isDOMPortal) return;
981
+ let currentA11yNode = null;
982
+ if (node.interactive && (node.width > 0 || node.a11yFullViewport)) {
983
+ const el = this.a11yElements.get(node.id);
984
+ if (el) {
985
+ const attrs = node.getA11yAttributes();
986
+ currentA11yNode = {
987
+ id: node.id,
988
+ tag: el.tagName.toLowerCase(),
989
+ role: el.getAttribute("role") || void 0,
990
+ label: el.getAttribute("aria-label") || void 0,
991
+ value: attrs.value,
992
+ checked: attrs.checked,
993
+ expanded: attrs.expanded,
994
+ valuemin: attrs.valuemin,
995
+ valuemax: attrs.valuemax,
996
+ children: []
997
+ };
998
+ map.set(node.id, currentA11yNode);
999
+ const parentA11y = parentNode ? map.get(parentNode.id) : null;
1000
+ if (parentA11y) {
1001
+ parentA11y.children.push(currentA11yNode);
1002
+ } else {
1003
+ roots.push(currentA11yNode);
1004
+ }
1005
+ }
1006
+ }
1007
+ for (const child of node.children) {
1008
+ traverse(child, currentA11yNode ? node : parentNode);
1009
+ }
1010
+ if (node === this.root) {
1011
+ for (const overlay of this.overlayRoot.children) {
1012
+ traverse(overlay, currentA11yNode ? node : parentNode);
1013
+ }
1014
+ }
1015
+ };
1016
+ traverse(this.root, null);
1017
+ return roots;
1018
+ }
1019
+ renderPortalDOM(portal, te, tf, a, b, c, d) {
1020
+ if (!this.portalRoot) return;
1021
+ this.activePortalsThisFrame.add(portal.id);
1022
+ this.portalEntities.set(portal.id, portal);
1023
+ if (portal.domElement.parentElement !== this.portalRoot) {
1024
+ this.portalRoot.appendChild(portal.domElement);
1025
+ }
1026
+ if (!portal.domElement.hasAttribute("data-vecto-id")) {
1027
+ portal.domElement.setAttribute("data-vecto-id", portal.id);
1028
+ }
1029
+ const transformStr = `matrix(${a}, ${b}, ${c}, ${d}, ${te}, ${tf})`;
1030
+ let widthStr = "";
1031
+ let heightStr = "";
1032
+ if (portal.width > 0) widthStr = `${portal.width}px`;
1033
+ if (portal.height > 0) heightStr = `${portal.height}px`;
1034
+ const zIndexStr = String(this.renderOrderCounter++);
1035
+ if (portal.lastWidth !== widthStr) {
1036
+ portal.domElement.style.width = widthStr;
1037
+ portal.lastWidth = widthStr;
1038
+ }
1039
+ if (portal.lastHeight !== heightStr) {
1040
+ portal.domElement.style.height = heightStr;
1041
+ portal.lastHeight = heightStr;
1042
+ }
1043
+ if (portal.lastTransform !== transformStr) {
1044
+ portal.domElement.style.left = "0px";
1045
+ portal.domElement.style.top = "0px";
1046
+ portal.domElement.style.transform = transformStr;
1047
+ portal.lastTransform = transformStr;
1048
+ }
1049
+ if (portal.lastZIndex !== zIndexStr) {
1050
+ portal.domElement.style.zIndex = zIndexStr;
1051
+ portal.lastZIndex = zIndexStr;
1052
+ }
1053
+ }
1054
+ reconcilePortals() {
1055
+ if (!this.portalRoot) return;
1056
+ for (const oldId of this.activePortalsPrevFrame) {
1057
+ if (!this.activePortalsThisFrame.has(oldId)) {
1058
+ const portal = this.portalEntities.get(oldId);
1059
+ if (portal) {
1060
+ if (portal.domElement.parentElement === this.portalRoot && (!portal.scene || portal.scene === this)) {
1061
+ portal.domElement.remove();
1062
+ }
1063
+ this.portalEntities.delete(oldId);
1064
+ }
1065
+ }
1066
+ }
1067
+ this.activePortalsPrevFrame = new Set(this.activePortalsThisFrame);
1068
+ this.activePortalsThisFrame.clear();
1069
+ }
1070
+ /**
1071
+ * The frame-rate cap actually in effect: the explicit {@link maxFPS}, further
1072
+ * lowered to {@link REDUCED_MOTION_FPS} when the OS requests reduced motion
1073
+ * (and {@link respectReducedMotion} is on). `0` means uncapped.
1074
+ */
1075
+ effectiveMaxFPS() {
1076
+ const reduced = this.respectReducedMotion && !!_optionalChain([this, 'access', _33 => _33.reducedMotionQuery, 'optionalAccess', _34 => _34.matches]);
1077
+ if (reduced)
1078
+ return this.maxFPS > 0 ? Math.min(this.maxFPS, REDUCED_MOTION_FPS) : REDUCED_MOTION_FPS;
1079
+ return this.maxFPS;
1080
+ }
1081
+ loop(time) {
1082
+ if (!this.isRunning) return;
1083
+ let cap = this.effectiveMaxFPS();
1084
+ const isStatic = !this.dirty && !this.hasAnyPendingAnimation(this.root) && !this.hasAnyPendingAnimation(this.overlayRoot);
1085
+ if (isStatic && this.renderMode === "always" && this.maxFPS > 0) {
1086
+ cap = Math.min(cap, 2);
1087
+ }
1088
+ if (cap > 0 && time - this.lastTime < 1e3 / cap - 1) {
1089
+ this.scheduleFrame();
1090
+ return;
1091
+ }
1092
+ const dt = time - this.lastTime;
1093
+ this.lastTime = time;
1094
+ if (this.renderMode === "onDemand" && isStatic) {
1095
+ this.scheduleFrame();
1096
+ return;
1097
+ }
1098
+ this.render(this.renderer, dt, time);
1099
+ const hasActiveAnimation = this.hasAnyPendingAnimation(this.root) || this.hasAnyPendingAnimation(this.overlayRoot);
1100
+ if (hasActiveAnimation) {
1101
+ this.a11yPendingSyncAfterAnimation = true;
1102
+ } else {
1103
+ const hasInteractive = this.hasAnyInteractive(this.root) || this.hasAnyInteractive(this.overlayRoot);
1104
+ const shouldSyncInterval = this.a11ySyncInterval <= 0 || time - this.lastA11ySync >= this.a11ySyncInterval;
1105
+ if ((hasInteractive || this.a11yElements.size > 0) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1106
+ this.lastA11ySync = time;
1107
+ if (hasInteractive) {
1108
+ this.syncA11y(this.root);
1109
+ }
1110
+ this.enforceA11yDomOrder();
1111
+ this.a11yPendingSyncAfterAnimation = false;
1112
+ }
1113
+ }
1114
+ this.dirty = false;
1115
+ this.scheduleFrame();
1116
+ }
1117
+ /**
1118
+ * Render the entire scene graph onto the specified renderer.
1119
+ *
1120
+ * @param renderer - The renderer instance to draw to.
1121
+ * @param dt - Delta time in milliseconds (default 0).
1122
+ * @param time - Current absolute time in milliseconds (default 0).
1123
+ */
1124
+ render(renderer, dt = 0, time = 0) {
1125
+ if (this.a11yRoot && this.canvas.parentElement) {
1126
+ const parentStyle = this.canvas.parentElement.style;
1127
+ if (!parentStyle.position || parentStyle.position === "static") {
1128
+ parentStyle.position = "relative";
1129
+ }
1130
+ }
1131
+ this.renderOrderCounter = 0;
1132
+ this.activePortalsThisFrame.clear();
1133
+ const computeEntities = [];
1134
+ const collectComputeEntities = (node) => {
1135
+ if (node instanceof ComputeParticleEntity) {
1136
+ computeEntities.push(node);
1137
+ }
1138
+ for (const child of node.children) {
1139
+ collectComputeEntities(child);
1140
+ }
1141
+ };
1142
+ collectComputeEntities(this.root);
1143
+ for (const overlay of this.overlayRoot.children) {
1144
+ collectComputeEntities(overlay);
1145
+ }
1146
+ if (computeEntities.length > 0) {
1147
+ if (!this.device && !this.webgpuDisabled && !this.initializingWebGPU && !this.deviceLost) {
1148
+ this.initializingWebGPU = true;
1149
+ this.initWebGPUContext(computeEntities).then((newDevice) => {
1150
+ this.device = newDevice;
1151
+ this.initializingWebGPU = false;
1152
+ const format = navigator.gpu ? navigator.gpu.getPreferredCanvasFormat() : "rgba8unorm";
1153
+ if (_Scene.webgpuManagerClass) {
1154
+ this.manager = new _Scene.webgpuManagerClass(newDevice);
1155
+ } else if (this.particleBackend === "webgpu") {
1156
+ throw new Error(
1157
+ "WebGPU particle manager is not registered. Please call Scene.registerWebGPUParticleSystemManager(WebGPUParticleSystemManager) first."
1158
+ );
1159
+ }
1160
+ if (this.manager) {
1161
+ this.manager.initPipelines(format);
1162
+ for (const entity of computeEntities) {
1163
+ this.manager.setupEntityResources(entity);
1164
+ if (entity.gpuStorageBuffer) {
1165
+ newDevice.queue.writeBuffer(entity.gpuStorageBuffer, 0, entity.particleData);
1166
+ }
1167
+ }
1168
+ }
1169
+ }).catch((err) => {
1170
+ console.error("Failed to initialize WebGPU:", err);
1171
+ this.webgpuDisabled = true;
1172
+ this.initializingWebGPU = false;
1173
+ });
1174
+ }
1175
+ if (this.device && this.manager && !this.deviceLost && !this.webgpuDisabled) {
1176
+ try {
1177
+ const commandEncoder = this.device.createCommandEncoder();
1178
+ const computePass = commandEncoder.beginComputePass();
1179
+ for (const entity of computeEntities) {
1180
+ if (!entity.gpuStorageBuffer || entity.needsInit) {
1181
+ if (!entity.gpuStorageBuffer) {
1182
+ this.manager.setupEntityResources(entity);
1183
+ }
1184
+ this.device.queue.writeBuffer(entity.gpuStorageBuffer, 0, entity.particleData);
1185
+ entity.needsInit = false;
1186
+ }
1187
+ this.manager.recordComputePass(
1188
+ computePass,
1189
+ entity,
1190
+ dt / 1e3,
1191
+ this.mouseX,
1192
+ this.mouseY,
1193
+ this.width,
1194
+ this.height
1195
+ );
1196
+ }
1197
+ computePass.end();
1198
+ if (this.gpuContext) {
1199
+ const view = this.gpuContext.getCurrentTexture().createView();
1200
+ const renderPassDescriptor = {
1201
+ colorAttachments: [
1202
+ {
1203
+ view,
1204
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1205
+ loadOp: "clear",
1206
+ storeOp: "store"
1207
+ }
1208
+ ]
1209
+ };
1210
+ const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);
1211
+ for (const entity of computeEntities) {
1212
+ this.manager.recordRenderPass(renderPass, entity);
1213
+ }
1214
+ renderPass.end();
1215
+ }
1216
+ this.device.queue.submit([commandEncoder.finish()]);
1217
+ } catch (e) {
1218
+ console.error("WebGPU frame execution failed. Falling back.", e);
1219
+ this.deviceLost = true;
1220
+ this.device = null;
1221
+ this.recreateWebGPUDeviceWithRetry(computeEntities);
1222
+ }
1223
+ } else {
1224
+ for (const entity of computeEntities) {
1225
+ entity.updateCPU(dt / 1e3, this.mouseX, this.mouseY, this.width, this.height);
1226
+ }
1227
+ }
1228
+ }
1229
+ renderer.clear();
1230
+ const isMainRenderer = renderer === this.renderer;
1231
+ if (isMainRenderer) {
1232
+ _optionalChain([this, 'access', _35 => _35.pointRenderer, 'optionalAccess', _36 => _36.begin, 'call', _37 => _37()]);
1233
+ }
1234
+ const vw = this.width;
1235
+ const vh = this.height;
1236
+ const renderNode = (node, pa, pb, pc, pd, pe, pf) => {
1237
+ node.update(dt, time);
1238
+ const cos = Math.cos(node.rotation);
1239
+ const sin = Math.sin(node.rotation);
1240
+ const te = pa * node.x + pc * node.y + pe;
1241
+ const tf = pb * node.x + pd * node.y + pf;
1242
+ const sxCos = node.scaleX * cos;
1243
+ const sxSin = node.scaleX * sin;
1244
+ const syCos = node.scaleY * cos;
1245
+ const sySin = node.scaleY * sin;
1246
+ const a = pa * sxCos + pc * sxSin;
1247
+ const b = pb * sxCos + pd * sxSin;
1248
+ const c = pa * -sySin + pc * syCos;
1249
+ const d = pb * -sySin + pd * syCos;
1250
+ const a11yEl = this.a11yElements.get(node.id);
1251
+ if (a11yEl) {
1252
+ a11yEl.style.zIndex = String(this.renderOrderCounter++);
1253
+ }
1254
+ if (node.isDOMPortal) {
1255
+ this.renderPortalDOM(node, te, tf, a, b, c, d);
1256
+ return;
1257
+ }
1258
+ let visible = true;
1259
+ const bounds = node.getBounds();
1260
+ if (bounds) {
1261
+ let minX = Infinity;
1262
+ let minY = Infinity;
1263
+ let maxX = -Infinity;
1264
+ let maxY = -Infinity;
1265
+ for (let i = 0; i < 4; i++) {
1266
+ const lx = i & 1 ? bounds.x + bounds.width : bounds.x;
1267
+ const ly = i & 2 ? bounds.y + bounds.height : bounds.y;
1268
+ const wx = a * lx + c * ly + te;
1269
+ const wy = b * lx + d * ly + tf;
1270
+ if (wx < minX) minX = wx;
1271
+ if (wx > maxX) maxX = wx;
1272
+ if (wy < minY) minY = wy;
1273
+ if (wy > maxY) maxY = wy;
1274
+ }
1275
+ visible = maxX >= 0 && minX <= vw && maxY >= 0 && minY <= vh;
1276
+ }
1277
+ if (!visible && node.children.length === 0) return;
1278
+ if (node.children.length === 0 && node.scaleX === node.scaleY) {
1279
+ const bc = node.getBatchCircle();
1280
+ if (bc) {
1281
+ if (visible) {
1282
+ if (isMainRenderer && this.pointRenderer) {
1283
+ this.pointRenderer.addCircle(
1284
+ te,
1285
+ tf,
1286
+ bc.radius * Math.hypot(a, b),
1287
+ bc.color,
1288
+ node.opacity
1289
+ );
1290
+ } else {
1291
+ renderer.fillCircle(node.x, node.y, bc.radius * node.scaleX, bc.color, node.opacity);
1292
+ }
1293
+ }
1294
+ return;
1295
+ }
1296
+ if (isMainRenderer && this.pointRenderer) {
1297
+ const br = node.getBatchRect();
1298
+ if (br) {
1299
+ if (visible) {
1300
+ const ws = Math.hypot(a, b);
1301
+ this.pointRenderer.addRect(
1302
+ te,
1303
+ tf,
1304
+ br.width * ws,
1305
+ br.height * ws,
1306
+ br.color,
1307
+ node.opacity,
1308
+ Math.atan2(b, a)
1309
+ );
1310
+ }
1311
+ return;
1312
+ }
1313
+ }
1314
+ }
1315
+ renderer.flush();
1316
+ renderer.save();
1317
+ renderer.translate(node.x, node.y);
1318
+ renderer.scale(node.scaleX, node.scaleY);
1319
+ renderer.rotate(node.rotation);
1320
+ renderer.setGlobalAlpha(node.opacity);
1321
+ if (visible) {
1322
+ if (node instanceof ComputeParticleEntity) {
1323
+ if (this.deviceLost || this.webgpuDisabled || !this.device || !this.manager) {
1324
+ this.renderCPUParticles(renderer, node);
1325
+ }
1326
+ } else {
1327
+ node.render(renderer);
1328
+ }
1329
+ }
1330
+ if (node.clipChildren) {
1331
+ renderer.clip(0, 0, node.width, node.height);
1332
+ }
1333
+ for (const child of node.children) {
1334
+ renderNode(child, a, b, c, d, te, tf);
1335
+ }
1336
+ renderer.flush();
1337
+ renderer.restore();
1338
+ };
1339
+ renderNode(this.root, 1, 0, 0, 1, 0, 0);
1340
+ for (const overlay of this.overlayRoot.children) {
1341
+ renderNode(overlay, 1, 0, 0, 1, 0, 0);
1342
+ }
1343
+ this.reconcilePortals();
1344
+ renderer.flush();
1345
+ if (isMainRenderer) {
1346
+ _optionalChain([this, 'access', _38 => _38.pointRenderer, 'optionalAccess', _39 => _39.flush, 'call', _40 => _40()]);
1347
+ }
1348
+ }
1349
+ /**
1350
+ * Export the current scene state to a lightweight, flat SVG XML string.
1351
+ */
1352
+ toSVG() {
1353
+ const renderer = new (0, _chunkLIX7DJTIjs.SVGRenderer)(this.width, this.height);
1354
+ this.render(renderer, 0, 0);
1355
+ return renderer.toXMLString();
1356
+ }
1357
+ /**
1358
+ * Manually resize the Scene's viewport.
1359
+ */
1360
+ resize(width, height) {
1361
+ this.width = width;
1362
+ this.height = height;
1363
+ if (typeof this.renderer.resize === "function") {
1364
+ this.renderer.resize(width, height);
1365
+ }
1366
+ _optionalChain([this, 'access', _41 => _41.pointRenderer, 'optionalAccess', _42 => _42.resize, 'call', _43 => _43(width, height)]);
1367
+ this.markDirty();
1368
+ }
1369
+ /**
1370
+ * Gets the accessibility DOM element projected for the given entity ID.
1371
+ */
1372
+ getA11yElement(entityId) {
1373
+ return this.a11yElements.get(entityId);
1374
+ }
1375
+ /**
1376
+ * Gets the root entity of the scene.
1377
+ */
1378
+ getRoot() {
1379
+ return this.root;
1380
+ }
1381
+ /**
1382
+ * Finds the topmost interactive entity at the given coordinates.
1383
+ */
1384
+ findEntityAt(x, y) {
1385
+ const overlayHit = this.findHitRecursively(this.overlayRoot, x, y);
1386
+ if (overlayHit) return overlayHit;
1387
+ return this.findHitRecursively(this.root, x, y);
1388
+ }
1389
+ async initWebGPUContext(entities) {
1390
+ if (!navigator.gpu) {
1391
+ throw new Error("WebGPU not supported on this platform.");
1392
+ }
1393
+ const adapter = await navigator.gpu.requestAdapter();
1394
+ if (!adapter) {
1395
+ throw new Error("No GPUAdapter found.");
1396
+ }
1397
+ const device = await adapter.requestDevice();
1398
+ if (typeof document !== "undefined" && !this.gpuCanvas) {
1399
+ const gpuCanvas = document.createElement("canvas");
1400
+ gpuCanvas.width = this.width;
1401
+ gpuCanvas.height = this.height;
1402
+ gpuCanvas.style.position = "absolute";
1403
+ gpuCanvas.style.top = "0";
1404
+ gpuCanvas.style.left = "0";
1405
+ gpuCanvas.style.pointerEvents = "none";
1406
+ gpuCanvas.style.zIndex = "6";
1407
+ if (this.canvas.parentElement) {
1408
+ this.canvas.parentElement.appendChild(gpuCanvas);
1409
+ }
1410
+ this.gpuCanvas = gpuCanvas;
1411
+ this.gpuContext = gpuCanvas.getContext("webgpu");
1412
+ }
1413
+ if (this.gpuContext) {
1414
+ this.gpuContext.configure({
1415
+ device,
1416
+ format: navigator.gpu.getPreferredCanvasFormat(),
1417
+ alphaMode: "premultiplied"
1418
+ });
1419
+ }
1420
+ this.setupDeviceLostHandler(device, entities);
1421
+ return device;
1422
+ }
1423
+ setupDeviceLostHandler(device, entities) {
1424
+ device.lost.then((info) => {
1425
+ if (info.reason === "destroyed") return;
1426
+ console.warn(`WebGPU device lost: ${info.message}`);
1427
+ this.deviceLost = true;
1428
+ this.device = null;
1429
+ this.recreateWebGPUDeviceWithRetry(entities);
1430
+ });
1431
+ }
1432
+ recreateWebGPUDeviceWithRetry(entities, attempt = 0) {
1433
+ if (this.destroyed) return;
1434
+ if (attempt >= 3) {
1435
+ console.error(
1436
+ "Failed to recover WebGPU device after 3 retries. Remaining on fallback renderer."
1437
+ );
1438
+ this.webgpuDisabled = true;
1439
+ this.deviceLost = true;
1440
+ return;
1441
+ }
1442
+ for (const entity of entities) {
1443
+ entity.destroyGPUResources();
1444
+ }
1445
+ if (this.manager) {
1446
+ this.manager.destroy();
1447
+ this.manager = null;
1448
+ }
1449
+ const backoff = Math.pow(2, attempt) * 1e3;
1450
+ if (this.recoveryTimerId) clearTimeout(this.recoveryTimerId);
1451
+ this.recoveryTimerId = setTimeout(() => {
1452
+ if (this.destroyed) return;
1453
+ this.initWebGPUContext(entities).then((newDevice) => {
1454
+ if (this.destroyed) {
1455
+ newDevice.destroy();
1456
+ return;
1457
+ }
1458
+ console.log("Successfully recovered WebGPU device.");
1459
+ this.device = newDevice;
1460
+ this.deviceLost = false;
1461
+ const format = navigator.gpu.getPreferredCanvasFormat();
1462
+ if (_Scene.webgpuManagerClass) {
1463
+ this.manager = new _Scene.webgpuManagerClass(newDevice);
1464
+ } else if (this.particleBackend === "webgpu") {
1465
+ throw new Error(
1466
+ "WebGPU particle manager is not registered. Please call Scene.registerWebGPUParticleSystemManager(WebGPUParticleSystemManager) first."
1467
+ );
1468
+ }
1469
+ if (this.manager) {
1470
+ this.manager.initPipelines(format);
1471
+ for (const entity of entities) {
1472
+ this.manager.setupEntityResources(entity);
1473
+ newDevice.queue.writeBuffer(entity.gpuStorageBuffer, 0, entity.particleData);
1474
+ }
1475
+ }
1476
+ }).catch(() => this.recreateWebGPUDeviceWithRetry(entities, attempt + 1));
1477
+ }, backoff);
1478
+ }
1479
+ renderCPUParticles(renderer, entity) {
1480
+ const data = entity.particleData;
1481
+ const size = entity.maxParticles;
1482
+ const isMain = renderer === this.renderer;
1483
+ for (let i = 0; i < size; i++) {
1484
+ const idx = i * 8;
1485
+ const x = data[idx];
1486
+ const y = data[idx + 1];
1487
+ const pSize = data[idx + 6];
1488
+ const life = data[idx + 7];
1489
+ if (life === 0) continue;
1490
+ const opacity = life < 0 ? entity.opacity : entity.opacity * Math.min(1, life);
1491
+ const scale = life >= 0 ? Math.min(1, life) : 1;
1492
+ if (isMain && this.pointRenderer) {
1493
+ this.pointRenderer.addCircle(x, y, pSize * scale, entity.baseColor, opacity);
1494
+ } else {
1495
+ renderer.fillCircle(x, y, pSize * scale, entity.baseColor, opacity);
1496
+ }
1497
+ }
1498
+ }
1499
+ findHitRecursively(node, x, y) {
1500
+ for (let i = node.children.length - 1; i >= 0; i--) {
1501
+ const hit = this.findHitRecursively(node.children[i], x, y);
1502
+ if (hit) return hit;
1503
+ }
1504
+ if (node.isPointInside && node.isPointInside(x, y)) {
1505
+ return node;
1506
+ }
1507
+ return null;
1508
+ }
1509
+ }, _class2.__initStatic(), _class2.__initStatic2(), _class2);
1510
+
1511
+ // src/components/TextEntity.ts
1512
+ var sharedMeasurer;
1513
+ function defaultMeasurer() {
1514
+ if (sharedMeasurer === void 0) sharedMeasurer = _chunk72WVPMSJjs.createCanvasMeasurer.call(void 0, "sans-serif");
1515
+ return sharedMeasurer;
1516
+ }
1517
+ var TextEntity = (_class3 = class extends _chunk53DAQC3Ujs.Entity {
1518
+
1519
+
1520
+
1521
+
1522
+ __init47() {this.nodes = []}
1523
+
1524
+ __init48() {this.fillStyle = "#94a3b8"}
1525
+ __init49() {this.strokeStyle = null}
1526
+ __init50() {this.hoveredFillStyle = "#ffffff"}
1527
+ __init51() {this.lineWidth = 1}
1528
+ __init52() {this.isHovered = false}
1529
+ constructor(text, atlas, maxWidth, fontSize = 24) {
1530
+ super();_class3.prototype.__init47.call(this);_class3.prototype.__init48.call(this);_class3.prototype.__init49.call(this);_class3.prototype.__init50.call(this);_class3.prototype.__init51.call(this);_class3.prototype.__init52.call(this);;
1531
+ this.text = text;
1532
+ this.atlas = atlas;
1533
+ this.fontSize = fontSize;
1534
+ this.layout = new (0, _chunk72WVPMSJjs.LayoutEngine)(maxWidth, 1e4, defaultMeasurer());
1535
+ this.prepared = this.layout.prepare(this.text, this.atlas, this.fontSize);
1536
+ this.applyLayout();
1537
+ this.interactive = true;
1538
+ this.on("hover", () => this.isHovered = true);
1539
+ this.on("pointerleave", () => this.isHovered = false);
1540
+ }
1541
+ /**
1542
+ * Replace the text content. Runs the **cold** measurement pass (re-segment +
1543
+ * re-measure) since the glyphs changed, then re-lays out.
1544
+ *
1545
+ * @returns `this` for chaining.
1546
+ */
1547
+ setText(text) {
1548
+ this.text = text;
1549
+ this.prepared = this.layout.prepare(this.text, this.atlas, this.fontSize);
1550
+ this.applyLayout();
1551
+ return this;
1552
+ }
1553
+ /**
1554
+ * Change the wrap width and reflow. Cheap **hot** path only — reuses the
1555
+ * cached {@link PreparedText}, doing no re-segmentation or re-measurement.
1556
+ * Ideal for responsive resize.
1557
+ *
1558
+ * @returns `this` for chaining.
1559
+ */
1560
+ setMaxWidth(maxWidth) {
1561
+ this.layout.maxWidth = maxWidth;
1562
+ this.applyLayout();
1563
+ return this;
1564
+ }
1565
+ /** Hot pass: place the cached {@link PreparedText} and refresh the a11y box. */
1566
+ applyLayout() {
1567
+ const result = this.layout.layoutPrepared(this.prepared);
1568
+ this.nodes = result.nodes;
1569
+ this.width = result.totalWidth;
1570
+ this.height = result.totalHeight;
1571
+ this.a11yOffsetY = 0;
1572
+ }
1573
+ isPointInside(globalX, globalY) {
1574
+ const pos = this.getGlobalPosition();
1575
+ const lx = globalX - pos.x;
1576
+ const ly = globalY - pos.y;
1577
+ return lx >= 0 && lx <= this.width && ly >= 0 && ly <= this.height;
1578
+ }
1579
+ render(renderer) {
1580
+ const currentFill = this.isHovered ? this.hoveredFillStyle : this.fillStyle;
1581
+ for (const node of this.nodes) {
1582
+ const glyph = this.atlas[node.char];
1583
+ if (!glyph) {
1584
+ renderer.save();
1585
+ renderer.translate(node.x, node.y + this.fontSize * 0.8);
1586
+ renderer.fillText(node.char, 0, 0, `${this.fontSize}px sans-serif`, currentFill);
1587
+ renderer.restore();
1588
+ continue;
1589
+ }
1590
+ renderer.save();
1591
+ renderer.translate(node.x, node.y);
1592
+ const scale = this.fontSize / glyph.baseSize;
1593
+ renderer.scale(scale, scale);
1594
+ for (const path of glyph.ast.paths) {
1595
+ renderer.beginPath();
1596
+ for (const cmd of path.commands) {
1597
+ if (cmd.type === "M") renderer.moveTo(cmd.x, cmd.y);
1598
+ else if (cmd.type === "L") renderer.lineTo(cmd.x, cmd.y);
1599
+ else if (cmd.type === "C")
1600
+ renderer.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
1601
+ else if (cmd.type === "Z") renderer.closePath();
1602
+ }
1603
+ if (currentFill) {
1604
+ renderer.fill(currentFill);
1605
+ }
1606
+ if (this.strokeStyle) {
1607
+ renderer.stroke(this.strokeStyle, this.lineWidth / scale);
1608
+ }
1609
+ }
1610
+ renderer.restore();
1611
+ }
1612
+ }
1613
+ }, _class3);
1614
+
1615
+ // src/components/GridTextEntity.ts
1616
+ var GridTextEntity = (_class4 = class extends _chunk53DAQC3Ujs.Entity {
1617
+
1618
+ __init53() {this.fillStyle = "#ffffff"}
1619
+ __init54() {this.grid = []}
1620
+ // Array of rows
1621
+ __init55() {this.cols = 0}
1622
+ __init56() {this.rows = 0}
1623
+
1624
+
1625
+ constructor(_atlas, fontSize = 10) {
1626
+ super();_class4.prototype.__init53.call(this);_class4.prototype.__init54.call(this);_class4.prototype.__init55.call(this);_class4.prototype.__init56.call(this);;
1627
+ this.fontSize = fontSize;
1628
+ this.charWidth = fontSize * 1;
1629
+ this.charHeight = fontSize * 1.1;
1630
+ this.interactive = false;
1631
+ }
1632
+ updateGrid(ascii) {
1633
+ this.grid = ascii;
1634
+ this.rows = ascii.length;
1635
+ this.cols = _optionalChain([ascii, 'access', _44 => _44[0], 'optionalAccess', _45 => _45.length]) || 0;
1636
+ }
1637
+ isPointInside(_globalX, _globalY) {
1638
+ return false;
1639
+ }
1640
+ render(renderer) {
1641
+ if (this.rows === 0) return;
1642
+ for (let r = 0; r < this.rows; r++) {
1643
+ const row = this.grid[r];
1644
+ if (!row) continue;
1645
+ for (let c = 0; c < this.cols; c++) {
1646
+ const char = row[c];
1647
+ if (char === " ") continue;
1648
+ const x = c * this.charWidth;
1649
+ const y = r * this.charHeight;
1650
+ renderer.save();
1651
+ renderer.translate(x, y + this.fontSize * 0.8);
1652
+ renderer.fillText(char, 0, 0, `bold ${this.fontSize}px monospace`, this.fillStyle);
1653
+ renderer.restore();
1654
+ }
1655
+ }
1656
+ }
1657
+ }, _class4);
1658
+
1659
+ // src/components/SplineEntity.ts
1660
+ function polySegmentToBezier(seg) {
1661
+ const ax = seg.x_poly[0] || 0;
1662
+ const bx = seg.x_poly[1] || 0;
1663
+ const cx = seg.x_poly[2] || 0;
1664
+ const dx = seg.x_poly[3] || 0;
1665
+ const ay = seg.y_poly[0] || 0;
1666
+ const by = seg.y_poly[1] || 0;
1667
+ const cy = seg.y_poly[2] || 0;
1668
+ const dy = seg.y_poly[3] || 0;
1669
+ return {
1670
+ x0: ax,
1671
+ y0: ay,
1672
+ cp1x: ax + bx / 3,
1673
+ cp1y: ay + by / 3,
1674
+ cp2x: ax + 2 * bx / 3 + cx / 3,
1675
+ cp2y: ay + 2 * by / 3 + cy / 3,
1676
+ x3: ax + bx + cx + dx,
1677
+ y3: ay + by + cy + dy
1678
+ };
1679
+ }
1680
+ function rgbToCss(rgb) {
1681
+ return `rgb(${Math.round(rgb[0] * 255)}, ${Math.round(rgb[1] * 255)}, ${Math.round(rgb[2] * 255)})`;
1682
+ }
1683
+ var HIT_SAMPLES = 16;
1684
+ function flattenBezier(b, samples) {
1685
+ const pts = new Float32Array((samples + 1) * 2);
1686
+ for (let i = 0; i <= samples; i++) {
1687
+ const t = i / samples;
1688
+ const mt = 1 - t;
1689
+ const a = mt * mt * mt;
1690
+ const c1 = 3 * mt * mt * t;
1691
+ const c2 = 3 * mt * t * t;
1692
+ const d = t * t * t;
1693
+ pts[i * 2] = a * b.x0 + c1 * b.cp1x + c2 * b.cp2x + d * b.x3;
1694
+ pts[i * 2 + 1] = a * b.y0 + c1 * b.cp1y + c2 * b.cp2y + d * b.y3;
1695
+ }
1696
+ return pts;
1697
+ }
1698
+ function distSqToSegment(px, py, x1, y1, x2, y2) {
1699
+ const dx = x2 - x1;
1700
+ const dy = y2 - y1;
1701
+ const lenSq = dx * dx + dy * dy;
1702
+ let t = lenSq > 0 ? ((px - x1) * dx + (py - y1) * dy) / lenSq : 0;
1703
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
1704
+ const cx = x1 + t * dx;
1705
+ const cy = y1 + t * dy;
1706
+ const ex = px - cx;
1707
+ const ey = py - cy;
1708
+ return ex * ex + ey * ey;
1709
+ }
1710
+ var SplineEntity = (_class5 = class extends _chunk53DAQC3Ujs.Entity {
1711
+
1712
+
1713
+
1714
+
1715
+
1716
+
1717
+
1718
+ __init57() {this.offscreen = null}
1719
+ __init58() {this.baked = false}
1720
+ /** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
1721
+ __init59() {this.polylines = null}
1722
+ /**
1723
+ * When `true`, the renderer draws a rounded-rect outline of the entity's
1724
+ * local bounds after painting the curves. Useful for drag feedback and
1725
+ * debugging hit areas. Defaults to `false`.
1726
+ */
1727
+ __init60() {this.showBounds = false}
1728
+ constructor(doc, opts = {}) {
1729
+ super();_class5.prototype.__init57.call(this);_class5.prototype.__init58.call(this);_class5.prototype.__init59.call(this);_class5.prototype.__init60.call(this);;
1730
+ this.doc = doc;
1731
+ this.lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
1732
+ this.cache = _nullishCoalesce(opts.cache, () => ( true));
1733
+ this.defaultColor = _nullishCoalesce(opts.defaultColor, () => ( "#e2e8f0"));
1734
+ this.hitMode = _nullishCoalesce(opts.hitTest, () => ( "curve"));
1735
+ this.hitTolerance = _nullishCoalesce(opts.hitTolerance, () => ( 0));
1736
+ this.bounds = this.computeBounds();
1737
+ this.width = this.bounds.width;
1738
+ this.height = this.bounds.height;
1739
+ this.interactive = true;
1740
+ }
1741
+ computeBounds() {
1742
+ if (this.doc.bounding_box) {
1743
+ const [minX2, minY2, maxX2, maxY2] = this.doc.bounding_box;
1744
+ return { x: minX2, y: minY2, width: maxX2 - minX2, height: maxY2 - minY2 };
1745
+ }
1746
+ let minX = Infinity;
1747
+ let minY = Infinity;
1748
+ let maxX = -Infinity;
1749
+ let maxY = -Infinity;
1750
+ if (this.doc.equations) {
1751
+ for (const eq of this.doc.equations) {
1752
+ for (const seg of eq.data) {
1753
+ const b = polySegmentToBezier(seg);
1754
+ for (const [x, y] of [
1755
+ [b.x0, b.y0],
1756
+ [b.cp1x, b.cp1y],
1757
+ [b.cp2x, b.cp2y],
1758
+ [b.x3, b.y3]
1759
+ ]) {
1760
+ if (x < minX) minX = x;
1761
+ if (x > maxX) maxX = x;
1762
+ if (y < minY) minY = y;
1763
+ if (y > maxY) maxY = y;
1764
+ }
1765
+ }
1766
+ }
1767
+ }
1768
+ if (this.doc.paths) {
1769
+ for (const path of this.doc.paths) {
1770
+ for (const pt of path.data) {
1771
+ if (pt.x < minX) minX = pt.x;
1772
+ if (pt.x > maxX) maxX = pt.x;
1773
+ if (pt.y < minY) minY = pt.y;
1774
+ if (pt.y > maxY) maxY = pt.y;
1775
+ }
1776
+ }
1777
+ }
1778
+ if (minX === Infinity) return { x: 0, y: 0, width: 0, height: 0 };
1779
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1780
+ }
1781
+ /** @inheritdoc */
1782
+ getBounds() {
1783
+ return this.bounds;
1784
+ }
1785
+ /**
1786
+ * AABB hit-test against the document bounds in world space.
1787
+ *
1788
+ * Curve-accurate hit-testing can be layered on later via {@link hitTestCurve};
1789
+ * this method already calls it as a refinement when it is overridden.
1790
+ */
1791
+ isPointInside(globalX, globalY) {
1792
+ const pos = this.getGlobalPosition();
1793
+ const scale = this.getWorldScale();
1794
+ const lx = (globalX - pos.x) / (scale.x || 1);
1795
+ const ly = (globalY - pos.y) / (scale.y || 1);
1796
+ const inAabb = lx >= this.bounds.x && lx <= this.bounds.x + this.bounds.width && ly >= this.bounds.y && ly <= this.bounds.y + this.bounds.height;
1797
+ if (!inAabb) return false;
1798
+ const refined = this.hitTestCurve(lx, ly);
1799
+ return refined === null ? true : refined;
1800
+ }
1801
+ /**
1802
+ * Curve-accurate refinement of {@link isPointInside}: hit only when the local
1803
+ * point lies within `lineWidth/2 + hitTolerance` of an actual curve.
1804
+ *
1805
+ * Returns `null` in `hitTest: 'aabb'` mode (keep the bounding-box result).
1806
+ * Curves are flattened to polylines once and cached. Override for custom logic.
1807
+ *
1808
+ * @param localX - X in the entity's local space.
1809
+ * @param localY - Y in the entity's local space.
1810
+ * @returns `true`/`false`, or `null` to keep the AABB result.
1811
+ */
1812
+ hitTestCurve(localX, localY) {
1813
+ if (this.hitMode === "aabb") return null;
1814
+ const tol = this.lineWidth / 2 + this.hitTolerance;
1815
+ const tol2 = tol * tol;
1816
+ const polylines = this.getPolylines();
1817
+ for (const pts of polylines) {
1818
+ for (let i = 0; i + 3 < pts.length; i += 2) {
1819
+ if (distSqToSegment(localX, localY, pts[i], pts[i + 1], pts[i + 2], pts[i + 3]) <= tol2) {
1820
+ return true;
1821
+ }
1822
+ }
1823
+ }
1824
+ return false;
1825
+ }
1826
+ /** Flatten every Bézier segment into a sampled polyline once, then cache. */
1827
+ getPolylines() {
1828
+ if (this.polylines) return this.polylines;
1829
+ const out = [];
1830
+ if (this.doc.equations) {
1831
+ for (const eq of this.doc.equations) {
1832
+ for (const seg of eq.data) {
1833
+ out.push(flattenBezier(polySegmentToBezier(seg), HIT_SAMPLES));
1834
+ }
1835
+ }
1836
+ }
1837
+ if (this.doc.paths) {
1838
+ for (const path of this.doc.paths) {
1839
+ const pts = new Float32Array(path.data.length * 2);
1840
+ for (let i = 0; i < path.data.length; i++) {
1841
+ pts[i * 2] = path.data[i].x;
1842
+ pts[i * 2 + 1] = path.data[i].y;
1843
+ }
1844
+ out.push(pts);
1845
+ }
1846
+ }
1847
+ this.polylines = out;
1848
+ return out;
1849
+ }
1850
+ resolveColor(color, r) {
1851
+ if (color === null) return this.defaultColor;
1852
+ if (Array.isArray(color)) return rgbToCss(color);
1853
+ const w = this.bounds.width || 1;
1854
+ const h = this.bounds.height || 1;
1855
+ return r.createLinearGradient(
1856
+ color.start_pos[0] * w + this.bounds.x,
1857
+ color.start_pos[1] * h + this.bounds.y,
1858
+ color.end_pos[0] * w + this.bounds.x,
1859
+ color.end_pos[1] * h + this.bounds.y,
1860
+ color.stops.map(([stop, rgb]) => ({
1861
+ stop: Math.max(0, Math.min(1, stop)),
1862
+ color: rgbToCss(rgb)
1863
+ }))
1864
+ );
1865
+ }
1866
+ strokeEquations(r) {
1867
+ if (this.doc.equations) {
1868
+ for (const eq of this.doc.equations) {
1869
+ const stroke = this.resolveColor(eq.color_rgb, r);
1870
+ r.beginPath();
1871
+ for (const seg of eq.data) {
1872
+ const b = polySegmentToBezier(seg);
1873
+ r.moveTo(b.x0, b.y0);
1874
+ r.bezierCurveTo(b.cp1x, b.cp1y, b.cp2x, b.cp2y, b.x3, b.y3);
1875
+ }
1876
+ r.stroke(stroke, this.lineWidth);
1877
+ }
1878
+ }
1879
+ if (this.doc.paths) {
1880
+ for (const path of this.doc.paths) {
1881
+ const stroke = this.resolveColor(path.color_rgb, r);
1882
+ r.beginPath();
1883
+ if (path.data.length > 0) {
1884
+ r.moveTo(path.data[0].x, path.data[0].y);
1885
+ for (let i = 1; i < path.data.length; i++) {
1886
+ r.lineTo(path.data[i].x, path.data[i].y);
1887
+ }
1888
+ }
1889
+ r.stroke(stroke, this.lineWidth);
1890
+ }
1891
+ }
1892
+ }
1893
+ /** Bake all equations into an OffscreenCanvas once (when available). */
1894
+ bake() {
1895
+ this.baked = true;
1896
+ const pad = this.lineWidth + 2;
1897
+ const w = Math.max(1, Math.ceil(this.bounds.width) + pad * 2);
1898
+ const h = Math.max(1, Math.ceil(this.bounds.height) + pad * 2);
1899
+ let canvas;
1900
+ if (typeof OffscreenCanvas !== "undefined") {
1901
+ canvas = new OffscreenCanvas(w, h);
1902
+ } else if (typeof document !== "undefined") {
1903
+ canvas = document.createElement("canvas");
1904
+ canvas.width = w;
1905
+ canvas.height = h;
1906
+ } else {
1907
+ return;
1908
+ }
1909
+ const ctx = canvas.getContext("2d");
1910
+ if (!ctx) return;
1911
+ ctx.translate(pad - this.bounds.x, pad - this.bounds.y);
1912
+ ctx.lineWidth = this.lineWidth;
1913
+ ctx.lineCap = "round";
1914
+ ctx.lineJoin = "round";
1915
+ if (this.doc.equations) {
1916
+ for (const eq of this.doc.equations) {
1917
+ ctx.strokeStyle = eq.color_rgb === null ? this.defaultColor : Array.isArray(eq.color_rgb) ? rgbToCss(eq.color_rgb) : this.defaultColor;
1918
+ ctx.beginPath();
1919
+ for (const seg of eq.data) {
1920
+ const b = polySegmentToBezier(seg);
1921
+ ctx.moveTo(b.x0, b.y0);
1922
+ ctx.bezierCurveTo(b.cp1x, b.cp1y, b.cp2x, b.cp2y, b.x3, b.y3);
1923
+ }
1924
+ ctx.stroke();
1925
+ }
1926
+ }
1927
+ if (this.doc.paths) {
1928
+ for (const path of this.doc.paths) {
1929
+ ctx.strokeStyle = path.color_rgb === null ? this.defaultColor : Array.isArray(path.color_rgb) ? rgbToCss(path.color_rgb) : this.defaultColor;
1930
+ ctx.beginPath();
1931
+ if (path.data.length > 0) {
1932
+ ctx.moveTo(path.data[0].x, path.data[0].y);
1933
+ for (let i = 1; i < path.data.length; i++) {
1934
+ ctx.lineTo(path.data[i].x, path.data[i].y);
1935
+ }
1936
+ }
1937
+ ctx.stroke();
1938
+ }
1939
+ }
1940
+ this.offscreen = canvas;
1941
+ }
1942
+ render(r) {
1943
+ let rendered = false;
1944
+ if (this.cache) {
1945
+ if (!this.baked) this.bake();
1946
+ if (this.offscreen) {
1947
+ const pad = this.lineWidth + 2;
1948
+ r.drawImage(
1949
+ this.offscreen,
1950
+ this.bounds.x - pad,
1951
+ this.bounds.y - pad,
1952
+ this.offscreen.width,
1953
+ this.offscreen.height
1954
+ );
1955
+ rendered = true;
1956
+ }
1957
+ }
1958
+ if (!rendered) {
1959
+ this.strokeEquations(r);
1960
+ }
1961
+ if (this.showBounds) {
1962
+ r.beginPath();
1963
+ r.roundRect(this.bounds.x, this.bounds.y, this.bounds.width, this.bounds.height, 4);
1964
+ r.stroke("rgba(0, 150, 255, 0.8)", 2);
1965
+ }
1966
+ }
1967
+ }, _class5);
1968
+ async function loadSpline(url) {
1969
+ const res = await fetch(url);
1970
+ return await res.json();
1971
+ }
1972
+
1973
+ // src/math/SpatialHashGrid.ts
1974
+ var SpatialHashGrid = (_class6 = class {
1975
+
1976
+ __init61() {this.grid = /* @__PURE__ */ new Map()}
1977
+ __init62() {this.entityCells = /* @__PURE__ */ new Map()}
1978
+ constructor(cellSize = 64) {;_class6.prototype.__init61.call(this);_class6.prototype.__init62.call(this);
1979
+ this.cellSize = cellSize;
1980
+ }
1981
+ hash(cx, cy) {
1982
+ const x = cx < 0 ? -2 * cx - 1 : 2 * cx;
1983
+ const y = cy < 0 ? -2 * cy - 1 : 2 * cy;
1984
+ return (x + y) * (x + y + 1) / 2 + y;
1985
+ }
1986
+ cellsForAABB(x, y, w, h) {
1987
+ const minCx = Math.floor(x / this.cellSize);
1988
+ const minCy = Math.floor(y / this.cellSize);
1989
+ const maxCx = Math.floor((x + w) / this.cellSize);
1990
+ const maxCy = Math.floor((y + h) / this.cellSize);
1991
+ const keys = [];
1992
+ for (let cx = minCx; cx <= maxCx; cx++) {
1993
+ for (let cy = minCy; cy <= maxCy; cy++) {
1994
+ keys.push(this.hash(cx, cy));
1995
+ }
1996
+ }
1997
+ return keys;
1998
+ }
1999
+ /**
2000
+ * Insert or update an entity's axis-aligned bounding box in the grid.
2001
+ *
2002
+ * If the entity is already registered its old cell memberships are removed
2003
+ * before the new ones are computed, so this method is safe to call every
2004
+ * frame.
2005
+ *
2006
+ * @param id - Unique string identifier for the entity.
2007
+ * @param x - Left edge of the AABB in world space.
2008
+ * @param y - Top edge of the AABB in world space.
2009
+ * @param w - Width of the AABB.
2010
+ * @param h - Height of the AABB.
2011
+ */
2012
+ insert(id, x, y, w, h) {
2013
+ this.remove(id);
2014
+ const keys = this.cellsForAABB(x, y, w, h);
2015
+ this.entityCells.set(id, keys);
2016
+ for (const key of keys) {
2017
+ if (!this.grid.has(key)) this.grid.set(key, /* @__PURE__ */ new Set());
2018
+ this.grid.get(key).add(id);
2019
+ }
2020
+ }
2021
+ /**
2022
+ * Remove an entity from all grid cells it currently occupies.
2023
+ *
2024
+ * Silently does nothing if the entity is not registered.
2025
+ *
2026
+ * @param id - Unique string identifier of the entity to remove.
2027
+ */
2028
+ remove(id) {
2029
+ const keys = this.entityCells.get(id);
2030
+ if (!keys) return;
2031
+ for (const key of keys) {
2032
+ _optionalChain([this, 'access', _46 => _46.grid, 'access', _47 => _47.get, 'call', _48 => _48(key), 'optionalAccess', _49 => _49.delete, 'call', _50 => _50(id)]);
2033
+ }
2034
+ this.entityCells.delete(id);
2035
+ }
2036
+ /**
2037
+ * Return all entity IDs whose grid cells overlap the given AABB.
2038
+ *
2039
+ * Time complexity: O(k) where k is the number of cells the query AABB spans
2040
+ * plus the number of results — O(1) average for small, similarly-sized entities.
2041
+ *
2042
+ * @param x - Left edge of the query AABB.
2043
+ * @param y - Top edge of the query AABB.
2044
+ * @param w - Width of the query AABB.
2045
+ * @param h - Height of the query AABB.
2046
+ * @returns A `Set` of entity ID strings whose cells intersect the query region.
2047
+ */
2048
+ query(x, y, w, h) {
2049
+ const result = /* @__PURE__ */ new Set();
2050
+ for (const key of this.cellsForAABB(x, y, w, h)) {
2051
+ const cell = this.grid.get(key);
2052
+ if (cell) for (const id of cell) result.add(id);
2053
+ }
2054
+ return result;
2055
+ }
2056
+ /**
2057
+ * Clear all cells and entity registrations, resetting the grid to an empty state.
2058
+ *
2059
+ * Call once per frame before re-inserting all dynamic entities.
2060
+ */
2061
+ clear() {
2062
+ this.grid.clear();
2063
+ this.entityCells.clear();
2064
+ }
2065
+ }, _class6);
2066
+
2067
+ // src/math/SpringPhysics.ts
2068
+ var SpringPhysics = (_class7 = class {
2069
+
2070
+
2071
+ __init63() {this.velocity = 0}
2072
+ __init64() {this.stiffness = 180}
2073
+ __init65() {this.damping = 12}
2074
+ __init66() {this.mass = 1}
2075
+ __init67() {this.valEpsilon = 5e-3}
2076
+ __init68() {this.velEpsilon = 5e-3}
2077
+ constructor(initial) {;_class7.prototype.__init63.call(this);_class7.prototype.__init64.call(this);_class7.prototype.__init65.call(this);_class7.prototype.__init66.call(this);_class7.prototype.__init67.call(this);_class7.prototype.__init68.call(this);
2078
+ this.value = initial;
2079
+ this.target = initial;
2080
+ }
2081
+ update(dt) {
2082
+ if (this.isAtRest()) {
2083
+ this.value = this.target;
2084
+ this.velocity = 0;
2085
+ return;
2086
+ }
2087
+ const forceSpring = -this.stiffness * (this.value - this.target);
2088
+ const forceDamping = -this.damping * this.velocity;
2089
+ const acceleration = (forceSpring + forceDamping) / this.mass;
2090
+ this.velocity += acceleration * dt;
2091
+ this.value += this.velocity * dt;
2092
+ }
2093
+ isAtRest() {
2094
+ return Math.abs(this.value - this.target) < this.valEpsilon && Math.abs(this.velocity) < this.velEpsilon;
2095
+ }
2096
+ }, _class7);
2097
+
2098
+ // src/tree/DOMPortalEntity.ts
2099
+ var DOMPortalEntity = (_class8 = class extends _chunk53DAQC3Ujs.Entity {
2100
+
2101
+ __init69() {this.isDOMPortal = true}
2102
+ __init70() {this.domListeners = []}
2103
+ __init71() {this.resizeObserver = null}
2104
+ __init72() {this.cachedWidth = 100}
2105
+ __init73() {this.cachedHeight = 100}
2106
+ __init74() {this.lastWidth = ""}
2107
+ __init75() {this.lastHeight = ""}
2108
+ __init76() {this.lastTransform = ""}
2109
+ __init77() {this.lastZIndex = ""}
2110
+ constructor(domElement, width, height, id) {
2111
+ super(id);_class8.prototype.__init69.call(this);_class8.prototype.__init70.call(this);_class8.prototype.__init71.call(this);_class8.prototype.__init72.call(this);_class8.prototype.__init73.call(this);_class8.prototype.__init74.call(this);_class8.prototype.__init75.call(this);_class8.prototype.__init76.call(this);_class8.prototype.__init77.call(this);;
2112
+ this.domElement = domElement;
2113
+ this.width = _nullishCoalesce(width, () => ( 0));
2114
+ this.height = _nullishCoalesce(height, () => ( 0));
2115
+ if (typeof window !== "undefined") {
2116
+ this.domElement.style.position = "absolute";
2117
+ this.domElement.style.transformOrigin = "0 0";
2118
+ this.domElement.style.pointerEvents = "auto";
2119
+ this.cachedWidth = parseFloat(domElement.style.width) || domElement.offsetWidth || 100;
2120
+ this.cachedHeight = parseFloat(domElement.style.height) || domElement.offsetHeight || 100;
2121
+ if (typeof ResizeObserver !== "undefined") {
2122
+ this.resizeObserver = new ResizeObserver((entries) => {
2123
+ for (const entry of entries) {
2124
+ this.cachedWidth = entry.contentRect.width || entry.target.offsetWidth;
2125
+ this.cachedHeight = entry.contentRect.height || entry.target.offsetHeight;
2126
+ }
2127
+ });
2128
+ this.resizeObserver.observe(this.domElement);
2129
+ }
2130
+ const events = ["click", "pointerdown", "pointerup", "pointermove", "wheel"];
2131
+ for (const type of events) {
2132
+ const handler = (e) => {
2133
+ this.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)(type, this, e));
2134
+ };
2135
+ this.domElement.addEventListener(type, handler);
2136
+ this.domListeners.push({ type, handler, capture: false });
2137
+ }
2138
+ const hoverEvents = [
2139
+ { native: "mouseenter", vecto: "hover" },
2140
+ { native: "mouseleave", vecto: "pointerleave" }
2141
+ ];
2142
+ for (const { native, vecto } of hoverEvents) {
2143
+ const handler = (e) => {
2144
+ this.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)(vecto, this, e, false));
2145
+ };
2146
+ this.domElement.addEventListener(native, handler);
2147
+ this.domListeners.push({ type: native, handler, capture: false });
2148
+ }
2149
+ const focusEvents = ["focus", "blur"];
2150
+ for (const type of focusEvents) {
2151
+ const handler = (e) => {
2152
+ this.dispatchEvent(new (0, _chunk53DAQC3Ujs.VectoJSEvent)(type, this, e, true));
2153
+ };
2154
+ this.domElement.addEventListener(type, handler, true);
2155
+ this.domListeners.push({ type, handler, capture: true });
2156
+ }
2157
+ }
2158
+ }
2159
+ isPointInside(globalX, globalY) {
2160
+ const pos = this.getGlobalPosition();
2161
+ const scale = this.getWorldScale();
2162
+ const rot = this.getWorldRotation();
2163
+ const dx = globalX - pos.x;
2164
+ const dy = globalY - pos.y;
2165
+ const cos = Math.cos(-rot);
2166
+ const sin = Math.sin(-rot);
2167
+ const lx = (dx * cos - dy * sin) / scale.x;
2168
+ const ly = (dx * sin + dy * cos) / scale.y;
2169
+ const w = this.width > 0 ? this.width : this.cachedWidth;
2170
+ const h = this.height > 0 ? this.height : this.cachedHeight;
2171
+ return lx >= 0 && lx <= w && ly >= 0 && ly <= h;
2172
+ }
2173
+ add(child) {
2174
+ console.warn(`DOMPortalEntity (${this.id}) is a leaf node. Child entities are not supported.`);
2175
+ return super.add(child);
2176
+ }
2177
+ render() {
2178
+ }
2179
+ destroy() {
2180
+ if (typeof window !== "undefined") {
2181
+ if (this.resizeObserver) {
2182
+ this.resizeObserver.disconnect();
2183
+ this.resizeObserver = null;
2184
+ }
2185
+ if (this.domElement) {
2186
+ for (const { type, handler, capture } of this.domListeners) {
2187
+ this.domElement.removeEventListener(type, handler, capture);
2188
+ }
2189
+ this.domListeners = [];
2190
+ this.domElement.remove();
2191
+ }
2192
+ }
2193
+ super.destroy();
2194
+ }
2195
+ }, _class8);
2196
+
2197
+ // src/index.ts
2198
+ Scene.registerWebGLPointRendererCreator(_chunkLIX7DJTIjs.createWebGLPointRenderer);
2199
+ Scene.registerWebGPUParticleSystemManager(_chunkLIX7DJTIjs.WebGPUParticleSystemManager);
2200
+
2201
+
2202
+
2203
+
2204
+
2205
+
2206
+
2207
+
2208
+
2209
+
2210
+
2211
+
2212
+
2213
+
2214
+
2215
+
2216
+
2217
+
2218
+
2219
+
2220
+
2221
+
2222
+
2223
+
2224
+
2225
+
2226
+
2227
+
2228
+
2229
+
2230
+
2231
+
2232
+
2233
+
2234
+
2235
+
2236
+
2237
+
2238
+ exports.ArabicShaper = _chunkRW6NC4RBjs.ArabicShaper; exports.BidiResolver = _chunkRW6NC4RBjs.BidiResolver; exports.CanvasRenderer = _chunkLIX7DJTIjs.CanvasRenderer; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DOMPortalEntity = DOMPortalEntity; exports.Entity = _chunk53DAQC3Ujs.Entity; exports.GridTextEntity = GridTextEntity; exports.LayoutEngine = _chunk72WVPMSJjs.LayoutEngine; exports.LayoutResultBuffer = _chunk72WVPMSJjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunkRW6NC4RBjs.LayoutWorkerManager; exports.MSDFFont = _chunk53DAQC3Ujs.MSDFFont; exports.MSDFTextEntity = _chunk53DAQC3Ujs.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.SVGEntity = _chunk53DAQC3Ujs.SVGEntity; exports.SVGRenderer = _chunkLIX7DJTIjs.SVGRenderer; exports.Scene = Scene; exports.SpatialHashGrid = SpatialHashGrid; exports.SplineEntity = SplineEntity; exports.SpringPhysics = SpringPhysics; exports.TextEntity = TextEntity; exports.VectoJSEvent = _chunk53DAQC3Ujs.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkLIX7DJTIjs.WebGPUParticleSystemManager; exports.computeLineSegments = _chunk72WVPMSJjs.computeLineSegments; exports.createCanvasMeasurer = _chunk72WVPMSJjs.createCanvasMeasurer; exports.createWebGLPointRenderer = _chunkLIX7DJTIjs.createWebGLPointRenderer; exports.loadSpline = loadSpline; exports.parseColorToRGBA = _chunkLIX7DJTIjs.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier;