@vectojs/core 1.14.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.
Files changed (52) hide show
  1. package/dist/{chunk-XIEQHSBB.mjs → chunk-AQTO7OSU.mjs} +253 -496
  2. package/dist/{chunk-2Z23LTH3.js → chunk-DUYB4GX4.js} +310 -551
  3. package/dist/{chunk-BEUIB3U7.js → chunk-IYPLG4Z4.js} +93 -43
  4. package/dist/{chunk-L5BCKFQE.mjs → chunk-JFU56BX4.mjs} +48 -0
  5. package/dist/index.d.ts +4 -12
  6. package/dist/index.js +2155 -456
  7. package/dist/index.mjs +1977 -279
  8. package/dist/layout/index.d.ts +1 -3
  9. package/dist/layout.js +3 -15
  10. package/dist/layout.mjs +2 -16
  11. package/dist/renderer/CanvasRenderer.d.ts +22 -0
  12. package/dist/renderer/IRenderer.d.ts +13 -0
  13. package/dist/renderer.js +4 -3
  14. package/dist/renderer.mjs +1 -1
  15. package/dist/text/MSDFTextEntity.d.ts +30 -1
  16. package/dist/text/index.d.ts +1 -5
  17. package/dist/text.js +5 -15
  18. package/dist/text.mjs +6 -17
  19. package/dist/tree/ComputeParticleEntity.d.ts +18 -0
  20. package/dist/tree/DOMPortalEntity.d.ts +18 -0
  21. package/dist/tree/Entity.d.ts +122 -6
  22. package/dist/tree/Scene.d.ts +380 -0
  23. package/dist/wasm/anim-backend.d.ts +86 -0
  24. package/dist/wasm/asset.d.ts +25 -0
  25. package/dist/wasm/asset.js +7 -0
  26. package/dist/wasm/asset.mjs +5 -0
  27. package/dist/wasm/backend.d.ts +154 -0
  28. package/dist/wasm/hit-backend.d.ts +92 -0
  29. package/dist/wasm/hit-store.d.ts +48 -0
  30. package/dist/wasm/particle-backend.d.ts +104 -0
  31. package/dist/wasm/scene-store.d.ts +28 -0
  32. package/dist/wasm/soa.d.ts +146 -0
  33. package/dist/wasm/vectojs_core.wasm +0 -0
  34. package/package.json +17 -8
  35. package/dist/animation/drivers.d.ts +0 -48
  36. package/dist/animation/easing.d.ts +0 -16
  37. package/dist/chunk-4AR425AR.js +0 -1121
  38. package/dist/chunk-BA5HUUDF.js +0 -760
  39. package/dist/chunk-IESDTEJ4.mjs +0 -1121
  40. package/dist/chunk-X7I465AQ.mjs +0 -760
  41. package/dist/layout/LayoutEngine.d.ts +0 -289
  42. package/dist/layout/LayoutWorker.d.ts +0 -23
  43. package/dist/layout/LayoutWorkerManager.d.ts +0 -26
  44. package/dist/layout/LayoutWorkerSource.d.ts +0 -1
  45. package/dist/layout/measure.d.ts +0 -20
  46. package/dist/math/SpatialHashGrid.d.ts +0 -53
  47. package/dist/math/SpringPhysics.d.ts +0 -13
  48. package/dist/text/ArabicShaper.d.ts +0 -10
  49. package/dist/text/BidiResolver.d.ts +0 -5
  50. package/dist/text/MSDFFont.d.ts +0 -129
  51. package/dist/text/PreparedContentGrid.d.ts +0 -60
  52. package/dist/text/Typography.d.ts +0 -11
@@ -1,137 +1,11 @@
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;
2
-
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;
3
2
 
3
+ // src/tree/Entity.ts
4
4
 
5
- var _chunk4AR425ARjs = require('./chunk-4AR425AR.js');
6
5
 
7
- // src/math/SpringPhysics.ts
8
- var MAX_FRAME_DT = 0.25;
9
- var MAX_STEP_DT = 1 / 120;
10
- var SpringPhysics = (_class = class {
11
-
12
-
13
- __init() {this.velocity = 0}
14
- __init2() {this.stiffness = 180}
15
- __init3() {this.damping = 12}
16
- __init4() {this.mass = 1}
17
- __init5() {this.valEpsilon = 5e-3}
18
- __init6() {this.velEpsilon = 5e-3}
19
- constructor(initial) {;_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);
20
- this.value = initial;
21
- this.target = initial;
22
- }
23
- update(dt) {
24
- if (this.isAtRest()) {
25
- this.value = this.target;
26
- this.velocity = 0;
27
- return;
28
- }
29
- if (!(dt > 0)) return;
30
- let remaining = dt < MAX_FRAME_DT ? dt : MAX_FRAME_DT;
31
- while (remaining > 0) {
32
- const step = remaining < MAX_STEP_DT ? remaining : MAX_STEP_DT;
33
- const forceSpring = -this.stiffness * (this.value - this.target);
34
- const forceDamping = -this.damping * this.velocity;
35
- const acceleration = (forceSpring + forceDamping) / this.mass;
36
- this.velocity += acceleration * step;
37
- this.value += this.velocity * step;
38
- remaining -= step;
39
- if (this.isAtRest()) {
40
- this.value = this.target;
41
- this.velocity = 0;
42
- return;
43
- }
44
- }
45
- }
46
- isAtRest() {
47
- return Math.abs(this.value - this.target) < this.valEpsilon && Math.abs(this.velocity) < this.velEpsilon;
48
- }
49
- }, _class);
50
6
 
