@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.
@@ -0,0 +1,907 @@
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;
2
+
3
+ var _chunkRW6NC4RBjs = require('./chunk-RW6NC4RB.js');
4
+
5
+ // src/tree/Entity.ts
6
+ var VectoJSEvent = (_class = class {
7
+ /** The event name. */
8
+
9
+ /** The entity the event originated on. */
10
+
11
+ /** The entity whose listeners are currently running (updated per node). */
12
+
13
+ /** The wrapped browser event, if any. */
14
+
15
+ /** Whether the event bubbles past its target (capture always runs). */
16
+
17
+ __init() {this.stopped = false}
18
+ __init2() {this.stoppedImmediate = false}
19
+ constructor(type, target, nativeEvent, bubbles = true) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);
20
+ this.type = type;
21
+ this.target = target;
22
+ this.currentTarget = target;
23
+ this.nativeEvent = nativeEvent;
24
+ this.bubbles = bubbles;
25
+ }
26
+ /** Stop the event from reaching the next node in the propagation path. */
27
+ stopPropagation() {
28
+ this.stopped = true;
29
+ }
30
+ /** Stop propagation AND skip any remaining listeners on the current node. */
31
+ stopImmediatePropagation() {
32
+ this.stopped = true;
33
+ this.stoppedImmediate = true;
34
+ }
35
+ /** Forward to the native event's `preventDefault` (e.g. stop page scroll). */
36
+ preventDefault() {
37
+ _optionalChain([this, 'access', _ => _.nativeEvent, 'optionalAccess', _2 => _2.preventDefault, 'optionalCall', _3 => _3()]);
38
+ }
39
+ /** Whether {@link stopPropagation} has been called. */
40
+ get propagationStopped() {
41
+ return this.stopped;
42
+ }
43
+ /** Whether {@link stopImmediatePropagation} has been called. */
44
+ get immediatePropagationStopped() {
45
+ return this.stoppedImmediate;
46
+ }
47
+ /** Whether the native event's default action was prevented. */
48
+ get defaultPrevented() {
49
+ return !!_optionalChain([this, 'access', _4 => _4.nativeEvent, 'optionalAccess', _5 => _5.defaultPrevented]);
50
+ }
51
+ /** Native horizontal wheel delta, if this wraps a `WheelEvent`. */
52
+ get deltaX() {
53
+ return _optionalChain([this, 'access', _6 => _6.nativeEvent, 'optionalAccess', _7 => _7.deltaX]);
54
+ }
55
+ /** Native vertical wheel delta, if this wraps a `WheelEvent`. */
56
+ get deltaY() {
57
+ return _optionalChain([this, 'access', _8 => _8.nativeEvent, 'optionalAccess', _9 => _9.deltaY]);
58
+ }
59
+ /** Native pointer X, if this wraps a pointer/mouse event. */
60
+ get clientX() {
61
+ return _optionalChain([this, 'access', _10 => _10.nativeEvent, 'optionalAccess', _11 => _11.clientX]);
62
+ }
63
+ /** Native pointer Y, if this wraps a pointer/mouse event. */
64
+ get clientY() {
65
+ return _optionalChain([this, 'access', _12 => _12.nativeEvent, 'optionalAccess', _13 => _13.clientY]);
66
+ }
67
+ /** Native key, if this wraps a keyboard event. */
68
+ get key() {
69
+ return _optionalChain([this, 'access', _14 => _14.nativeEvent, 'optionalAccess', _15 => _15.key]);
70
+ }
71
+ }, _class);
72
+ var Entity = (_class2 = class {
73
+
74
+ __init3() {this.children = []}
75
+ __init4() {this.parent = null}
76
+ /**
77
+ * Walk up the parent chain to find the scene this entity is currently attached to.
78
+ */
79
+ get scene() {
80
+ if (this._scene) return this._scene;
81
+ return this.parent ? this.parent.scene : null;
82
+ }
83
+ __init5() {this.x = 0}
84
+ __init6() {this.y = 0}
85
+ __init7() {this.scaleX = 1}
86
+ __init8() {this.scaleY = 1}
87
+ __init9() {this.rotation = 0}
88
+ __init10() {this.opacity = 1}
89
+ __init11() {this.isDOMPortal = false}
90
+ __init12() {this._interactive = false}
91
+ get interactive() {
92
+ return this._interactive;
93
+ }
94
+ set interactive(val) {
95
+ if (this._interactive !== val) {
96
+ this._interactive = val;
97
+ const s = this.scene;
98
+ if (s) {
99
+ s.a11yNeedsReorder = true;
100
+ s.markDirty();
101
+ }
102
+ }
103
+ }
104
+ __init13() {this.width = 0}
105
+ __init14() {this.height = 0}
106
+ __init15() {this.a11yOffsetX = 0}
107
+ __init16() {this.a11yOffsetY = 0}
108
+ /**
109
+ * Opt in to a viewport-filling accessibility/automation shadow node even when
110
+ * this entity has no intrinsic box (`width`/`height` of `0`). Use for
111
+ * full-screen, boundless interaction surfaces (e.g. an infinite-canvas graph)
112
+ * that need global pointer events. The node is mounted behind all other shadow
113
+ * nodes, so on-top components stay clickable.
114
+ */
115
+ __init17() {this.a11yFullViewport = false}
116
+ /**
117
+ * Clip this node's children to its local box (`[0,0]–[width,height]`) while
118
+ * rendering. Combined with translating a content child, this is how
119
+ * scroll/overflow containers (e.g. `ScrollView`) keep their content inside a
120
+ * fixed viewport. Off by default (children render unclipped). Canvas2D only.
121
+ */
122
+ __init18() {this.clipChildren = false}
123
+ __init19() {this.listeners = /* @__PURE__ */ new Map()}
124
+ /** Capture-phase listeners (fired root→target before bubble). */
125
+ __init20() {this.captureListeners = /* @__PURE__ */ new Map()}
126
+ __init21() {this.animations = []}
127
+ constructor(id) {;_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);_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);
128
+ this.id = id || `entity_${Math.random().toString(36).substring(2, 9)}`;
129
+ }
130
+ /**
131
+ * Append a child entity to this node's children array.
132
+ *
133
+ * @param child - The entity to add as a child.
134
+ * @returns `this` for method chaining.
135
+ */
136
+ add(child) {
137
+ child.parent = this;
138
+ this.children.push(child);
139
+ const s = this.scene;
140
+ if (s) {
141
+ s.a11yNeedsReorder = true;
142
+ s.markDirty();
143
+ }
144
+ return this;
145
+ }
146
+ /**
147
+ * Remove a child entity from this node.
148
+ *
149
+ * @param child - The entity to remove.
150
+ * @returns `this` for method chaining.
151
+ */
152
+ remove(child) {
153
+ const index = this.children.indexOf(child);
154
+ if (index !== -1) {
155
+ this.children.splice(index, 1);
156
+ child.parent = null;
157
+ const s = this.scene;
158
+ if (s) {
159
+ s.a11yNeedsReorder = true;
160
+ s.markDirty();
161
+ }
162
+ }
163
+ return this;
164
+ }
165
+ /**
166
+ * Set the local position of this entity.
167
+ *
168
+ * @param x - Horizontal position in local space.
169
+ * @param y - Vertical position in local space.
170
+ * @returns `this` for method chaining.
171
+ * @example entity.setPosition(100, 200);
172
+ */
173
+ setPosition(x, y) {
174
+ this.x = x;
175
+ this.y = y;
176
+ return this;
177
+ }
178
+ /**
179
+ * Queue a tween animation toward the specified target property values.
180
+ *
181
+ * Multiple calls chain animations sequentially. Only numeric properties
182
+ * are interpolated; non-numeric values are ignored.
183
+ *
184
+ * @param targetProps - Partial set of numeric properties to tween to.
185
+ * @param durationMs - Duration of the tween in milliseconds.
186
+ * @returns `this` for method chaining.
187
+ * @example entity.animate({ x: 400, opacity: 0 }, 500);
188
+ */
189
+ animate(targetProps, durationMs) {
190
+ this.animations.push({
191
+ target: targetProps,
192
+ duration: durationMs,
193
+ startTime: -1,
194
+ startProps: {}
195
+ });
196
+ return this;
197
+ }
198
+ /**
199
+ * Advance the entity's internal state for one frame.
200
+ *
201
+ * Called automatically by the {@link Scene} render loop — override in
202
+ * subclasses to implement custom per-frame logic.
203
+ *
204
+ * @param dt - Elapsed time since the last frame in milliseconds.
205
+ * @param time - Absolute timestamp from `performance.now()`.
206
+ */
207
+ update(_dt, time) {
208
+ if (this.animations.length > 0) {
209
+ const anim = this.animations[0];
210
+ if (anim.startTime === -1) {
211
+ anim.startTime = time;
212
+ for (const key in anim.target) {
213
+ anim.startProps[key] = this[key];
214
+ }
215
+ }
216
+ const progress = Math.min((time - anim.startTime) / anim.duration, 1);
217
+ for (const key in anim.target) {
218
+ const start = anim.startProps[key];
219
+ const end = anim.target[key];
220
+ if (typeof start === "number" && typeof end === "number") {
221
+ const easeOut = progress * (2 - progress);
222
+ this[key] = start + (end - start) * easeOut;
223
+ }
224
+ }
225
+ if (progress >= 1) {
226
+ this.animations.shift();
227
+ }
228
+ }
229
+ }
230
+ /**
231
+ * Register a listener for a {@link VectoEvent}.
232
+ *
233
+ * Listeners run in the bubble phase by default; pass `{ capture: true }` for the
234
+ * capture phase (root→target). Bubble listeners also fire for the legacy
235
+ * {@link emit} (direct, self-only) path.
236
+ *
237
+ * @param event - The event name to listen for.
238
+ * @param callback - Handler invoked when the event fires.
239
+ * @param options - `{ capture }` to register for the capture phase.
240
+ * @returns `this` for method chaining.
241
+ * @example entity.on('click', (e) => console.log('clicked', e));
242
+ */
243
+ on(event, callback, options) {
244
+ const map = _optionalChain([options, 'optionalAccess', _16 => _16.capture]) ? this.captureListeners : this.listeners;
245
+ if (!map.has(event)) {
246
+ map.set(event, []);
247
+ }
248
+ map.get(event).push(callback);
249
+ return this;
250
+ }
251
+ /**
252
+ * Remove a previously registered event listener.
253
+ *
254
+ * @param event - The event name to stop listening to.
255
+ * @param callback - The exact handler reference passed to {@link on}.
256
+ * @param options - Must match the phase the listener was registered with.
257
+ * @returns `this` for method chaining.
258
+ */
259
+ off(event, callback, options) {
260
+ const handlers = (_optionalChain([options, 'optionalAccess', _17 => _17.capture]) ? this.captureListeners : this.listeners).get(event);
261
+ if (handlers) {
262
+ const idx = handlers.indexOf(callback);
263
+ if (idx !== -1) handlers.splice(idx, 1);
264
+ }
265
+ return this;
266
+ }
267
+ /**
268
+ * Tear down this entity: clear all animations, event listeners, and detach
269
+ * from parent. Call before discarding an entity to prevent memory leaks.
270
+ */
271
+ destroy() {
272
+ this.animations = [];
273
+ this.listeners.clear();
274
+ this.captureListeners.clear();
275
+ if (this.parent) {
276
+ this.parent.remove(this);
277
+ }
278
+ }
279
+ /**
280
+ * Dispatch a {@link VectoEvent} directly to this entity's bubble-phase listeners
281
+ * only — no tree propagation. Kept for component-internal/self events (e.g. a
282
+ * form control emitting its own `change`); use {@link dispatchEvent} for the
283
+ * capture/bubble path.
284
+ *
285
+ * @param event - The event name to dispatch.
286
+ * @param payload - Arbitrary data forwarded to each listener.
287
+ */
288
+ emit(event, payload) {
289
+ const handlers = this.listeners.get(event);
290
+ if (handlers) {
291
+ handlers.forEach((h) => h(payload));
292
+ }
293
+ }
294
+ /** Run one node's listeners for the event, honoring stopImmediatePropagation. */
295
+ fireListeners(node, map, event) {
296
+ const handlers = map.get(event.type);
297
+ if (!handlers) return;
298
+ event.currentTarget = node;
299
+ for (const h of handlers.slice()) {
300
+ h(event);
301
+ if (event.immediatePropagationStopped) return;
302
+ }
303
+ }
304
+ /**
305
+ * Dispatch a {@link VectoJSEvent} through the entity tree, DOM-style: a capture
306
+ * phase from the root down to `event.target`, then a bubble phase back up to the
307
+ * root. `event.stopPropagation()` halts the walk; `stopImmediatePropagation()`
308
+ * also skips the remaining listeners on the current node. A non-bubbling event
309
+ * only fires its target in the bubble phase (capture still runs).
310
+ *
311
+ * @param event - The event to propagate (its `target` defines the path).
312
+ */
313
+ dispatchEvent(event) {
314
+ const path = [];
315
+ for (let n = event.target; n; n = n.parent) path.push(n);
316
+ for (let i = path.length - 1; i >= 0; i--) {
317
+ if (event.propagationStopped) return;
318
+ this.fireListeners(path[i], path[i].captureListeners, event);
319
+ }
320
+ for (let i = 0; i < path.length; i++) {
321
+ if (event.propagationStopped) return;
322
+ this.fireListeners(path[i], path[i].listeners, event);
323
+ if (!event.bubbles) return;
324
+ }
325
+ }
326
+ /**
327
+ * Compute the entity's position in world/canvas space by accumulating
328
+ * local offsets up the scene-graph hierarchy using affine transformations (scale and rotation).
329
+ *
330
+ * @returns World-space {@link Point} for this entity.
331
+ */
332
+ getGlobalPosition() {
333
+ let px = this.x;
334
+ let py = this.y;
335
+ let curr = this.parent;
336
+ while (curr && curr.id !== "root") {
337
+ const cos = Math.cos(curr.rotation);
338
+ const sin = Math.sin(curr.rotation);
339
+ const rotatedX = px * cos - py * sin;
340
+ const rotatedY = px * sin + py * cos;
341
+ px = curr.x + curr.scaleX * rotatedX;
342
+ py = curr.y + curr.scaleY * rotatedY;
343
+ curr = curr.parent;
344
+ }
345
+ return { x: px, y: py };
346
+ }
347
+ /**
348
+ * Accumulated world scale factors: this entity's own `scaleX`/`scaleY` times
349
+ * those of every ancestor (excluding the scene root). Useful for mapping a
350
+ * world-space point back into local space for hit-testing.
351
+ *
352
+ * @returns The world scale `{ x, y }`.
353
+ */
354
+ getWorldScale() {
355
+ let sx = this.scaleX;
356
+ let sy = this.scaleY;
357
+ let curr = this.parent;
358
+ while (curr && curr.id !== "root") {
359
+ sx *= curr.scaleX;
360
+ sy *= curr.scaleY;
361
+ curr = curr.parent;
362
+ }
363
+ return { x: sx, y: sy };
364
+ }
365
+ /**
366
+ * Accumulated world rotation: this entity's own `rotation` plus
367
+ * that of every ancestor (excluding the scene root).
368
+ *
369
+ * @returns The accumulated world rotation in radians.
370
+ */
371
+ getWorldRotation() {
372
+ let rot = this.rotation;
373
+ let curr = this.parent;
374
+ while (curr && curr.id !== "root") {
375
+ rot += curr.rotation;
376
+ curr = curr.parent;
377
+ }
378
+ return rot;
379
+ }
380
+ /**
381
+ * Return `true` when the given world-space point lies within this entity's
382
+ * interactive hit area.
383
+ *
384
+ * @param globalX - World-space X coordinate.
385
+ * @param globalY - World-space Y coordinate.
386
+ * @returns Whether the point is inside this entity.
387
+ */
388
+ /**
389
+ * Describe this entity's semantics for the accessibility / automation shadow
390
+ * layer. Override in components to project a real `<button>`, `<a href>`, etc.
391
+ *
392
+ * The default returns `{}`, which `Scene.syncA11y` maps to a plain `div`
393
+ * (preserving the historical behavior of interactive entities).
394
+ *
395
+ * @returns The {@link A11yAttributes} for this entity's shadow node.
396
+ */
397
+ getA11yAttributes() {
398
+ return {};
399
+ }
400
+ /**
401
+ * Local-space axis-aligned bounding box of what this entity's {@link render}
402
+ * draws, used by {@link Scene} for viewport culling.
403
+ *
404
+ * Returns `null` by default, meaning "unknown bounds" — the entity is then
405
+ * never culled (always rendered). Override to return a {@link Bounds} so the
406
+ * scene can skip rendering it when it lies outside the viewport.
407
+ *
408
+ * @returns The local bounds, or `null` to opt out of culling.
409
+ */
410
+ getBounds() {
411
+ return null;
412
+ }
413
+ /**
414
+ * Opt into the renderer's draw-call batching fast-path for point-cloud /
415
+ * particle entities that draw as a single filled circle at their local origin.
416
+ *
417
+ * When a leaf entity returns a {@link BatchCircle} and has uniform scale, the
418
+ * {@link Scene} skips its per-entity `save`/`translate`/`scale`/`rotate`/
419
+ * `restore` and {@link render}, emitting the circle through
420
+ * {@link IRenderer.fillCircle} so runs of same-color siblings coalesce into a
421
+ * single `fill()`. Returns `null` by default (normal render path). Read each
422
+ * frame, so an animated color/radius is honored.
423
+ *
424
+ * @returns The circle to batch, or `null` to use the normal {@link render} path.
425
+ */
426
+ getBatchCircle() {
427
+ return null;
428
+ }
429
+ /**
430
+ * Opt into the GPU instanced-rectangle fast-path for a leaf entity that draws
431
+ * as a single filled rectangle from its local origin. Only used when the
432
+ * {@link Scene} runs a WebGL `pointBackend`; otherwise the entity renders
433
+ * normally via {@link render}. Returns `null` by default. Read each frame.
434
+ *
435
+ * @returns The rectangle to batch, or `null` for the normal render path.
436
+ */
437
+ getBatchRect() {
438
+ return null;
439
+ }
440
+ /**
441
+ * Whether this entity still has a queued/running tween animation.
442
+ *
443
+ * Used by {@link Scene}'s `onDemand` render mode to keep redrawing while an
444
+ * animation is in flight.
445
+ *
446
+ * @returns `true` if at least one animation remains.
447
+ */
448
+ hasPendingAnimations() {
449
+ return this.animations.length > 0;
450
+ }
451
+ }, _class2);
452
+
453
+ // src/text/MSDFFont.ts
454
+ function kernKey(a, b) {
455
+ return a * 1114112 + b;
456
+ }
457
+ var MSDFFont = (_class3 = class _MSDFFont {
458
+ static __initStatic() {this.idCounter = 0}
459
+
460
+
461
+ __init22() {this.byCode = /* @__PURE__ */ new Map()}
462
+ __init23() {this.kern = /* @__PURE__ */ new Map()}
463
+ constructor(data) {;_class3.prototype.__init22.call(this);_class3.prototype.__init23.call(this);
464
+ this.id = `font-${_MSDFFont.idCounter++}`;
465
+ this.data = data;
466
+ for (const g of data.glyphs) this.byCode.set(g.unicode, g);
467
+ for (const k of _nullishCoalesce(data.kerning, () => ( []))) this.kern.set(kernKey(k.unicode1, k.unicode2), k.advance);
468
+ }
469
+ /** Parse the `msdf-atlas-gen` JSON (string or already-parsed object). */
470
+ static parse(json) {
471
+ return new _MSDFFont(typeof json === "string" ? JSON.parse(json) : json);
472
+ }
473
+ /** Get a glyph's definition by its unicode value in O(1) time. */
474
+ getGlyph(unicode) {
475
+ return this.byCode.get(unicode);
476
+ }
477
+ /** Distance field range in atlas pixels (for the shader's `u_distanceRange`). */
478
+ get distanceRange() {
479
+ return this.data.atlas.distanceRange;
480
+ }
481
+ get atlasWidth() {
482
+ return this.data.atlas.width;
483
+ }
484
+ get atlasHeight() {
485
+ return this.data.atlas.height;
486
+ }
487
+ /**
488
+ * Lay `text` out at `fontSizePx`. Returns positioned quads (skipping glyphs the
489
+ * font doesn't contain), the widest line's advance, and the total block height.
490
+ * Honors `\n`, kerning pairs, and `letterSpacing`.
491
+ */
492
+ layout(text, fontSizePx, opts = {}) {
493
+ const { x = 0, y = 0, letterSpacing = 0 } = opts;
494
+ const { width: aw, height: ah, yOrigin } = this.data.atlas;
495
+ const { lineHeight, ascender } = this.data.metrics;
496
+ const glyphs = [];
497
+ let penX = x;
498
+ let line = 0;
499
+ let maxAdvance = 0;
500
+ let prevCode = -1;
501
+ const chars = Array.from(text);
502
+ for (const char of chars) {
503
+ if (char === "\n") {
504
+ maxAdvance = Math.max(maxAdvance, penX - x);
505
+ penX = x;
506
+ line++;
507
+ prevCode = -1;
508
+ continue;
509
+ }
510
+ const code = char.codePointAt(0);
511
+ const def = this.byCode.get(code);
512
+ if (!def) {
513
+ prevCode = -1;
514
+ continue;
515
+ }
516
+ if (prevCode >= 0) {
517
+ const k = this.kern.get(kernKey(prevCode, code));
518
+ if (k) penX += k * fontSizePx;
519
+ }
520
+ const baseline = y + (ascender + line * lineHeight) * fontSizePx;
521
+ const pb = def.planeBounds;
522
+ const ab = def.atlasBounds;
523
+ if (pb && ab) {
524
+ const v0 = yOrigin === "bottom" ? 1 - ab.top / ah : ab.top / ah;
525
+ const v1 = yOrigin === "bottom" ? 1 - ab.bottom / ah : ab.bottom / ah;
526
+ glyphs.push({
527
+ char,
528
+ x: penX + pb.left * fontSizePx,
529
+ y: baseline - pb.top * fontSizePx,
530
+ w: (pb.right - pb.left) * fontSizePx,
531
+ h: (pb.top - pb.bottom) * fontSizePx,
532
+ u0: ab.left / aw,
533
+ v0,
534
+ u1: ab.right / aw,
535
+ v1
536
+ });
537
+ }
538
+ penX += def.advance * fontSizePx + letterSpacing;
539
+ prevCode = code;
540
+ }
541
+ maxAdvance = Math.max(maxAdvance, penX - x);
542
+ return {
543
+ glyphs,
544
+ width: maxAdvance,
545
+ height: (line + 1) * lineHeight * fontSizePx
546
+ };
547
+ }
548
+ }, _class3.__initStatic(), _class3);
549
+
550
+ // src/text/MSDFTextEntity.ts
551
+ var MSDFTextEntity = (_class4 = class extends Entity {
552
+
553
+
554
+
555
+
556
+
557
+
558
+
559
+ __init24() {this.text = ""}
560
+ __init25() {this.lastRenderedSeqId = 0}
561
+ __init26() {this.rgbColorCache = /* @__PURE__ */ new Map()}
562
+ __init27() {this.fontStringCache = []}
563
+ __init28() {this.layoutResult = null}
564
+ constructor(text, options) {
565
+ super();_class4.prototype.__init24.call(this);_class4.prototype.__init25.call(this);_class4.prototype.__init26.call(this);_class4.prototype.__init27.call(this);_class4.prototype.__init28.call(this);;
566
+ this.font = options.font;
567
+ this.texture = options.texture;
568
+ this.fallbackFont = _nullishCoalesce(options.fallbackFont, () => ( "sans-serif"));
569
+ this.fontSize = _nullishCoalesce(options.fontSize, () => ( 32));
570
+ this.color = _nullishCoalesce(options.color, () => ( "#ffffff"));
571
+ this.letterSpacing = _nullishCoalesce(options.letterSpacing, () => ( 0));
572
+ this.lineHeight = options.lineHeight;
573
+ this.setText(text);
574
+ }
575
+ setText(text) {
576
+ if (this.text === text && this.layoutResult) return;
577
+ this.text = text;
578
+ _chunkRW6NC4RBjs.LayoutWorkerManager.getInstance().queueLayout(this.id, this.text, {
579
+ fontId: this.font.id,
580
+ fontSize: this.fontSize,
581
+ maxWidth: 1e3,
582
+ // standard wrap boundary
583
+ maxHeight: 1e3,
584
+ fontData: this.font.data,
585
+ letterSpacing: this.letterSpacing,
586
+ lineHeight: this.lineHeight,
587
+ callback: (res) => {
588
+ if (res.seqId < this.lastRenderedSeqId) return;
589
+ this.lastRenderedSeqId = res.seqId;
590
+ this.layoutResult = res;
591
+ _optionalChain([this, 'access', _18 => _18.scene, 'optionalAccess', _19 => _19.markDirty, 'call', _20 => _20()]);
592
+ }
593
+ });
594
+ }
595
+ isPointInside(globalX, globalY) {
596
+ if (!this.layoutResult) return false;
597
+ const pos = this.getGlobalPosition();
598
+ const scale = this.getWorldScale();
599
+ const lx = (globalX - pos.x) / scale.x;
600
+ const ly = (globalY - pos.y) / scale.y;
601
+ return lx >= 0 && lx <= this.layoutResult.width && ly >= 0 && ly <= this.layoutResult.height;
602
+ }
603
+ render(renderer) {
604
+ if (!this.layoutResult) return;
605
+ const scene = this.scene;
606
+ if (scene && scene.pointRenderer && scene.glCanvas) {
607
+ scene.pointRenderer.setMSDFTexture(this.texture, this.font.distanceRange);
608
+ const globalPos = this.getGlobalPosition();
609
+ const scale = this.getWorldScale();
610
+ const worldRot = this.getWorldRotation();
611
+ const rCos = Math.cos(worldRot);
612
+ const rSin = Math.sin(worldRot);
613
+ const len2 = this.layoutResult.codePoints.length;
614
+ for (let i = 0; i < len2; i++) {
615
+ const code = this.layoutResult.codePoints[i];
616
+ const nodeX = this.layoutResult.xCoords[i];
617
+ const nodeY = this.layoutResult.yCoords[i];
618
+ const packedStyle = this.layoutResult.packedStyles[i];
619
+ const def = this.font.getGlyph(code);
620
+ if (!def || !def.atlasBounds || !def.planeBounds) continue;
621
+ const { atlasBounds: ab, planeBounds: pb } = def;
622
+ const aw = this.font.atlasWidth;
623
+ const ah = this.font.atlasHeight;
624
+ const lx = (nodeX + pb.left * this.fontSize) * scale.x;
625
+ const ly = (nodeY - pb.top * this.fontSize) * scale.y;
626
+ const glyphX = globalPos.x + lx * rCos - ly * rSin;
627
+ const glyphY = globalPos.y + lx * rSin + ly * rCos;
628
+ const glyphW = (pb.right - pb.left) * this.fontSize * scale.x;
629
+ const glyphH = (pb.top - pb.bottom) * this.fontSize * scale.y;
630
+ const v0 = this.font.data.atlas.yOrigin === "bottom" ? 1 - ab.top / ah : ab.top / ah;
631
+ const v1 = this.font.data.atlas.yOrigin === "bottom" ? 1 - ab.bottom / ah : ab.bottom / ah;
632
+ const colorVal = packedStyle >>> 8;
633
+ let runColor = this.rgbColorCache.get(colorVal);
634
+ if (!runColor) {
635
+ const r = colorVal >> 16 & 255;
636
+ const g = colorVal >> 8 & 255;
637
+ const b = colorVal & 255;
638
+ runColor = `rgb(${r},${g},${b})`;
639
+ this.rgbColorCache.set(colorVal, runColor);
640
+ }
641
+ scene.pointRenderer.addGlyph(
642
+ glyphX,
643
+ glyphY,
644
+ glyphW,
645
+ glyphH,
646
+ ab.left / aw,
647
+ v0,
648
+ ab.right / aw,
649
+ v1,
650
+ runColor,
651
+ this.opacity,
652
+ worldRot
653
+ );
654
+ }
655
+ return;
656
+ }
657
+ if (this.fontStringCache.length === 0) {
658
+ this.fontStringCache[0] = `${this.fontSize}px ${this.fallbackFont}`;
659
+ this.fontStringCache[1] = `bold ${this.fontSize}px ${this.fallbackFont}`;
660
+ this.fontStringCache[2] = `italic ${this.fontSize}px ${this.fallbackFont}`;
661
+ this.fontStringCache[3] = `italic bold ${this.fontSize}px ${this.fallbackFont}`;
662
+ }
663
+ const len = this.layoutResult.codePoints.length;
664
+ for (let i = 0; i < len; i++) {
665
+ const code = this.layoutResult.codePoints[i];
666
+ const nodeX = this.layoutResult.xCoords[i];
667
+ const nodeY = this.layoutResult.yCoords[i];
668
+ const packedStyle = this.layoutResult.packedStyles[i];
669
+ const fontString = this.fontStringCache[packedStyle & 3];
670
+ const colorVal = packedStyle >>> 8;
671
+ let runColor = this.rgbColorCache.get(colorVal);
672
+ if (!runColor) {
673
+ const r = colorVal >> 16 & 255;
674
+ const g = colorVal >> 8 & 255;
675
+ const b = colorVal & 255;
676
+ runColor = `rgb(${r},${g},${b})`;
677
+ this.rgbColorCache.set(colorVal, runColor);
678
+ }
679
+ renderer.fillText(String.fromCodePoint(code), nodeX, nodeY, fontString, runColor);
680
+ }
681
+ }
682
+ destroy() {
683
+ _chunkRW6NC4RBjs.LayoutWorkerManager.getInstance().cancelLayout(this.id);
684
+ super.destroy();
685
+ }
686
+ }, _class4);
687
+
688
+ // src/text/SVGEntity.ts
689
+ var SVGEntity = (_class5 = class extends Entity {
690
+ __init29() {this.svgSource = ""}
691
+ __init30() {this.imageBitmap = null}
692
+ __init31() {this.imageElement = null}
693
+ __init32() {this.blobURL = null}
694
+ __init33() {this.currentImg = null}
695
+ __init34() {this.lodTimeout = null}
696
+ __init35() {this.cachedDoc = null}
697
+ __init36() {this.baseWidth = 100}
698
+ __init37() {this.baseHeight = 100}
699
+ __init38() {this.lastRasterizedScale = 1}
700
+ __init39() {this.targetScale = 1}
701
+ constructor(svgSource, id) {
702
+ super(id);_class5.prototype.__init29.call(this);_class5.prototype.__init30.call(this);_class5.prototype.__init31.call(this);_class5.prototype.__init32.call(this);_class5.prototype.__init33.call(this);_class5.prototype.__init34.call(this);_class5.prototype.__init35.call(this);_class5.prototype.__init36.call(this);_class5.prototype.__init37.call(this);_class5.prototype.__init38.call(this);_class5.prototype.__init39.call(this);;
703
+ this.setSVGSource(svgSource);
704
+ }
705
+ setSVGSource(svgSource) {
706
+ if (this.svgSource === svgSource) return;
707
+ this.svgSource = svgSource;
708
+ this.cachedDoc = null;
709
+ this.parseSVGDimensions();
710
+ this.triggerRasterization(this.lastRasterizedScale);
711
+ }
712
+ parseSVGDimensions() {
713
+ let width = 100;
714
+ let height = 100;
715
+ if (typeof window !== "undefined" && typeof DOMParser !== "undefined") {
716
+ try {
717
+ const parser = new DOMParser();
718
+ const doc = parser.parseFromString(this.svgSource, "image/svg+xml");
719
+ const parserError = doc.querySelector("parsererror");
720
+ if (parserError) {
721
+ console.error("SVG Parsing error:", parserError.textContent);
722
+ } else {
723
+ this.cachedDoc = doc;
724
+ const svgEl = doc.documentElement;
725
+ const wAttr = svgEl.getAttribute("width");
726
+ const hAttr = svgEl.getAttribute("height");
727
+ const vbAttr = svgEl.getAttribute("viewBox");
728
+ if (wAttr && hAttr) {
729
+ width = parseFloat(wAttr) || 100;
730
+ height = parseFloat(hAttr) || 100;
731
+ } else if (vbAttr) {
732
+ const parts = vbAttr.split(/[\s,]+/).map(parseFloat);
733
+ if (parts.length === 4) {
734
+ width = parts[2];
735
+ height = parts[3];
736
+ }
737
+ }
738
+ }
739
+ } catch (e) {
740
+ console.error("Failed parsing SVG via DOMParser, falling back to regex:", e);
741
+ }
742
+ } else {
743
+ const wMatch = /<svg[^>]*\bwidth\s*=\s*["']([^"']+)["']/i.exec(this.svgSource);
744
+ const hMatch = /<svg[^>]*\bheight\s*=\s*["']([^"']+)["']/i.exec(this.svgSource);
745
+ const vbMatch = /<svg[^>]*\bviewBox\s*=\s*["']([^"']+)["']/i.exec(this.svgSource);
746
+ if (wMatch && hMatch) {
747
+ width = parseFloat(wMatch[1]) || 100;
748
+ height = parseFloat(hMatch[1]) || 100;
749
+ } else if (vbMatch) {
750
+ const parts = vbMatch[1].split(/[\s,]+/).map(parseFloat);
751
+ if (parts.length === 4) {
752
+ width = parts[2];
753
+ height = parts[3];
754
+ }
755
+ }
756
+ }
757
+ this.baseWidth = width;
758
+ this.baseHeight = height;
759
+ this.width = width;
760
+ this.height = height;
761
+ }
762
+ triggerRasterization(scale) {
763
+ if (typeof window === "undefined" || typeof Blob === "undefined") return;
764
+ if (this.currentImg) {
765
+ this.currentImg.onload = null;
766
+ this.currentImg.onerror = null;
767
+ this.currentImg = null;
768
+ }
769
+ if (this.blobURL) {
770
+ URL.revokeObjectURL(this.blobURL);
771
+ this.blobURL = null;
772
+ }
773
+ let processedSource = this.svgSource;
774
+ try {
775
+ let doc = this.cachedDoc;
776
+ if (!doc) {
777
+ const parser = new DOMParser();
778
+ doc = parser.parseFromString(this.svgSource, "image/svg+xml");
779
+ this.cachedDoc = doc;
780
+ }
781
+ const parserError = doc.querySelector("parsererror");
782
+ if (parserError) {
783
+ console.error(
784
+ "SVG Parsing validation error in triggerRasterization:",
785
+ parserError.textContent
786
+ );
787
+ } else {
788
+ const clonedDoc = doc.cloneNode(true);
789
+ const svgEl = clonedDoc.documentElement;
790
+ if (svgEl.tagName.toLowerCase() === "svg") {
791
+ const targetWidth = Math.max(1, Math.round(this.baseWidth * scale));
792
+ const targetHeight = Math.max(1, Math.round(this.baseHeight * scale));
793
+ svgEl.setAttribute("width", `${targetWidth}`);
794
+ svgEl.setAttribute("height", `${targetHeight}`);
795
+ if (!svgEl.hasAttribute("viewBox")) {
796
+ svgEl.setAttribute("viewBox", `0 0 ${this.baseWidth} ${this.baseHeight}`);
797
+ }
798
+ const serializer = new XMLSerializer();
799
+ processedSource = serializer.serializeToString(clonedDoc);
800
+ }
801
+ }
802
+ } catch (e) {
803
+ console.error("Failed to apply LOD scaling to SVG XML:", e);
804
+ }
805
+ const blob = new Blob([processedSource], { type: "image/svg+xml;charset=utf-8" });
806
+ this.blobURL = URL.createObjectURL(blob);
807
+ const img = new Image();
808
+ img.crossOrigin = "anonymous";
809
+ this.currentImg = img;
810
+ img.onload = () => {
811
+ if (this.currentImg !== img) return;
812
+ this.imageElement = img;
813
+ if (typeof createImageBitmap === "undefined") {
814
+ this.currentImg = null;
815
+ if (this.scene) this.scene.markDirty();
816
+ return;
817
+ }
818
+ createImageBitmap(img).then((bitmap) => {
819
+ if (this.currentImg !== img) {
820
+ bitmap.close();
821
+ return;
822
+ }
823
+ if (this.imageBitmap) {
824
+ this.imageBitmap.close();
825
+ }
826
+ this.imageBitmap = bitmap;
827
+ this.currentImg = null;
828
+ if (this.scene) this.scene.markDirty();
829
+ }).catch((e) => {
830
+ console.error("Failed to create ImageBitmap from SVG:", e);
831
+ this.currentImg = null;
832
+ });
833
+ };
834
+ img.onerror = (e) => {
835
+ if (this.currentImg !== img) return;
836
+ console.error("Failed to load SVG Image element:", e);
837
+ this.currentImg = null;
838
+ };
839
+ img.src = this.blobURL;
840
+ }
841
+ isPointInside(globalX, globalY) {
842
+ const pos = this.getGlobalPosition();
843
+ const scale = this.getWorldScale();
844
+ const rot = this.getWorldRotation();
845
+ const dx = globalX - pos.x;
846
+ const dy = globalY - pos.y;
847
+ const cos = Math.cos(-rot);
848
+ const sin = Math.sin(-rot);
849
+ const lx = (dx * cos - dy * sin) / scale.x;
850
+ const ly = (dx * sin + dy * cos) / scale.y;
851
+ return lx >= 0 && lx <= this.width && ly >= 0 && ly <= this.height;
852
+ }
853
+ render(r) {
854
+ const isSVGExporter = typeof r.toXMLString === "function";
855
+ if (isSVGExporter) {
856
+ const dataUri = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(this.svgSource);
857
+ r.drawImage({ src: dataUri }, 0, 0, this.width, this.height);
858
+ return;
859
+ }
860
+ const scale = this.getWorldScale();
861
+ const currentScale = Math.max(0.1, Math.max(scale.x, scale.y));
862
+ if (Math.abs(currentScale - this.lastRasterizedScale) / this.lastRasterizedScale > 0.2) {
863
+ this.targetScale = currentScale;
864
+ if (this.lodTimeout) clearTimeout(this.lodTimeout);
865
+ this.lodTimeout = setTimeout(() => {
866
+ this.triggerRasterization(this.targetScale);
867
+ this.lastRasterizedScale = this.targetScale;
868
+ this.lodTimeout = null;
869
+ }, 200);
870
+ }
871
+ if (this.imageBitmap) {
872
+ r.drawImage(this.imageBitmap, 0, 0, this.width, this.height);
873
+ } else if (this.imageElement) {
874
+ r.drawImage(this.imageElement, 0, 0, this.width, this.height);
875
+ }
876
+ }
877
+ destroy() {
878
+ if (this.lodTimeout) {
879
+ clearTimeout(this.lodTimeout);
880
+ this.lodTimeout = null;
881
+ }
882
+ if (this.currentImg) {
883
+ this.currentImg.onload = null;
884
+ this.currentImg.onerror = null;
885
+ this.currentImg = null;
886
+ }
887
+ if (this.imageBitmap) {
888
+ this.imageBitmap.close();
889
+ this.imageBitmap = null;
890
+ }
891
+ if (this.blobURL) {
892
+ URL.revokeObjectURL(this.blobURL);
893
+ this.blobURL = null;
894
+ }
895
+ this.imageElement = null;
896
+ this.cachedDoc = null;
897
+ super.destroy();
898
+ }
899
+ }, _class5);
900
+
901
+
902
+
903
+
904
+
905
+
906
+
907
+ exports.VectoJSEvent = VectoJSEvent; exports.Entity = Entity; exports.MSDFFont = MSDFFont; exports.MSDFTextEntity = MSDFTextEntity; exports.SVGEntity = SVGEntity;