@vectojs/core 1.15.0 → 1.16.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.
@@ -1,4 +1,6 @@
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;// src/tree/Entity.ts
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;const __vecto_cjs_url=require("url").pathToFileURL(__filename).href;
2
+
3
+ // src/tree/Entity.ts
2
4
 
3
5
 
4
6
 
@@ -146,8 +148,42 @@ var Entity = (_class2 = class {
146
148
  // Danmaku hot loop), so a bare `entity.x = v` is one boolean check + field write.
147
149
  __init11() {this._hasTransitions = false}
148
150
  __init12() {this._transitions = null}
149
- __init13() {this._drivers = /* @__PURE__ */ new Map()}
151
+ // Lazily allocated: null until first use. A scene of many passive entities
152
+ // (particles, data points) never touches these, so paying an empty-Map/array
153
+ // allocation per entity in the constructor is pure waste at scale.
154
+ __init13() {this._drivers = null}
150
155
  __init14() {this._mounted = false}
156
+ __init15() {this._destroyed = false}
157
+ // Frame this entity's active drivers were last advanced by Scene's batched
158
+ // WASM animation pass (see Scene._tickBatchedDrivers), or -1 if never. When
159
+ // it equals the current frame, tickDrivers() must skip its own tick loop —
160
+ // otherwise a driver already advanced by the batch pass would be ticked a
161
+ // second time by the normal per-entity update() walk in the same frame.
162
+ // Irrelevant (and harmless) for entities never touched by WASM batching.
163
+ __init16() {this._driversTickedFrame = -1}
164
+ // Cached cos/sin, recomputed only when rotation actually changes. renderNode
165
+ // and getWorldTransform() both read this instead of calling Math.cos/sin per
166
+ // entity per frame (V8's are ~2.5x slower than other engines).
167
+ __init17() {this._trig = { cos: 1, sin: 0 }}
168
+ __init18() {this._trigRotation = Number.NaN}
169
+ // NaN !== any rotation -> first read computes
170
+ // Per-frame world-matrix cache. Written by Scene during the render walk;
171
+ // getWorldTransform() returns it only while `_worldFrame === scene.currentFrame`
172
+ // and otherwise falls back to the full ancestor walk, so it can never return a
173
+ // stale/wrong transform — only sometimes miss the fast path.
174
+ __init19() {this._wa = 1}
175
+ __init20() {this._wb = 0}
176
+ __init21() {this._wc = 0}
177
+ __init22() {this._wd = 1}
178
+ __init23() {this._we = 0}
179
+ __init24() {this._wf = 0}
180
+ __init25() {this._worldFrame = -1}
181
+ // Slot in the Scene's resident WASM transform store, or -1 when this entity is
182
+ // not in that store (JS transform path, overlay/detached, or before the first
183
+ // structural rebuild). Assigned by Scene on a structural rebuild; the Scene
184
+ // validates it against its slot table before trusting it, so a stale value can
185
+ // only cost a JS-path fallback, never a wrong read.
186
+ __init26() {this._storeSlot = -1}
151
187
  get x() {
152
188
  return this._x;
153
189
  }
@@ -190,8 +226,8 @@ var Entity = (_class2 = class {
190
226
  if (this._hasTransitions) this._animateProp("opacity", v);
191
227
  else this._opacity = v;
192
228
  }
193
- __init15() {this.isDOMPortal = false}
194
- __init16() {this._interactive = false}
229
+ __init27() {this.isDOMPortal = false}
230
+ __init28() {this._interactive = false}
195
231
  get interactive() {
196
232
  return this._interactive;
197
233
  }
@@ -205,10 +241,10 @@ var Entity = (_class2 = class {
205
241
  }
206
242
  }
207
243
  }
208
- __init17() {this.width = 0}
209
- __init18() {this.height = 0}
210
- __init19() {this.a11yOffsetX = 0}
211
- __init20() {this.a11yOffsetY = 0}
244
+ __init29() {this.width = 0}
245
+ __init30() {this.height = 0}
246
+ __init31() {this.a11yOffsetX = 0}
247
+ __init32() {this.a11yOffsetY = 0}
212
248
  /**
213
249
  * Opt in to a viewport-filling accessibility/automation shadow node even when
214
250
  * this entity has no intrinsic box (`width`/`height` of `0`). Use for
@@ -216,19 +252,21 @@ var Entity = (_class2 = class {
216
252
  * that need global pointer events. The node is mounted behind all other shadow
217
253
  * nodes, so on-top components stay clickable.
218
254
  */
219
- __init21() {this.a11yFullViewport = false}
255
+ __init33() {this.a11yFullViewport = false}
220
256
  /**
221
257
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
222
258
  * rendering. Combined with translating a content child, this is how
223
259
  * scroll/overflow containers (e.g. `ScrollView`) keep their content inside a
224
260
  * fixed viewport. Off by default (children render unclipped). Canvas2D only.
225
261
  */
226
- __init22() {this.clipChildren = false}
227
- __init23() {this.listeners = /* @__PURE__ */ new Map()}
262
+ __init34() {this.clipChildren = false}
263
+ // Lazily allocated (see _drivers above). Most entities never register a
264
+ // listener or an imperative animate() tween.
265
+ __init35() {this.listeners = null}
228
266
  /** Capture-phase listeners (fired root→target before bubble). */
229
- __init24() {this.captureListeners = /* @__PURE__ */ new Map()}
230
- __init25() {this.animations = []}
231
- 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);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);
267
+ __init36() {this.captureListeners = null}
268
+ __init37() {this.animations = null}
269
+ 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);_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);
232
270
  this.id = id || `entity_${Math.random().toString(36).substring(2, 9)}`;