51
- // src/animation/easing.ts
52
- var c1 = 1.70158;
53
- var c3 = c1 + 1;
54
- var Easing = {
55
- linear: (t) => t,
56
- easeInQuad: (t) => t * t,
57
- easeOutQuad: (t) => t * (2 - t),
58
- easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
59
- easeInCubic: (t) => t * t * t,
60
- easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
61
- easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
62
- easeOutBack: (t) => 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2),
63
- easeInOutBack: (t) => {
64
- const c2 = c1 * 1.525;
65
- return t < 0.5 ? Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2) / 2 : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
66
- }
67
- };
68
7
 
69
- // src/animation/drivers.ts
70
- function isTweenConfig(c) {
71
- return typeof c === "object" && "duration" in c;
72
- }
73
- var TweenDriver = (_class2 = class {
74
-
75
-
76
-
77
- __init7() {this.elapsed = 0}
78
-
79
-
80
-
81
- constructor(from, to, cfg) {;_class2.prototype.__init7.call(this);
82
- this.value = from;
83
- this.from = from;
84
- this.to = to;
85
- this.duration = Math.max(1, cfg.duration);
86
- this.delay = _nullishCoalesce(cfg.delay, () => ( 0));
87
- this.ease = typeof cfg.easing === "function" ? cfg.easing : Easing[_nullishCoalesce(cfg.easing, () => ( "easeOutQuad"))];
88
- }
89
- get target() {
90
- return this.to;
91
- }
92
- retarget(to) {
93
- this.from = this.value;
94
- this.to = to;
95
- this.elapsed = 0;
96
- }
97
- tick(dtMs) {
98
- this.elapsed += dtMs;
99
- const active = this.elapsed - this.delay;
100
- if (active <= 0) return;
101
- const p = Math.min(active / this.duration, 1);
102
- this.value = this.from + (this.to - this.from) * this.ease(p);
103
- }
104
- isDone() {
105
- return this.elapsed - this.delay >= this.duration;
106
- }
107
- }, _class2);
108
- var SpringDriver = class {
109
-
110
- constructor(from, to, cfg) {
111
- this.spring = new SpringPhysics(from);
112
- if (cfg.stiffness !== void 0) this.spring.stiffness = cfg.stiffness;
113
- if (cfg.damping !== void 0) this.spring.damping = cfg.damping;
114
- if (cfg.mass !== void 0) this.spring.mass = cfg.mass;
115
- this.spring.target = to;
116
- }
117
- get value() {
118
- return this.spring.value;
119
- }
120
- get target() {
121
- return this.spring.target;
122
- }
123
- retarget(to) {
124
- this.spring.target = to;
125
- }
126
- tick(dtMs) {
127
- this.spring.update(dtMs / 1e3);
128
- }
129
- isDone() {
130
- return this.spring.isAtRest();
131
- }
132
- };
133
-
134
- // src/tree/Entity.ts
8
+ var _animation = require('@vectojs/animation');
135
9
  var ANIMATABLE_PROPS = /* @__PURE__ */ new Set([
136
10
  "x",
137
11
  "y",
@@ -140,7 +14,7 @@ var ANIMATABLE_PROPS = /* @__PURE__ */ new Set([
140
14
  "rotation",
141
15
  "opacity"
142
16
  ]);
143
- var VectoJSEvent = (_class3 = class {
17
+ var VectoJSEvent = (_class = class {
144
18
  /** The event name. */
145
19
 
146
20
  /** The entity the event originated on. */
@@ -152,9 +26,9 @@ var VectoJSEvent = (_class3 = class {
152
26
  /** Whether the event bubbles past its target (capture always runs). */
153
27
 
154
28
 
155
- __init8() {this.stopped = false}
156
- __init9() {this.stoppedImmediate = false}
157
- constructor(type, target, nativeEvent, bubbles = true, scenePoint) {;_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);
29
+ __init() {this.stopped = false}
30
+ __init2() {this.stoppedImmediate = false}
31
+ constructor(type, target, nativeEvent, bubbles = true, scenePoint) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);
158
32
  this.type = type;
159
33
  this.target = target;
160
34
  this.currentTarget = target;
@@ -252,11 +126,11 @@ var VectoJSEvent = (_class3 = class {
252
126
  get key() {
253
127
  return _optionalChain([this, 'access', _38 => _38.nativeEvent, 'optionalAccess', _39 => _39.key]);
254
128
  }
255
- }, _class3);
256
- var Entity = (_class4 = class {
129
+ }, _class);
130
+ var Entity = (_class2 = class {
257
131
 
258
- __init10() {this.children = []}
259
- __init11() {this.parent = null}
132
+ __init3() {this.children = []}
133
+ __init4() {this.parent = null}
260
134
  /**
261
135
  * Walk up the parent chain to find the scene this entity is currently attached to.
262
136
  */
@@ -264,18 +138,52 @@ var Entity = (_class4 = class {
264
138
  if (this._scene) return this._scene;
265
139
  return this.parent ? this.parent.scene : null;
266
140
  }
267
- __init12() {this._x = 0}
268
- __init13() {this._y = 0}
269
- __init14() {this._scaleX = 1}
270
- __init15() {this._scaleY = 1}
271
- __init16() {this._rotation = 0}
272
- __init17() {this._opacity = 1}
141
+ __init5() {this._x = 0}
142
+ __init6() {this._y = 0}
143
+ __init7() {this._scaleX = 1}
144
+ __init8() {this._scaleY = 1}
145
+ __init9() {this._rotation = 0}
146
+ __init10() {this._opacity = 1}
273
147
  // Fast-path flag: false for the overwhelming majority of entities (incl. the
274
148
  // Danmaku hot loop), so a bare `entity.x = v` is one boolean check + field write.
275
- __init18() {this._hasTransitions = false}
276
- __init19() {this._transitions = null}
277
- __init20() {this._drivers = /* @__PURE__ */ new Map()}
278
- __init21() {this._mounted = false}
149
+ __init11() {this._hasTransitions = false}
150
+ __init12() {this._transitions = null}
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}
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}
279
187
  get x() {
280
188
  return this._x;
281
189
  }
@@ -318,8 +226,8 @@ var Entity = (_class4 = class {
318
226
  if (this._hasTransitions) this._animateProp("opacity", v);
319
227
  else this._opacity = v;
320
228
  }
321
- __init22() {this.isDOMPortal = false}
322
- __init23() {this._interactive = false}
229
+ __init27() {this.isDOMPortal = false}
230
+ __init28() {this._interactive = false}
323
231
  get interactive() {
324
232
  return this._interactive;
325
233
  }
@@ -333,10 +241,10 @@ var Entity = (_class4 = class {
333
241
  }
334
242
  }
335
243
  }
336
- __init24() {this.width = 0}
337
- __init25() {this.height = 0}
338
- __init26() {this.a11yOffsetX = 0}
339
- __init27() {this.a11yOffsetY = 0}
244
+ __init29() {this.width = 0}
245
+ __init30() {this.height = 0}
246
+ __init31() {this.a11yOffsetX = 0}
247
+ __init32() {this.a11yOffsetY = 0}
340
248
  /**
341
249
  * Opt in to a viewport-filling accessibility/automation shadow node even when
342
250
  * this entity has no intrinsic box (`width`/`height` of `0`). Use for
@@ -344,19 +252,21 @@ var Entity = (_class4 = class {
344
252
  * that need global pointer events. The node is mounted behind all other shadow
345
253
  * nodes, so on-top components stay clickable.
346
254
  */
347
- __init28() {this.a11yFullViewport = false}
255
+ __init33() {this.a11yFullViewport = false}
348
256
  /**
349
257
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
350
258
  * rendering. Combined with translating a content child, this is how
351
259
  * scroll/overflow containers (e.g. `ScrollView`) keep their content inside a
352
260
  * fixed viewport. Off by default (children render unclipped). Canvas2D only.
353
261
  */
354
- __init29() {this.clipChildren = false}
355
- __init30() {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}
356
266
  /** Capture-phase listeners (fired root→target before bubble). */
357
- __init31() {this.captureListeners = /* @__PURE__ */ new Map()}
358
- __init32() {this.animations = []}
359
- constructor(id) {;_class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this);_class4.prototype.__init13.call(this);_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);_class4.prototype.__init16.call(this);_class4.prototype.__init17.call(this);_class4.prototype.__init18.call(this);_class4.prototype.__init19.call(this);_class4.prototype.__init20.call(this);_class4.prototype.__init21.call(this);_class4.prototype.__init22.call(this);_class4.prototype.__init23.call(this);_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);_class4.prototype.__init29.call(this);_class4.prototype.__init30.call(this);_class4.prototype.__init31.call(this);_class4.prototype.__init32.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);
360
270
  this.id = id || `entity_${Math.random().toString(36).substring(2, 9)}`;
361
271
  }
362
272
  /**
@@ -390,6 +300,7 @@ var Entity = (_class4 = class {
390
300
  const s = this.scene;
391
301
  if (s) {
392
302
  s.a11yNeedsReorder = true;
303
+ _optionalChain([s, 'access', _40 => _40.markStructureChanged, 'optionalCall', _41 => _41()]);
393
304
  s.markDirty();
394
305
  child._notifyMounted();
395
306
  }
@@ -419,6 +330,7 @@ var Entity = (_class4 = class {
419
330
  if (s) {
420
331
  s.detachA11y(child);
421
332
  s.a11yNeedsReorder = true;
333
+ _optionalChain([s, 'access', _42 => _42.markStructureChanged, 'optionalCall', _43 => _43()]);
422
334
  s.markDirty();
423
335
  }
424
336
  }
@@ -467,13 +379,13 @@ var Entity = (_class4 = class {
467
379
  * @example entity.animate({ x: 400, opacity: 0 }, 500);
468
380
  */
469
381
  animate(targetProps, durationMs) {
470
- this.animations.push({
382
+ (this.animations ??= []).push({
471
383
  target: targetProps,
472
384
  duration: durationMs,
473
385
  startTime: -1,
474
386
  startProps: {}
475
387
  });
476
- _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()]);
477
389
  return this;
478
390
  }
479
391
  /** Write a driver-computed value to a backing field without re-triggering the setter. */
@@ -520,39 +432,40 @@ var Entity = (_class4 = class {
520
432
  * that need to seed a starting state (e.g. the presence helper's enter `from`).
521
433
  */
522
434
  setImmediate(prop, v) {
523
- const existing = this._drivers.get(prop);
435
+ const existing = _optionalChain([this, 'access', _47 => _47._drivers, 'optionalAccess', _48 => _48.get, 'call', _49 => _49(prop)]);
524
436
  if (existing) this._settleDriver(existing);
525
- this._drivers.delete(prop);
437
+ _optionalChain([this, 'access', _50 => _50._drivers, 'optionalAccess', _51 => _51.delete, 'call', _52 => _52(prop)]);
526
438
  this._applyAnimated(prop, v);
527
439
  }
528
440
  _settleDriver(driver) {
529
441
  const active = driver;
530
442
  const onDone = active.onDone;
531
443
  active.onDone = void 0;
532
- _optionalChain([onDone, 'optionalCall', _43 => _43()]);
444
+ _optionalChain([onDone, 'optionalCall', _53 => _53()]);
533
445
  }
534
446
  _spawnDriver(prop, to, cfg) {
535
- if (prop !== "opacity" && _optionalChain([this, 'access', _44 => _44.scene, 'optionalAccess', _45 => _45.prefersReducedMotion])) {
536
- 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)]);
537
449
  if (existing2) this._settleDriver(existing2);
538
- this._drivers.delete(prop);
450
+ _optionalChain([this, 'access', _59 => _59._drivers, 'optionalAccess', _60 => _60.delete, 'call', _61 => _61(prop)]);
539
451
  this._applyAnimated(prop, to);
540
452
  return;
541
453
  }
542
- const existing = this._drivers.get(prop);
454
+ const existing = _optionalChain([this, 'access', _62 => _62._drivers, 'optionalAccess', _63 => _63.get, 'call', _64 => _64(prop)]);
543
455
  if (existing) {
544
456
  this._settleDriver(existing);
545
457
  existing.retarget(to);
546
458
  return;
547
459
  }
548
460
  const from = this._currentOf(prop);
549
- const driver = isTweenConfig(cfg) ? new TweenDriver(from, to, cfg) : new SpringDriver(from, to, cfg === "spring" ? {} : cfg);
550
- this._drivers.set(prop, driver);
551
- _optionalChain([this, 'access', _46 => _46.scene, 'optionalAccess', _47 => _47.markDirty, 'call', _48 => _48()]);
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);
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)]);
552
465
  }
553
466
  /** Assignment path when a declarative transition is configured for `prop`. */
554
467
  _animateProp(prop, to) {
555
- 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)]);
556
469
  if (!cfg) {
557
470
  this._applyAnimated(prop, to);
558
471
  return;
@@ -581,7 +494,7 @@ var Entity = (_class4 = class {
581
494
  entries.map(
582
495
  (e) => new Promise((resolve) => {
583
496
  this._spawnDriver(e[0], e[1], cfg);
584
- 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])]);
585
498
  if (!d)
586
499
  resolve();
587
500
  else d.onDone = resolve;
@@ -591,7 +504,8 @@ var Entity = (_class4 = class {
591
504
  }
592
505
  /** Advance active property drivers one frame. Call from update(). */
593
506
  tickDrivers(dt) {
594
- 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;
595
509
  for (const [prop, driver] of this._drivers) {
596
510
  driver.tick(dt);
597
511
  if (driver.isDone()) {
@@ -602,7 +516,39 @@ var Entity = (_class4 = class {
602
516
  this._applyAnimated(prop, driver.value);
603
517
  }
604
518
  }
605
- _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;
606
552
  }
607
553
  /**
608
554
  * Advance the entity's internal state for one frame.
@@ -615,7 +561,7 @@ var Entity = (_class4 = class {
615
561
  */
616
562
  update(dt, time) {
617
563
  this.tickDrivers(dt);
618
- if (this.animations.length > 0) {
564
+ if (this.animations && this.animations.length > 0) {
619
565
  const anim = this.animations[0];
620
566
  if (anim.startTime === -1) {
621
567
  anim.startTime = time;
@@ -656,7 +602,7 @@ var Entity = (_class4 = class {
656
602
  * @example entity.on('click', (e) => console.log('clicked', e));
657
603
  */
658
604
  on(event, callback, options) {
659
- 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();
660
606
  if (!map.has(event)) {
661
607
  map.set(event, []);
662
608
  }
@@ -672,7 +618,7 @@ var Entity = (_class4 = class {
672
618
  * @returns `this` for method chaining.
673
619
  */
674
620
  off(event, callback, options) {
675
- 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)]);
676
622
  if (handlers) {
677
623
  const idx = handlers.indexOf(callback);
678
624
  if (idx !== -1) handlers.splice(idx, 1);
@@ -680,17 +626,37 @@ var Entity = (_class4 = class {
680
626
  return this;
681
627
  }
682
628
  /**
683
- * Tear down this entity: clear all animations, event listeners, and detach
684
- * 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.
685
644
  */
686
645
  destroy() {
687
- this.animations = [];
688
- for (const driver of this._drivers.values()) {
689
- this._settleDriver(driver);
646
+ if (this._destroyed) return;
647
+ this._destroyed = true;
648
+ while (this.children.length > 0) {
649
+ this.children.at(-1).destroy();
690
650
  }
691
- this._drivers.clear();
692
- this.listeners.clear();
693
- this.captureListeners.clear();
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();
657
+ }
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()]);
694
660
  if (this.parent) {
695
661
  this.parent.remove(this);
696
662
  }
@@ -705,7 +671,7 @@ var Entity = (_class4 = class {
705
671
  * @param payload - Arbitrary data forwarded to each listener.
706
672
  */
707
673
  emit(event, payload) {
708
- const handlers = this.listeners.get(event);
674
+ const handlers = _optionalChain([this, 'access', _93 => _93.listeners, 'optionalAccess', _94 => _94.get, 'call', _95 => _95(event)]);
709
675
  if (handlers) {
710
676
  handlers.forEach((h) => h(payload));
711
677
  }
@@ -719,19 +685,19 @@ var Entity = (_class4 = class {
719
685
  * with `requestAnimationFrame(() => document.getElementById(id)?.focus())`.
720
686
  */
721
687
  focus() {
722
- 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)]);
723
689
  if (el) {
724
690
  el.focus();
725
691
  return;
726
692
  }
727
693
  requestAnimationFrame(() => {
728
- 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)]);
729
695
  if (retry) retry.focus();
730
696
  });
731
697
  }
732
698
  /** Run one node's listeners for the event, honoring stopImmediatePropagation. */
733
699
  fireListeners(node, map, event) {
734
- const handlers = map.get(event.type);
700
+ const handlers = _optionalChain([map, 'optionalAccess', _102 => _102.get, 'call', _103 => _103(event.type)]);
735
701
  if (!handlers) return;
736
702
  event.currentTarget = node;
737
703
  for (const h of handlers.slice()) {
@@ -770,10 +736,45 @@ var Entity = (_class4 = class {
770
736
  getGlobalPosition() {
771
737
  return this.localToWorld(0, 0);
772
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
+ }
773
761
  /**
774
762
  * Return the exact accumulated Canvas `T * S * R` transform for this entity.
775
763
  */
776
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
+ }
777
778
  const path = [this];
778
779
  let ancestor = this.parent;
779
780
  while (ancestor) {
@@ -788,8 +789,9 @@ var Entity = (_class4 = class {
788
789
  let f = 0;
789
790
  for (let i = path.length - 1; i >= 0; i--) {
790
791
  const node = path[i];
791
- const cos = Math.cos(node.rotation);
792
- const sin = Math.sin(node.rotation);
792
+ const trig = node._getTrig();
793
+ const cos = trig.cos;
794
+ const sin = trig.sin;
793
795
  const la = node.scaleX * cos;
794
796
  const lb = node.scaleY * sin;
795
797
  const lc = -node.scaleX * sin;
@@ -811,6 +813,37 @@ var Entity = (_class4 = class {
811
813
  }
812
814
  return { a, b, c, d, e, f };
813
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
+ }
814
847
  /** Convert a point from this entity's local space to Scene/world space. */
815
848
  localToWorld(localX, localY) {
816
849
  const { a, b, c, d, e, f } = this.getWorldTransform();
@@ -840,7 +873,12 @@ var Entity = (_class4 = class {
840
873
  * does not provide a render-specific box.
841
874
  */
842
875
  getWorldBounds() {
843
- 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
+ }));
844
882
  const { a, b, c, d, e, f } = this.getWorldTransform();
845
883
  let minX = Infinity;
846
884
  let minY = Infinity;
@@ -981,134 +1019,13 @@ var Entity = (_class4 = class {
981
1019
  * @returns `true` if at least one animation or property driver remains.
982
1020
  */
983
1021
  hasPendingAnimations() {
984
- return this.animations.length > 0 || this._drivers.size > 0;
985
- }
986
- }, _class4);
987
-
988
- // src/text/Typography.ts
989
- var typographyContext;
990
- var baselineCache = /* @__PURE__ */ new Map();
991
- function cssLineBoxBaseline(font, lineHeight) {
992
- if (typeof document === "undefined") return lineHeight * 0.8;
993
- const key = `${font}\0${lineHeight}`;
994
- const cached = baselineCache.get(key);
995
- if (cached !== void 0) return cached;
996
- if (typographyContext === void 0) {
997
- typographyContext = document.createElement("canvas").getContext("2d");
998
- }
999
- if (!typographyContext) return lineHeight * 0.8;
1000
- typographyContext.font = font;
1001
- const metrics = typographyContext.measureText("Mg");
1002
- const ascent = metrics.fontBoundingBoxAscent || metrics.actualBoundingBoxAscent;
1003
- const descent = metrics.fontBoundingBoxDescent || metrics.actualBoundingBoxDescent;
1004
- if (!(ascent > 0) || !(descent >= 0)) return lineHeight * 0.8;
1005
- const baseline = (lineHeight - ascent - descent) / 2 + ascent;
1006
- baselineCache.set(key, baseline);
1007
- return baseline;
1008
- }
1009
- function clearCssLineBoxMetrics() {
1010
- baselineCache.clear();
1011
- }
1012
-
1013
- // src/text/MSDFFont.ts
1014
- function kernKey(a, b) {
1015
- return a * 1114112 + b;
1016
- }
1017
- var MSDFFont = (_class5 = class _MSDFFont {
1018
- static __initStatic() {this.idCounter = 0}
1019
-
1020
-
1021
- __init33() {this.byCode = /* @__PURE__ */ new Map()}
1022
- __init34() {this.kern = /* @__PURE__ */ new Map()}
1023
- constructor(data) {;_class5.prototype.__init33.call(this);_class5.prototype.__init34.call(this);
1024
- this.id = `font-${_MSDFFont.idCounter++}`;
1025
- this.data = data;
1026
- for (const g of data.glyphs) this.byCode.set(g.unicode, g);
1027
- for (const k of _nullishCoalesce(data.kerning, () => ( []))) this.kern.set(kernKey(k.unicode1, k.unicode2), k.advance);
1028
- }
1029
- /** Parse the `msdf-atlas-gen` JSON (string or already-parsed object). */
1030
- static parse(json) {
1031
- return new _MSDFFont(typeof json === "string" ? JSON.parse(json) : json);
1032
- }
1033
- /** Get a glyph's definition by its unicode value in O(1) time. */
1034
- getGlyph(unicode) {
1035
- return this.byCode.get(unicode);
1036
- }
1037
- /** Distance field range in atlas pixels (for the shader's `u_distanceRange`). */
1038
- get distanceRange() {
1039
- return this.data.atlas.distanceRange;
1040
- }
1041
- get atlasWidth() {
1042
- return this.data.atlas.width;
1043
- }
1044
- get atlasHeight() {
1045
- return this.data.atlas.height;
1046
- }
1047
- /**
1048
- * Lay `text` out at `fontSizePx`. Returns positioned quads (skipping glyphs the
1049
- * font doesn't contain), the widest line's advance, and the total block height.
1050
- * Honors `\n`, kerning pairs, and `letterSpacing`.
1051
- */
1052
- layout(text, fontSizePx, opts = {}) {
1053
- const { x = 0, y = 0, letterSpacing = 0 } = opts;
1054
- const { width: aw, height: ah, yOrigin } = this.data.atlas;
1055
- const { lineHeight, ascender } = this.data.metrics;
1056
- const glyphs = [];
1057
- let penX = x;
1058
- let line = 0;
1059
- let maxAdvance = 0;
1060
- let prevCode = -1;
1061
- const chars = Array.from(text);
1062
- for (const char of chars) {
1063
- if (char === "\n") {
1064
- maxAdvance = Math.max(maxAdvance, penX - x);
1065
- penX = x;
1066
- line++;
1067
- prevCode = -1;
1068
- continue;
1069
- }
1070
- const code = char.codePointAt(0);
1071
- const def = this.byCode.get(code);
1072
- if (!def) {
1073
- prevCode = -1;
1074
- continue;
1075
- }
1076
- if (prevCode >= 0) {
1077
- const k = this.kern.get(kernKey(prevCode, code));
1078
- if (k) penX += k * fontSizePx;
1079
- }
1080
- const baseline = y + (ascender + line * lineHeight) * fontSizePx;
1081
- const pb = def.planeBounds;
1082
- const ab = def.atlasBounds;
1083
- if (pb && ab) {
1084
- const v0 = yOrigin === "bottom" ? 1 - ab.top / ah : ab.top / ah;
1085
- const v1 = yOrigin === "bottom" ? 1 - ab.bottom / ah : ab.bottom / ah;
1086
- glyphs.push({
1087
- char,
1088
- x: penX + pb.left * fontSizePx,
1089
- y: baseline - pb.top * fontSizePx,
1090
- w: (pb.right - pb.left) * fontSizePx,
1091
- h: (pb.top - pb.bottom) * fontSizePx,
1092
- u0: ab.left / aw,
1093
- v0,
1094
- u1: ab.right / aw,
1095
- v1
1096
- });
1097
- }
1098
- penX += def.advance * fontSizePx + letterSpacing;
1099
- prevCode = code;
1100
- }
1101
- maxAdvance = Math.max(maxAdvance, penX - x);
1102
- return {
1103
- glyphs,
1104
- width: maxAdvance,
1105
- height: (line + 1) * lineHeight * fontSizePx
1106
- };
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;
1107
1023
  }
1108
- }, _class5.__initStatic(), _class5);
1024
+ }, _class2);
1109
1025
 
1110
1026
  // src/text/MSDFTextEntity.ts
1111
- var MSDFTextEntity = (_class6 = class extends Entity {
1027
+ var _layout = require('@vectojs/layout');
1028
+ var MSDFTextEntity = (_class3 = class extends Entity {
1112
1029
 
1113
1030
 
1114
1031
 
@@ -1118,13 +1035,21 @@ var MSDFTextEntity = (_class6 = class extends Entity {
1118
1035
 
1119
1036
 
1120
1037
 
1121
- __init35() {this.text = ""}
1122
- __init36() {this.lastRenderedSeqId = 0}
1123
- __init37() {this.rgbColorCache = /* @__PURE__ */ new Map()}
1124
- __init38() {this.fontStringCache = []}
1125
- __init39() {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}
1126
1051
  constructor(text, options) {
1127
- super();_class6.prototype.__init35.call(this);_class6.prototype.__init36.call(this);_class6.prototype.__init37.call(this);_class6.prototype.__init38.call(this);_class6.prototype.__init39.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);;
1128
1053
  this.font = options.font;
1129
1054
  this.texture = options.texture;
1130
1055
  this.fallbackFont = _nullishCoalesce(options.fallbackFont, () => ( "sans-serif"));
@@ -1134,6 +1059,7 @@ var MSDFTextEntity = (_class6 = class extends Entity {
1134
1059
  this.lineHeight = options.lineHeight;
1135
1060
  this.maxWidth = _nullishCoalesce(options.maxWidth, () => ( 1e3));
1136
1061
  this.maxHeight = _nullishCoalesce(options.maxHeight, () => ( 1e3));
1062
+ this.textAlign = _nullishCoalesce(options.textAlign, () => ( "left"));
1137
1063
  this.setText(text);
1138
1064
  }
1139
1065
  /** Change the wrap boundary and re-run layout for the current text. */
@@ -1142,13 +1068,53 @@ var MSDFTextEntity = (_class6 = class extends Entity {
1142
1068
  this.maxWidth = maxWidth;
1143
1069
  this.queueLayout();
1144
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
+ }
1145
1092
  setText(text) {
1146
1093
  if (this.text === text && this.layoutResult) return;
1147
1094
  this.text = text;
1095
+ this.rebuildLayoutText();
1148
1096
  this.queueLayout();
1149
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
+ }
1150
1116
  queueLayout() {
1151
- _chunk4AR425ARjs.LayoutWorkerManager.getInstance().queueLayout(this.id, this.text, {
1117
+ _layout.LayoutWorkerManager.getInstance().queueLayout(this.id, this.layoutText, {
1152
1118
  fontId: this.font.id,
1153
1119
  fontSize: this.fontSize,
1154
1120
  maxWidth: this.maxWidth,
@@ -1156,11 +1122,12 @@ var MSDFTextEntity = (_class6 = class extends Entity {
1156
1122
  fontData: this.font.data,
1157
1123
  letterSpacing: this.letterSpacing,
1158
1124
  lineHeight: this.lineHeight,
1125
+ textAlign: this.textAlign,
1159
1126
  callback: (res) => {
1160
1127
  if (res.seqId < this.lastRenderedSeqId) return;
1161
1128
  this.lastRenderedSeqId = res.seqId;
1162
1129
  this.layoutResult = res;
1163
- _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()]);
1164
1131
  }
1165
1132
  });
1166
1133
  }
@@ -1265,10 +1232,10 @@ var MSDFTextEntity = (_class6 = class extends Entity {
1265
1232
  }
1266
1233
  }
1267
1234
  destroy() {
1268
- _chunk4AR425ARjs.LayoutWorkerManager.getInstance().cancelLayout(this.id);
1235
+ _layout.LayoutWorkerManager.cancelLayoutForEntity(this.id);
1269
1236
  super.destroy();
1270
1237
  }
1271
- }, _class6);
1238
+ }, _class3);
1272
1239
 
1273
1240
  // src/text/SVGEntity.ts
1274
1241
  function isSvgWhitespace(ch) {
@@ -1301,20 +1268,20 @@ function readSvgAttribute(source, name) {
1301
1268
  }
1302
1269
  return null;
1303
1270
  }
1304
- var SVGEntity = (_class7 = class extends Entity {
1305
- __init40() {this.svgSource = ""}
1306
- __init41() {this.imageBitmap = null}
1307
- __init42() {this.imageElement = null}
1308
- __init43() {this.blobURL = null}
1309
- __init44() {this.currentImg = null}
1310
- __init45() {this.lodTimeout = null}
1311
- __init46() {this.cachedDoc = null}
1312
- __init47() {this.baseWidth = 100}
1313
- __init48() {this.baseHeight = 100}
1314
- __init49() {this.lastRasterizedScale = 1}
1315
- __init50() {this.targetScale = 1}
1271
+ var SVGEntity = (_class4 = class extends Entity {
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}
1316
1283
  constructor(svgSource, id) {
1317
- super(id);_class7.prototype.__init40.call(this);_class7.prototype.__init41.call(this);_class7.prototype.__init42.call(this);_class7.prototype.__init43.call(this);_class7.prototype.__init44.call(this);_class7.prototype.__init45.call(this);_class7.prototype.__init46.call(this);_class7.prototype.__init47.call(this);_class7.prototype.__init48.call(this);_class7.prototype.__init49.call(this);_class7.prototype.__init50.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);;
1318
1285
  this.setSVGSource(svgSource);
1319
1286
  }
1320
1287
  setSVGSource(svgSource) {
@@ -1503,219 +1470,11 @@ var SVGEntity = (_class7 = class extends Entity {
1503
1470
  this.cachedDoc = null;
1504
1471
  super.destroy();
1505
1472
  }
1506
- }, _class7);
1507
-
1508
- // src/text/PreparedContentGrid.ts
1509
- var nextRevision = 1;
1510
- var graphemeSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
1511
- var MARK = /\p{Mark}/u;
1512
- var EXTENDED_PICTOGRAPHIC = /\p{Extended_Pictographic}/u;
1513
- var REGIONAL_INDICATOR = /\p{Regional_Indicator}/u;
1514
- var BIDI_CONTROL = /\p{Bidi_Control}/u;
1515
- var EAST_ASIAN_WIDE = /[ᄀ-ᅟ⌚-⌛⏩-⏬⏰⏳◽-◾⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/u;
1516
- function codePointAt(text, index) {
1517
- const point = text.codePointAt(index);
1518
- if (point === void 0) return { value: "", next: index };
1519
- const value = String.fromCodePoint(point);
1520
- return { value, next: index + value.length };
1521
- }
1522
- function fallbackGraphemes(text) {
1523
- const parts = [];
1524
- let index = 0;
1525
- while (index < text.length) {
1526
- const start = index;
1527
- let current = codePointAt(text, index);
1528
- let segment = current.value;
1529
- index = current.next;
1530
- let regionalCount = REGIONAL_INDICATOR.test(segment) ? 1 : 0;
1531
- while (index < text.length) {
1532
- current = codePointAt(text, index);
1533
- const point = _nullishCoalesce(current.value.codePointAt(0), () => ( 0));
1534
- const isVariation = point >= 65024 && point <= 65039;
1535
- const isEmojiModifier = point >= 127995 && point <= 127999;
1536
- const isKeycap = point === 8419;
1537
- if (MARK.test(current.value) || isVariation || isEmojiModifier || isKeycap) {
1538
- segment += current.value;
1539
- index = current.next;
1540
- continue;
1541
- }
1542
- if (REGIONAL_INDICATOR.test(current.value) && regionalCount === 1) {
1543
- segment += current.value;
1544
- index = current.next;
1545
- regionalCount++;
1546
- continue;
1547
- }
1548
- if (point === 8205) {
1549
- segment += current.value;
1550
- index = current.next;
1551
- if (index < text.length) {
1552
- current = codePointAt(text, index);
1553
- segment += current.value;
1554
- index = current.next;
1555
- }
1556
- continue;
1557
- }
1558
- break;
1559
- }
1560
- parts.push({ segment, index: start });
1561
- }
1562
- return parts;
1563
- }
1564
- function graphemes(text) {
1565
- if (!graphemeSegmenter) return fallbackGraphemes(text);
1566
- return Array.from(graphemeSegmenter.segment(text), (part) => ({
1567
- segment: part.segment,
1568
- index: part.index
1569
- }));
1570
- }
1571
- function lowerBound(values, target) {
1572
- let low = 0;
1573
- let high = values.length;
1574
- while (low < high) {
1575
- const middle = low + high >>> 1;
1576
- if (values[middle] < target) low = middle + 1;
1577
- else high = middle;
1578
- }
1579
- return low;
1580
- }
1581
- function isWideCluster(cluster) {
1582
- if (EXTENDED_PICTOGRAPHIC.test(cluster) || REGIONAL_INDICATOR.test(cluster)) return true;
1583
- if (cluster.includes("\u20E3")) return true;
1584
- if (EAST_ASIAN_WIDE.test(cluster)) return true;
1585
- const point = _nullishCoalesce(cluster.codePointAt(0), () => ( 0));
1586
- return point >= 131072 && point <= 262141;
1587
- }
1588
- function sourceLines(source) {
1589
- const lines = [];
1590
- let start = 0;
1591
- while (true) {
1592
- let end = start;
1593
- while (end < source.length && source[end] !== "\r" && source[end] !== "\n") end++;
1594
- if (end === source.length) {
1595
- lines.push({
1596
- sourceStart: start,
1597
- sourceEnd: end,
1598
- nextSourceStart: end,
1599
- text: source.slice(start)
1600
- });
1601
- break;
1602
- }
1603
- const next = source[end] === "\r" && source[end + 1] === "\n" ? end + 2 : end + 1;
1604
- lines.push({
1605
- sourceStart: start,
1606
- sourceEnd: end,
1607
- nextSourceStart: next,
1608
- text: source.slice(start, end)
1609
- });
1610
- start = next;
1611
- if (start === source.length) {
1612
- lines.push({ sourceStart: start, sourceEnd: start, nextSourceStart: start, text: "" });
1613
- break;
1614
- }
1615
- }
1616
- return lines;
1617
- }
1618
- function assertPositiveFinite(value, name) {
1619
- if (!Number.isFinite(value) || value <= 0) {
1620
- throw new RangeError(`${name} must be a positive finite number`);
1621
- }
1622
- }
1623
- function prepareContentGrid(source, options) {
1624
- assertPositiveFinite(options.cellWidth, "cellWidth");
1625
- assertPositiveFinite(options.lineHeight, "lineHeight");
1626
- if (!Number.isFinite(options.baseline)) throw new RangeError("baseline must be finite");
1627
- const tabSize = _nullishCoalesce(options.tabSize, () => ( 4));
1628
- if (!Number.isInteger(tabSize) || tabSize <= 0) {
1629
- throw new RangeError("tabSize must be a positive integer");
1630
- }
1631
- const rawLines = sourceLines(source);
1632
- const lines = [];
1633
- for (let lineIndex = 0; lineIndex < rawLines.length; lineIndex++) {
1634
- const sourceLine = rawLines[lineIndex];
1635
- const rawLine = sourceLine.text;
1636
- const { sourceStart: lineStart, sourceEnd, nextSourceStart } = sourceLine;
1637
- const rawCaretBoundaries = [
1638
- 0,
1639
- ...graphemes(rawLine).map((grapheme) => grapheme.index + grapheme.segment.length)
1640
- ];
1641
- const shaped = _chunk4AR425ARjs.ArabicShaper.shapeArabic(rawLine);
1642
- const shapedParts = graphemes(shaped.shapedText);
1643
- const levels = _chunk4AR425ARjs.BidiResolver.resolveLevels(shaped.shapedText);
1644
- const cells = [];
1645
- let column = 0;
1646
- for (let index = 0; index < shapedParts.length; index++) {
1647
- const part = shapedParts[index];
1648
- const sourceOffset = _nullishCoalesce(shaped.indexMap[part.index], () => ( part.index));
1649
- const nextPart = shapedParts[index + 1];
1650
- const sourceOffsetEnd = nextPart ? _nullishCoalesce(shaped.indexMap[nextPart.index], () => ( nextPart.index)) : rawLine.length;
1651
- const raw = rawLine.slice(sourceOffset, sourceOffsetEnd);
1652
- const sourceCaretOffsets = [0];
1653
- for (let caretIndex = lowerBound(rawCaretBoundaries, sourceOffset + 1); caretIndex < rawCaretBoundaries.length && rawCaretBoundaries[caretIndex] < sourceOffsetEnd; caretIndex++) {
1654
- sourceCaretOffsets.push(rawCaretBoundaries[caretIndex] - sourceOffset);
1655
- }
1656
- if (sourceCaretOffsets.at(-1) !== sourceOffsetEnd - sourceOffset) {
1657
- sourceCaretOffsets.push(sourceOffsetEnd - sourceOffset);
1658
- }
1659
- let columns;
1660
- if (BIDI_CONTROL.test(raw)) columns = 0;
1661
- else if (raw === " ") columns = tabSize - column % tabSize;
1662
- else columns = isWideCluster(raw) ? 2 : 1;
1663
- const advance = columns * options.cellWidth;
1664
- cells.push({
1665
- sourceStart: lineStart + sourceOffset,
1666
- sourceEnd: lineStart + sourceOffsetEnd,
1667
- sourceCaretOffsets: Object.freeze(sourceCaretOffsets),
1668
- glyph: part.segment,
1669
- x: 0,
1670
- advance,
1671
- level: _nullishCoalesce(levels[part.index], () => ( 0)),
1672
- char: part.segment
1673
- });
1674
- column += columns;
1675
- }
1676
- const visualCells = [...cells];
1677
- _chunk4AR425ARjs.BidiResolver.reorderVisual(visualCells, _chunk4AR425ARjs.BidiResolver.getBaseLevel(shaped.shapedText));
1678
- let visualX = 0;
1679
- for (const cell of visualCells) {
1680
- cell.x = visualX;
1681
- visualX += cell.advance;
1682
- }
1683
- const frozenCells = cells.map(({ char: _char, ...cell }) => Object.freeze(cell));
1684
- lines.push(
1685
- Object.freeze({
1686
- sourceStart: lineStart,
1687
- sourceEnd,
1688
- nextSourceStart,
1689
- width: visualX,
1690
- cells: Object.freeze(frozenCells)
1691
- })
1692
- );
1693
- }
1694
- return Object.freeze({
1695
- kind: "content-grid",
1696
- revision: nextRevision++,
1697
- source,
1698
- font: options.font,
1699
- cellWidth: options.cellWidth,
1700
- lineHeight: options.lineHeight,
1701
- baseline: options.baseline,
1702
- tabSize,
1703
- lines: Object.freeze(lines)
1704
- });
1705
- }
1706
-
1707
-
1708
-
1709
-
1710
-
1711
-
1712
-
1713
-
1714
-
1473
+ }, _class4);
1715
1474
 
1716
1475
 
1717
1476
 
1718
1477
 
1719
1478
 
1720
1479
 
1721
- exports.SpringPhysics = SpringPhysics; exports.Easing = Easing; exports.isTweenConfig = isTweenConfig; exports.TweenDriver = TweenDriver; exports.SpringDriver = SpringDriver; exports.VectoJSEvent = VectoJSEvent; exports.Entity = Entity; exports.cssLineBoxBaseline = cssLineBoxBaseline; exports.clearCssLineBoxMetrics = clearCssLineBoxMetrics; exports.MSDFFont = MSDFFont; exports.MSDFTextEntity = MSDFTextEntity; exports.SVGEntity = SVGEntity; exports.prepareContentGrid = prepareContentGrid;
1480
+ exports.VectoJSEvent = VectoJSEvent; exports.Entity = Entity; exports.MSDFTextEntity = MSDFTextEntity; exports.SVGEntity = SVGEntity;