233
271
  }
234
272
  /**
@@ -262,6 +300,7 @@ var Entity = (_class2 = class {
262
300
  const s = this.scene;
263
301
  if (s) {
264
302
  s.a11yNeedsReorder = true;
303
+ _optionalChain([s, 'access', _40 => _40.markStructureChanged, 'optionalCall', _41 => _41()]);
265
304
  s.markDirty();
266
305
  child._notifyMounted();
267
306
  }
@@ -291,6 +330,7 @@ var Entity = (_class2 = class {
291
330
  if (s) {
292
331
  s.detachA11y(child);
293
332
  s.a11yNeedsReorder = true;
333
+ _optionalChain([s, 'access', _42 => _42.markStructureChanged, 'optionalCall', _43 => _43()]);
294
334
  s.markDirty();
295
335
  }
296
336
  }
@@ -339,13 +379,13 @@ var Entity = (_class2 = class {
339
379
  * @example entity.animate({ x: 400, opacity: 0 }, 500);
340
380
  */
341
381
  animate(targetProps, durationMs) {
342
- this.animations.push({
382
+ (this.animations ??= []).push({
343
383
  target: targetProps,
344
384
  duration: durationMs,
345
385
  startTime: -1,
346
386
  startProps: {}
347
387
  });
348
- _optionalChain([this, 'access', _40 => _40.scene, 'optionalAccess', _41 => _41.markDirty, 'call', _42 => _42()]);
388
+ _optionalChain([this, 'access', _44 => _44.scene, 'optionalAccess', _45 => _45.markDirty, 'call', _46 => _46()]);
349
389
  return this;
350
390
  }
351
391
  /** Write a driver-computed value to a backing field without re-triggering the setter. */
@@ -392,26 +432,26 @@ var Entity = (_class2 = class {
392
432
  * that need to seed a starting state (e.g. the presence helper's enter `from`).
393
433
  */
394
434
  setImmediate(prop, v) {
395
- const existing = this._drivers.get(prop);
435
+ const existing = _optionalChain([this, 'access', _47 => _47._drivers, 'optionalAccess', _48 => _48.get, 'call', _49 => _49(prop)]);
396
436
  if (existing) this._settleDriver(existing);
397
- this._drivers.delete(prop);
437
+ _optionalChain([this, 'access', _50 => _50._drivers, 'optionalAccess', _51 => _51.delete, 'call', _52 => _52(prop)]);
398
438
  this._applyAnimated(prop, v);
399
439
  }
400
440
  _settleDriver(driver) {
401
441
  const active = driver;
402
442
  const onDone = active.onDone;
403
443
  active.onDone = void 0;
404
- _optionalChain([onDone, 'optionalCall', _43 => _43()]);
444
+ _optionalChain([onDone, 'optionalCall', _53 => _53()]);
405
445
  }
406
446
  _spawnDriver(prop, to, cfg) {
407
- if (prop !== "opacity" && _optionalChain([this, 'access', _44 => _44.scene, 'optionalAccess', _45 => _45.prefersReducedMotion])) {
408
- const existing2 = this._drivers.get(prop);
447
+ if (prop !== "opacity" && _optionalChain([this, 'access', _54 => _54.scene, 'optionalAccess', _55 => _55.prefersReducedMotion])) {
448
+ const existing2 = _optionalChain([this, 'access', _56 => _56._drivers, 'optionalAccess', _57 => _57.get, 'call', _58 => _58(prop)]);
409
449
  if (existing2) this._settleDriver(existing2);
410
- this._drivers.delete(prop);
450
+ _optionalChain([this, 'access', _59 => _59._drivers, 'optionalAccess', _60 => _60.delete, 'call', _61 => _61(prop)]);
411
451
  this._applyAnimated(prop, to);
412
452
  return;
413
453
  }
414
- const existing = this._drivers.get(prop);
454
+ const existing = _optionalChain([this, 'access', _62 => _62._drivers, 'optionalAccess', _63 => _63.get, 'call', _64 => _64(prop)]);
415
455
  if (existing) {
416
456
  this._settleDriver(existing);
417
457
  existing.retarget(to);
@@ -419,12 +459,13 @@ var Entity = (_class2 = class {
419
459
  }
420
460
  const from = this._currentOf(prop);
421
461
  const driver = _animation.isTweenConfig.call(void 0, cfg) ? new (0, _animation.TweenDriver)(from, to, cfg) : new (0, _animation.SpringDriver)(from, to, cfg === "spring" ? {} : cfg);
422
- this._drivers.set(prop, driver);
423
- _optionalChain([this, 'access', _46 => _46.scene, 'optionalAccess', _47 => _47.markDirty, 'call', _48 => _48()]);
462
+ (this._drivers ??= /* @__PURE__ */ new Map()).set(prop, driver);
463
+ _optionalChain([this, 'access', _65 => _65.scene, 'optionalAccess', _66 => _66.markDirty, 'call', _67 => _67()]);
464
+ _optionalChain([this, 'access', _68 => _68.scene, 'optionalAccess', _69 => _69._registerActiveDriverEntity, 'call', _70 => _70(this)]);
424
465
  }
425
466
  /** Assignment path when a declarative transition is configured for `prop`. */
426
467
  _animateProp(prop, to) {
427
- const cfg = _optionalChain([this, 'access', _49 => _49._transitions, 'optionalAccess', _50 => _50.get, 'call', _51 => _51(prop)]);
468
+ const cfg = _optionalChain([this, 'access', _71 => _71._transitions, 'optionalAccess', _72 => _72.get, 'call', _73 => _73(prop)]);
428
469
  if (!cfg) {
429
470
  this._applyAnimated(prop, to);
430
471
  return;
@@ -453,7 +494,7 @@ var Entity = (_class2 = class {
453
494
  entries.map(
454
495
  (e) => new Promise((resolve) => {
455
496
  this._spawnDriver(e[0], e[1], cfg);
456
- const d = this._drivers.get(e[0]);
497
+ const d = _optionalChain([this, 'access', _74 => _74._drivers, 'optionalAccess', _75 => _75.get, 'call', _76 => _76(e[0])]);
457
498
  if (!d)
458
499
  resolve();
459
500
  else d.onDone = resolve;
@@ -463,7 +504,8 @@ var Entity = (_class2 = class {
463
504
  }
464
505
  /** Advance active property drivers one frame. Call from update(). */
465
506
  tickDrivers(dt) {
466
- if (this._drivers.size === 0) return;
507
+ if (!this._drivers || this._drivers.size === 0) return;
508
+ if (this.scene && this._driversTickedFrame === this.scene.currentFrame) return;
467
509
  for (const [prop, driver] of this._drivers) {
468
510
  driver.tick(dt);
469
511
  if (driver.isDone()) {
@@ -474,7 +516,39 @@ var Entity = (_class2 = class {
474
516
  this._applyAnimated(prop, driver.value);
475
517
  }
476
518
  }
477
- _optionalChain([this, 'access', _52 => _52.scene, 'optionalAccess', _53 => _53.markDirty, 'call', _54 => _54()]);
519
+ _optionalChain([this, 'access', _77 => _77.scene, 'optionalAccess', _78 => _78.markDirty, 'call', _79 => _79()]);
520
+ }
521
+ /**
522
+ * Internal: this entity's active-driver map (read-only view), or `null` if
523
+ * it has none. Called only by Scene's batched WASM animation pass, never
524
+ * application code. Returns the Map directly (not a callback iteration) so
525
+ * a caller can `for...of` it with zero per-entity closure allocation — the
526
+ * integrated benchmark (benchmarks/anim-wasm-scene) found a fresh callback
527
+ * per entity per frame was a real cost, not a negligible one.
528
+ */
529
+ _driverEntries() {
530
+ return this._drivers;
531
+ }
532
+ /**
533
+ * Internal: finalize one driver that was ALREADY advanced externally this
534
+ * frame (e.g. by Scene's batched WASM tick via `driver.syncExternal`, or a
535
+ * direct `driver.tick()` call for a driver the batch can't offload) —
536
+ * exactly mirrors tickDrivers()'s own per-driver completion logic, so a
537
+ * driver behaves identically regardless of which path ticked it. Called
538
+ * only by Scene, never application code.
539
+ */
540
+ _applyDriverTick(prop, driver) {
541
+ if (driver.isDone()) {
542
+ this._applyAnimated(prop, driver.target);
543
+ this._settleDriver(driver);
544
+ _optionalChain([this, 'access', _80 => _80._drivers, 'optionalAccess', _81 => _81.delete, 'call', _82 => _82(prop)]);
545
+ } else {
546
+ this._applyAnimated(prop, driver.value);
547
+ }
548
+ }
549
+ /** Internal: true if this entity currently has any active property driver. */
550
+ _hasActiveDrivers() {
551
+ return !!this._drivers && this._drivers.size > 0;
478
552
  }
479
553
  /**
480
554
  * Advance the entity's internal state for one frame.
@@ -487,7 +561,7 @@ var Entity = (_class2 = class {
487
561
  */
488
562
  update(dt, time) {
489
563
  this.tickDrivers(dt);
490
- if (this.animations.length > 0) {
564
+ if (this.animations && this.animations.length > 0) {
491
565
  const anim = this.animations[0];
492
566
  if (anim.startTime === -1) {
493
567
  anim.startTime = time;
@@ -528,7 +602,7 @@ var Entity = (_class2 = class {
528
602
  * @example entity.on('click', (e) => console.log('clicked', e));
529
603
  */
530
604
  on(event, callback, options) {
531
- const map = _optionalChain([options, 'optionalAccess', _55 => _55.capture]) ? this.captureListeners : this.listeners;
605
+ const map = _optionalChain([options, 'optionalAccess', _83 => _83.capture]) ? this.captureListeners ??= /* @__PURE__ */ new Map() : this.listeners ??= /* @__PURE__ */ new Map();
532
606
  if (!map.has(event)) {
533
607
  map.set(event, []);
534
608
  }
@@ -544,7 +618,7 @@ var Entity = (_class2 = class {
544
618
  * @returns `this` for method chaining.
545
619
  */
546
620
  off(event, callback, options) {
547
- const handlers = (_optionalChain([options, 'optionalAccess', _56 => _56.capture]) ? this.captureListeners : this.listeners).get(event);
621
+ const handlers = _optionalChain([(_optionalChain([options, 'optionalAccess', _84 => _84.capture]) ? this.captureListeners : this.listeners), 'optionalAccess', _85 => _85.get, 'call', _86 => _86(event)]);
548
622
  if (handlers) {
549
623
  const idx = handlers.indexOf(callback);
550
624
  if (idx !== -1) handlers.splice(idx, 1);
@@ -552,17 +626,37 @@ var Entity = (_class2 = class {
552
626
  return this;
553
627
  }
554
628
  /**
555
- * Tear down this entity: clear all animations, event listeners, and detach
556
- * from parent. Call before discarding an entity to prevent memory leaks.
629
+ * Tear down this entity **and its entire subtree**: recursively destroy every
630
+ * descendant (leaf-first), clear all animations, event listeners, and property
631
+ * drivers, then detach from the parent. Call before discarding an entity to
632
+ * prevent memory leaks.
633
+ *
634
+ * Recursing here is what frees a subtree's GPU buffers, layout workers, and DOM
635
+ * observers when an app does `entity.destroy()` or `scene.remove(subtree)` on a
636
+ * route change — without it, only the root's own state was released and every
637
+ * descendant (and its subclass resources) was stranded. Subclasses that own
638
+ * external resources override `destroy()`, free their resource, then call
639
+ * `super.destroy()`; because children don't depend on a parent's resource, the
640
+ * order (parent resource first, then descendants) is safe.
641
+ *
642
+ * Idempotent and re-entrancy safe: a second call is a no-op, and because each
643
+ * child is detached as it is destroyed, destroying a subtree never double-frees.
557
644
  */
558
645
  destroy() {
559
- this.animations = [];
560
- for (const driver of this._drivers.values()) {
561
- this._settleDriver(driver);
646
+ if (this._destroyed) return;
647
+ this._destroyed = true;
648
+ while (this.children.length > 0) {
649
+ this.children.at(-1).destroy();
650
+ }
651
+ this.animations = null;
652
+ if (this._drivers) {
653
+ for (const driver of this._drivers.values()) {
654
+ this._settleDriver(driver);
655
+ }
656
+ this._drivers.clear();
562
657
  }
563
- this._drivers.clear();
564
- this.listeners.clear();
565
- this.captureListeners.clear();
658
+ _optionalChain([this, 'access', _87 => _87.listeners, 'optionalAccess', _88 => _88.clear, 'call', _89 => _89()]);
659
+ _optionalChain([this, 'access', _90 => _90.captureListeners, 'optionalAccess', _91 => _91.clear, 'call', _92 => _92()]);
566
660
  if (this.parent) {
567
661
  this.parent.remove(this);
568
662
  }
@@ -577,7 +671,7 @@ var Entity = (_class2 = class {
577
671
  * @param payload - Arbitrary data forwarded to each listener.
578
672
  */
579
673
  emit(event, payload) {
580
- const handlers = this.listeners.get(event);
674
+ const handlers = _optionalChain([this, 'access', _93 => _93.listeners, 'optionalAccess', _94 => _94.get, 'call', _95 => _95(event)]);
581
675
  if (handlers) {
582
676
  handlers.forEach((h) => h(payload));
583
677
  }
@@ -591,19 +685,19 @@ var Entity = (_class2 = class {
591
685
  * with `requestAnimationFrame(() => document.getElementById(id)?.focus())`.
592
686
  */
593
687
  focus() {
594
- const el = _optionalChain([this, 'access', _57 => _57.scene, 'optionalAccess', _58 => _58.getA11yElement, 'call', _59 => _59(this.id)]);
688
+ const el = _optionalChain([this, 'access', _96 => _96.scene, 'optionalAccess', _97 => _97.getA11yElement, 'call', _98 => _98(this.id)]);
595
689
  if (el) {
596
690
  el.focus();
597
691
  return;
598
692
  }
599
693
  requestAnimationFrame(() => {
600
- const retry = _optionalChain([this, 'access', _60 => _60.scene, 'optionalAccess', _61 => _61.getA11yElement, 'call', _62 => _62(this.id)]);
694
+ const retry = _optionalChain([this, 'access', _99 => _99.scene, 'optionalAccess', _100 => _100.getA11yElement, 'call', _101 => _101(this.id)]);
601
695
  if (retry) retry.focus();
602
696
  });
603
697
  }
604
698
  /** Run one node's listeners for the event, honoring stopImmediatePropagation. */
605
699
  fireListeners(node, map, event) {
606
- const handlers = map.get(event.type);
700
+ const handlers = _optionalChain([map, 'optionalAccess', _102 => _102.get, 'call', _103 => _103(event.type)]);
607
701
  if (!handlers) return;
608
702
  event.currentTarget = node;
609
703
  for (const h of handlers.slice()) {
@@ -642,10 +736,45 @@ var Entity = (_class2 = class {
642
736
  getGlobalPosition() {
643
737
  return this.localToWorld(0, 0);
644
738
  }
739
+ /**
740
+ * Internal: read this entity's cached world matrix into `out` — the SAME
741
+ * cache {@link getWorldTransform} reads — without allocating a wrapper
742
+ * object. Returns `false` (leaving `out` untouched) when the cache isn't
743
+ * valid for `frame` (typically the caller's `scene.currentFrame`), exactly
744
+ * mirroring {@link getWorldTransform}'s own validity check; the caller
745
+ * falls back to {@link getWorldTransform}'s full walk in that case. Exists
746
+ * so a per-entity gather that runs every entity through this (e.g. G3's
747
+ * `gatherHitAABBs`) pays for six scalar reads instead of one object
748
+ * allocation per entity per call — the exact class of per-frame garbage
749
+ * the G2 integrated benchmark found dominating its own gather cost.
750
+ */
751
+ _readWorldCache(frame, out) {
752
+ if (this._worldFrame < 0 || this._worldFrame !== frame) return false;
753
+ out.a = this._wa;
754
+ out.b = this._wb;
755
+ out.c = this._wc;
756
+ out.d = this._wd;
757
+ out.e = this._we;
758
+ out.f = this._wf;
759
+ return true;
760
+ }
645
761
  /**
646
762
  * Return the exact accumulated Canvas `T * S * R` transform for this entity.
647
763
  */
648
764
  getWorldTransform() {
765
+ if (this._worldFrame >= 0) {
766
+ const s = this.scene;
767
+ if (s && this._worldFrame === s.currentFrame) {
768
+ return {
769
+ a: this._wa,
770
+ b: this._wb,
771
+ c: this._wc,
772
+ d: this._wd,
773
+ e: this._we,
774
+ f: this._wf
775
+ };
776
+ }
777
+ }
649
778
  const path = [this];
650
779
  let ancestor = this.parent;
651
780
  while (ancestor) {
@@ -660,8 +789,9 @@ var Entity = (_class2 = class {
660
789
  let f = 0;
661
790
  for (let i = path.length - 1; i >= 0; i--) {
662
791
  const node = path[i];
663
- const cos = Math.cos(node.rotation);
664
- const sin = Math.sin(node.rotation);
792
+ const trig = node._getTrig();
793
+ const cos = trig.cos;
794
+ const sin = trig.sin;
665
795
  const la = node.scaleX * cos;
666
796
  const lb = node.scaleY * sin;
667
797
  const lc = -node.scaleX * sin;
@@ -683,6 +813,37 @@ var Entity = (_class2 = class {
683
813
  }
684
814
  return { a, b, c, d, e, f };
685
815
  }
816
+ /**
817
+ * Return this entity's cached `{ cos, sin }` of its current rotation,
818
+ * recomputing only when `rotation` has actually changed since the last call.
819
+ * The same object identity is returned across calls with an unchanged
820
+ * rotation, so callers must treat it as read-only. Used by the render walk
821
+ * and {@link getWorldTransform} to avoid V8's comparatively slow Math.cos/sin
822
+ * (a software libm call, ~2.5x slower than other engines) per entity/frame.
823
+ */
824
+ _getTrig() {
825
+ if (this._trigRotation !== this._rotation) {
826
+ this._trig.cos = Math.cos(this._rotation);
827
+ this._trig.sin = Math.sin(this._rotation);
828
+ this._trigRotation = this._rotation;
829
+ }
830
+ return this._trig;
831
+ }
832
+ /**
833
+ * Store the world matrix Scene computed for this entity during the render
834
+ * walk, stamped with the frame it belongs to. {@link getWorldTransform}
835
+ * returns it verbatim while `frame === scene.currentFrame`. Internal: called
836
+ * only by Scene's renderer, never by application code.
837
+ */
838
+ _setWorldCache(a, b, c, d, e, f, frame) {
839
+ this._wa = a;
840
+ this._wb = b;
841
+ this._wc = c;
842
+ this._wd = d;
843
+ this._we = e;
844
+ this._wf = f;
845
+ this._worldFrame = frame;
846
+ }
686
847
  /** Convert a point from this entity's local space to Scene/world space. */
687
848
  localToWorld(localX, localY) {
688
849
  const { a, b, c, d, e, f } = this.getWorldTransform();
@@ -712,7 +873,12 @@ var Entity = (_class2 = class {
712
873
  * does not provide a render-specific box.
713
874
  */
714
875
  getWorldBounds() {
715
- const bounds = _nullishCoalesce(this.getBounds(), () => ( { x: 0, y: 0, width: this.width, height: this.height }));
876
+ const bounds = _nullishCoalesce(this.getBounds(), () => ( {
877
+ x: 0,
878
+ y: 0,
879
+ width: this.width,
880
+ height: this.height
881
+ }));
716
882
  const { a, b, c, d, e, f } = this.getWorldTransform();
717
883
  let minX = Infinity;
718
884
  let minY = Infinity;
@@ -853,7 +1019,7 @@ var Entity = (_class2 = class {
853
1019
  * @returns `true` if at least one animation or property driver remains.
854
1020
  */
855
1021
  hasPendingAnimations() {
856
- return this.animations.length > 0 || this._drivers.size > 0;
1022
+ return (_nullishCoalesce(_optionalChain([this, 'access', _104 => _104.animations, 'optionalAccess', _105 => _105.length]), () => ( 0))) > 0 || (_nullishCoalesce(_optionalChain([this, 'access', _106 => _106._drivers, 'optionalAccess', _107 => _107.size]), () => ( 0))) > 0;
857
1023
  }
858
1024
  }, _class2);
859
1025
 
@@ -869,13 +1035,21 @@ var MSDFTextEntity = (_class3 = class extends Entity {
869
1035
 
870
1036
 
871
1037
 
872
- __init26() {this.text = ""}
873
- __init27() {this.lastRenderedSeqId = 0}
874
- __init28() {this.rgbColorCache = /* @__PURE__ */ new Map()}
875
- __init29() {this.fontStringCache = []}
876
- __init30() {this.layoutResult = null}
1038
+
1039
+ // Optional hyphenator. Runs on the MAIN thread (a function can't be
1040
+ // structure-cloned into the layout worker), turning each word into parts
1041
+ // joined by soft hyphens (U+00AD); the worker then treats those as break
1042
+ // opportunities. `text` keeps the original string for a11y/content
1043
+ // projection; `layoutText` is the soft-hyphen-annotated string sent to layout.
1044
+ __init38() {this.hyphenator = null}
1045
+ __init39() {this.layoutText = ""}
1046
+ __init40() {this.text = ""}
1047
+ __init41() {this.lastRenderedSeqId = 0}
1048
+ __init42() {this.rgbColorCache = /* @__PURE__ */ new Map()}
1049
+ __init43() {this.fontStringCache = []}
1050
+ __init44() {this.layoutResult = null}
877
1051
  constructor(text, options) {
878
- super();_class3.prototype.__init26.call(this);_class3.prototype.__init27.call(this);_class3.prototype.__init28.call(this);_class3.prototype.__init29.call(this);_class3.prototype.__init30.call(this);;
1052
+ super();_class3.prototype.__init38.call(this);_class3.prototype.__init39.call(this);_class3.prototype.__init40.call(this);_class3.prototype.__init41.call(this);_class3.prototype.__init42.call(this);_class3.prototype.__init43.call(this);_class3.prototype.__init44.call(this);;
879
1053
  this.font = options.font;
880
1054
  this.texture = options.texture;
881
1055
  this.fallbackFont = _nullishCoalesce(options.fallbackFont, () => ( "sans-serif"));
@@ -885,6 +1059,7 @@ var MSDFTextEntity = (_class3 = class extends Entity {
885
1059
  this.lineHeight = options.lineHeight;
886
1060
  this.maxWidth = _nullishCoalesce(options.maxWidth, () => ( 1e3));
887
1061
  this.maxHeight = _nullishCoalesce(options.maxHeight, () => ( 1e3));
1062
+ this.textAlign = _nullishCoalesce(options.textAlign, () => ( "left"));
888
1063
  this.setText(text);
889
1064
  }
890
1065
  /** Change the wrap boundary and re-run layout for the current text. */
@@ -893,13 +1068,53 @@ var MSDFTextEntity = (_class3 = class extends Entity {
893
1068
  this.maxWidth = maxWidth;
894
1069
  this.queueLayout();
895
1070
  }
1071
+ /**
1072
+ * Set horizontal alignment (`'justify'` stretches wrapped lines flush to
1073
+ * {@link setMaxWidth}'s width; the last line stays ragged) and re-run layout.
1074
+ */
1075
+ setTextAlign(align) {
1076
+ if (this.textAlign === align) return;
1077
+ this.textAlign = align;
1078
+ this.queueLayout();
1079
+ }
1080
+ /**
1081
+ * Plug a hyphenator (word → parts). Break opportunities are inserted as soft
1082
+ * hyphens (U+00AD) into the string sent to layout, so a word that doesn't fit
1083
+ * can break with a visible hyphen. Soft hyphens already present in the text
1084
+ * work without one. Pass `null` to disable. The original text is preserved
1085
+ * for accessibility — only the layout string carries the hyphens.
1086
+ */
1087
+ setHyphenator(fn) {
1088
+ this.hyphenator = fn;
1089
+ this.rebuildLayoutText();
1090
+ this.queueLayout();
1091
+ }
896
1092
  setText(text) {
897
1093
  if (this.text === text && this.layoutResult) return;
898
1094
  this.text = text;
1095
+ this.rebuildLayoutText();
899
1096
  this.queueLayout();
900
1097
  }
1098
+ /**
1099
+ * Recompute {@link layoutText} from {@link text}: with a hyphenator active,
1100
+ * split each whitespace-delimited word and rejoin its parts with U+00AD so
1101
+ * the worker sees the break opportunities. Without one, the layout string is
1102
+ * the text unchanged.
1103
+ */
1104
+ rebuildLayoutText() {
1105
+ if (!this.hyphenator) {
1106
+ this.layoutText = this.text;
1107
+ return;
1108
+ }
1109
+ const SHY = "\xAD";
1110
+ this.layoutText = this.text.replace(/[^\s]+/g, (word) => {
1111
+ if (word.length <= 3 || word.includes(SHY)) return word;
1112
+ const parts = this.hyphenator(word);
1113
+ return parts.length > 1 ? parts.join(SHY) : word;
1114
+ });
1115
+ }
901
1116
  queueLayout() {
902
- _layout.LayoutWorkerManager.getInstance().queueLayout(this.id, this.text, {
1117
+ _layout.LayoutWorkerManager.getInstance().queueLayout(this.id, this.layoutText, {
903
1118
  fontId: this.font.id,
904
1119
  fontSize: this.fontSize,
905
1120
  maxWidth: this.maxWidth,
@@ -907,11 +1122,12 @@ var MSDFTextEntity = (_class3 = class extends Entity {
907
1122
  fontData: this.font.data,
908
1123
  letterSpacing: this.letterSpacing,
909
1124
  lineHeight: this.lineHeight,
1125
+ textAlign: this.textAlign,
910
1126
  callback: (res) => {
911
1127
  if (res.seqId < this.lastRenderedSeqId) return;
912
1128
  this.lastRenderedSeqId = res.seqId;
913
1129
  this.layoutResult = res;
914
- _optionalChain([this, 'access', _63 => _63.scene, 'optionalAccess', _64 => _64.markDirty, 'call', _65 => _65()]);
1130
+ _optionalChain([this, 'access', _108 => _108.scene, 'optionalAccess', _109 => _109.markDirty, 'call', _110 => _110()]);
915
1131
  }
916
1132
  });
917
1133
  }
@@ -1016,7 +1232,7 @@ var MSDFTextEntity = (_class3 = class extends Entity {
1016
1232
  }
1017
1233
  }
1018
1234
  destroy() {
1019
- _layout.LayoutWorkerManager.getInstance().cancelLayout(this.id);
1235
+ _layout.LayoutWorkerManager.cancelLayoutForEntity(this.id);
1020
1236
  super.destroy();
1021
1237
  }
1022
1238
  }, _class3);
@@ -1053,19 +1269,19 @@ function readSvgAttribute(source, name) {
1053
1269
  return null;
1054
1270
  }
1055
1271
  var SVGEntity = (_class4 = class extends Entity {
1056
- __init31() {this.svgSource = ""}
1057
- __init32() {this.imageBitmap = null}
1058
- __init33() {this.imageElement = null}
1059
- __init34() {this.blobURL = null}
1060
- __init35() {this.currentImg = null}
1061
- __init36() {this.lodTimeout = null}
1062
- __init37() {this.cachedDoc = null}
1063
- __init38() {this.baseWidth = 100}
1064
- __init39() {this.baseHeight = 100}
1065
- __init40() {this.lastRasterizedScale = 1}
1066
- __init41() {this.targetScale = 1}
1272
+ __init45() {this.svgSource = ""}
1273
+ __init46() {this.imageBitmap = null}
1274
+ __init47() {this.imageElement = null}
1275
+ __init48() {this.blobURL = null}
1276
+ __init49() {this.currentImg = null}
1277
+ __init50() {this.lodTimeout = null}
1278
+ __init51() {this.cachedDoc = null}
1279
+ __init52() {this.baseWidth = 100}
1280
+ __init53() {this.baseHeight = 100}
1281
+ __init54() {this.lastRasterizedScale = 1}
1282
+ __init55() {this.targetScale = 1}
1067
1283
  constructor(svgSource, id) {
1068
- super(id);_class4.prototype.__init31.call(this);_class4.prototype.__init32.call(this);_class4.prototype.__init33.call(this);_class4.prototype.__init34.call(this);_class4.prototype.__init35.call(this);_class4.prototype.__init36.call(this);_class4.prototype.__init37.call(this);_class4.prototype.__init38.call(this);_class4.prototype.__init39.call(this);_class4.prototype.__init40.call(this);_class4.prototype.__init41.call(this);;
1284
+ super(id);_class4.prototype.__init45.call(this);_class4.prototype.__init46.call(this);_class4.prototype.__init47.call(this);_class4.prototype.__init48.call(this);_class4.prototype.__init49.call(this);_class4.prototype.__init50.call(this);_class4.prototype.__init51.call(this);_class4.prototype.__init52.call(this);_class4.prototype.__init53.call(this);_class4.prototype.__init54.call(this);_class4.prototype.__init55.call(this);;
1069
1285
  this.setSVGSource(svgSource);
1070
1286
  }
1071
1287
  setSVGSource(svgSource) {