@vectojs/core 1.15.0 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-64UFEHOJ.mjs → chunk-AQTO7OSU.mjs} +246 -32
- package/dist/{chunk-J6NHSFIE.js → chunk-DUYB4GX4.js} +285 -69
- package/dist/{chunk-BEUIB3U7.js → chunk-IYPLG4Z4.js} +93 -43
- package/dist/{chunk-L5BCKFQE.mjs → chunk-JFU56BX4.mjs} +48 -0
- package/dist/index.js +2144 -325
- package/dist/index.mjs +1966 -148
- package/dist/layout.js +3 -1
- package/dist/renderer/CanvasRenderer.d.ts +22 -0
- package/dist/renderer/IRenderer.d.ts +13 -0
- package/dist/renderer.js +4 -3
- package/dist/renderer.mjs +1 -1
- package/dist/text/MSDFTextEntity.d.ts +29 -0
- package/dist/text.js +4 -3
- package/dist/text.mjs +1 -1
- package/dist/tree/ComputeParticleEntity.d.ts +18 -0
- package/dist/tree/DOMPortalEntity.d.ts +18 -0
- package/dist/tree/Entity.d.ts +121 -5
- package/dist/tree/Scene.d.ts +380 -0
- package/dist/wasm/anim-backend.d.ts +86 -0
- package/dist/wasm/asset.d.ts +25 -0
- package/dist/wasm/asset.js +7 -0
- package/dist/wasm/asset.mjs +5 -0
- package/dist/wasm/backend.d.ts +154 -0
- package/dist/wasm/hit-backend.d.ts +92 -0
- package/dist/wasm/hit-store.d.ts +48 -0
- package/dist/wasm/particle-backend.d.ts +104 -0
- package/dist/wasm/scene-store.d.ts +28 -0
- package/dist/wasm/soa.d.ts +146 -0
- package/dist/wasm/vectojs_core.wasm +0 -0
- package/package.json +15 -8
|
@@ -146,8 +146,42 @@ var Entity = class {
|
|
|
146
146
|
// Danmaku hot loop), so a bare `entity.x = v` is one boolean check + field write.
|
|
147
147
|
_hasTransitions = false;
|
|
148
148
|
_transitions = null;
|
|
149
|
-
|
|
149
|
+
// Lazily allocated: null until first use. A scene of many passive entities
|
|
150
|
+
// (particles, data points) never touches these, so paying an empty-Map/array
|
|
151
|
+
// allocation per entity in the constructor is pure waste at scale.
|
|
152
|
+
_drivers = null;
|
|
150
153
|
_mounted = false;
|
|
154
|
+
_destroyed = false;
|
|
155
|
+
// Frame this entity's active drivers were last advanced by Scene's batched
|
|
156
|
+
// WASM animation pass (see Scene._tickBatchedDrivers), or -1 if never. When
|
|
157
|
+
// it equals the current frame, tickDrivers() must skip its own tick loop —
|
|
158
|
+
// otherwise a driver already advanced by the batch pass would be ticked a
|
|
159
|
+
// second time by the normal per-entity update() walk in the same frame.
|
|
160
|
+
// Irrelevant (and harmless) for entities never touched by WASM batching.
|
|
161
|
+
_driversTickedFrame = -1;
|
|
162
|
+
// Cached cos/sin, recomputed only when rotation actually changes. renderNode
|
|
163
|
+
// and getWorldTransform() both read this instead of calling Math.cos/sin per
|
|
164
|
+
// entity per frame (V8's are ~2.5x slower than other engines).
|
|
165
|
+
_trig = { cos: 1, sin: 0 };
|
|
166
|
+
_trigRotation = Number.NaN;
|
|
167
|
+
// NaN !== any rotation -> first read computes
|
|
168
|
+
// Per-frame world-matrix cache. Written by Scene during the render walk;
|
|
169
|
+
// getWorldTransform() returns it only while `_worldFrame === scene.currentFrame`
|
|
170
|
+
// and otherwise falls back to the full ancestor walk, so it can never return a
|
|
171
|
+
// stale/wrong transform — only sometimes miss the fast path.
|
|
172
|
+
_wa = 1;
|
|
173
|
+
_wb = 0;
|
|
174
|
+
_wc = 0;
|
|
175
|
+
_wd = 1;
|
|
176
|
+
_we = 0;
|
|
177
|
+
_wf = 0;
|
|
178
|
+
_worldFrame = -1;
|
|
179
|
+
// Slot in the Scene's resident WASM transform store, or -1 when this entity is
|
|
180
|
+
// not in that store (JS transform path, overlay/detached, or before the first
|
|
181
|
+
// structural rebuild). Assigned by Scene on a structural rebuild; the Scene
|
|
182
|
+
// validates it against its slot table before trusting it, so a stale value can
|
|
183
|
+
// only cost a JS-path fallback, never a wrong read.
|
|
184
|
+
_storeSlot = -1;
|
|
151
185
|
get x() {
|
|
152
186
|
return this._x;
|
|
153
187
|
}
|
|
@@ -224,10 +258,12 @@ var Entity = class {
|
|
|
224
258
|
* fixed viewport. Off by default (children render unclipped). Canvas2D only.
|
|
225
259
|
*/
|
|
226
260
|
clipChildren = false;
|
|
227
|
-
|
|
261
|
+
// Lazily allocated (see _drivers above). Most entities never register a
|
|
262
|
+
// listener or an imperative animate() tween.
|
|
263
|
+
listeners = null;
|
|
228
264
|
/** Capture-phase listeners (fired root→target before bubble). */
|
|
229
|
-
captureListeners =
|
|
230
|
-
animations =
|
|
265
|
+
captureListeners = null;
|
|
266
|
+
animations = null;
|
|
231
267
|
constructor(id) {
|
|
232
268
|
this.id = id || `entity_${Math.random().toString(36).substring(2, 9)}`;
|
|
233
269
|
}
|
|
@@ -262,6 +298,7 @@ var Entity = class {
|
|
|
262
298
|
const s = this.scene;
|
|
263
299
|
if (s) {
|
|
264
300
|
s.a11yNeedsReorder = true;
|
|
301
|
+
s.markStructureChanged?.();
|
|
265
302
|
s.markDirty();
|
|
266
303
|
child._notifyMounted();
|
|
267
304
|
}
|
|
@@ -291,6 +328,7 @@ var Entity = class {
|
|
|
291
328
|
if (s) {
|
|
292
329
|
s.detachA11y(child);
|
|
293
330
|
s.a11yNeedsReorder = true;
|
|
331
|
+
s.markStructureChanged?.();
|
|
294
332
|
s.markDirty();
|
|
295
333
|
}
|
|
296
334
|
}
|
|
@@ -339,7 +377,7 @@ var Entity = class {
|
|
|
339
377
|
* @example entity.animate({ x: 400, opacity: 0 }, 500);
|
|
340
378
|
*/
|
|
341
379
|
animate(targetProps, durationMs) {
|
|
342
|
-
this.animations.push({
|
|
380
|
+
(this.animations ??= []).push({
|
|
343
381
|
target: targetProps,
|
|
344
382
|
duration: durationMs,
|
|
345
383
|
startTime: -1,
|
|
@@ -392,9 +430,9 @@ var Entity = class {
|
|
|
392
430
|
* that need to seed a starting state (e.g. the presence helper's enter `from`).
|
|
393
431
|
*/
|
|
394
432
|
setImmediate(prop, v) {
|
|
395
|
-
const existing = this._drivers
|
|
433
|
+
const existing = this._drivers?.get(prop);
|
|
396
434
|
if (existing) this._settleDriver(existing);
|
|
397
|
-
this._drivers
|
|
435
|
+
this._drivers?.delete(prop);
|
|
398
436
|
this._applyAnimated(prop, v);
|
|
399
437
|
}
|
|
400
438
|
_settleDriver(driver) {
|
|
@@ -405,13 +443,13 @@ var Entity = class {
|
|
|
405
443
|
}
|
|
406
444
|
_spawnDriver(prop, to, cfg) {
|
|
407
445
|
if (prop !== "opacity" && this.scene?.prefersReducedMotion) {
|
|
408
|
-
const existing2 = this._drivers
|
|
446
|
+
const existing2 = this._drivers?.get(prop);
|
|
409
447
|
if (existing2) this._settleDriver(existing2);
|
|
410
|
-
this._drivers
|
|
448
|
+
this._drivers?.delete(prop);
|
|
411
449
|
this._applyAnimated(prop, to);
|
|
412
450
|
return;
|
|
413
451
|
}
|
|
414
|
-
const existing = this._drivers
|
|
452
|
+
const existing = this._drivers?.get(prop);
|
|
415
453
|
if (existing) {
|
|
416
454
|
this._settleDriver(existing);
|
|
417
455
|
existing.retarget(to);
|
|
@@ -419,8 +457,9 @@ var Entity = class {
|
|
|
419
457
|
}
|
|
420
458
|
const from = this._currentOf(prop);
|
|
421
459
|
const driver = isTweenConfig(cfg) ? new TweenDriver(from, to, cfg) : new SpringDriver(from, to, cfg === "spring" ? {} : cfg);
|
|
422
|
-
this._drivers.set(prop, driver);
|
|
460
|
+
(this._drivers ??= /* @__PURE__ */ new Map()).set(prop, driver);
|
|
423
461
|
this.scene?.markDirty();
|
|
462
|
+
this.scene?._registerActiveDriverEntity(this);
|
|
424
463
|
}
|
|
425
464
|
/** Assignment path when a declarative transition is configured for `prop`. */
|
|
426
465
|
_animateProp(prop, to) {
|
|
@@ -453,7 +492,7 @@ var Entity = class {
|
|
|
453
492
|
entries.map(
|
|
454
493
|
(e) => new Promise((resolve) => {
|
|
455
494
|
this._spawnDriver(e[0], e[1], cfg);
|
|
456
|
-
const d = this._drivers
|
|
495
|
+
const d = this._drivers?.get(e[0]);
|
|
457
496
|
if (!d)
|
|
458
497
|
resolve();
|
|
459
498
|
else d.onDone = resolve;
|
|
@@ -463,7 +502,8 @@ var Entity = class {
|
|
|
463
502
|
}
|
|
464
503
|
/** Advance active property drivers one frame. Call from update(). */
|
|
465
504
|
tickDrivers(dt) {
|
|
466
|
-
if (this._drivers.size === 0) return;
|
|
505
|
+
if (!this._drivers || this._drivers.size === 0) return;
|
|
506
|
+
if (this.scene && this._driversTickedFrame === this.scene.currentFrame) return;
|
|
467
507
|
for (const [prop, driver] of this._drivers) {
|
|
468
508
|
driver.tick(dt);
|
|
469
509
|
if (driver.isDone()) {
|
|
@@ -476,6 +516,38 @@ var Entity = class {
|
|
|
476
516
|
}
|
|
477
517
|
this.scene?.markDirty();
|
|
478
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Internal: this entity's active-driver map (read-only view), or `null` if
|
|
521
|
+
* it has none. Called only by Scene's batched WASM animation pass, never
|
|
522
|
+
* application code. Returns the Map directly (not a callback iteration) so
|
|
523
|
+
* a caller can `for...of` it with zero per-entity closure allocation — the
|
|
524
|
+
* integrated benchmark (benchmarks/anim-wasm-scene) found a fresh callback
|
|
525
|
+
* per entity per frame was a real cost, not a negligible one.
|
|
526
|
+
*/
|
|
527
|
+
_driverEntries() {
|
|
528
|
+
return this._drivers;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Internal: finalize one driver that was ALREADY advanced externally this
|
|
532
|
+
* frame (e.g. by Scene's batched WASM tick via `driver.syncExternal`, or a
|
|
533
|
+
* direct `driver.tick()` call for a driver the batch can't offload) —
|
|
534
|
+
* exactly mirrors tickDrivers()'s own per-driver completion logic, so a
|
|
535
|
+
* driver behaves identically regardless of which path ticked it. Called
|
|
536
|
+
* only by Scene, never application code.
|
|
537
|
+
*/
|
|
538
|
+
_applyDriverTick(prop, driver) {
|
|
539
|
+
if (driver.isDone()) {
|
|
540
|
+
this._applyAnimated(prop, driver.target);
|
|
541
|
+
this._settleDriver(driver);
|
|
542
|
+
this._drivers?.delete(prop);
|
|
543
|
+
} else {
|
|
544
|
+
this._applyAnimated(prop, driver.value);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** Internal: true if this entity currently has any active property driver. */
|
|
548
|
+
_hasActiveDrivers() {
|
|
549
|
+
return !!this._drivers && this._drivers.size > 0;
|
|
550
|
+
}
|
|
479
551
|
/**
|
|
480
552
|
* Advance the entity's internal state for one frame.
|
|
481
553
|
*
|
|
@@ -487,7 +559,7 @@ var Entity = class {
|
|
|
487
559
|
*/
|
|
488
560
|
update(dt, time) {
|
|
489
561
|
this.tickDrivers(dt);
|
|
490
|
-
if (this.animations.length > 0) {
|
|
562
|
+
if (this.animations && this.animations.length > 0) {
|
|
491
563
|
const anim = this.animations[0];
|
|
492
564
|
if (anim.startTime === -1) {
|
|
493
565
|
anim.startTime = time;
|
|
@@ -528,7 +600,7 @@ var Entity = class {
|
|
|
528
600
|
* @example entity.on('click', (e) => console.log('clicked', e));
|
|
529
601
|
*/
|
|
530
602
|
on(event, callback, options) {
|
|
531
|
-
const map = options?.capture ? this.captureListeners : this.listeners;
|
|
603
|
+
const map = options?.capture ? this.captureListeners ??= /* @__PURE__ */ new Map() : this.listeners ??= /* @__PURE__ */ new Map();
|
|
532
604
|
if (!map.has(event)) {
|
|
533
605
|
map.set(event, []);
|
|
534
606
|
}
|
|
@@ -544,7 +616,7 @@ var Entity = class {
|
|
|
544
616
|
* @returns `this` for method chaining.
|
|
545
617
|
*/
|
|
546
618
|
off(event, callback, options) {
|
|
547
|
-
const handlers = (options?.capture ? this.captureListeners : this.listeners)
|
|
619
|
+
const handlers = (options?.capture ? this.captureListeners : this.listeners)?.get(event);
|
|
548
620
|
if (handlers) {
|
|
549
621
|
const idx = handlers.indexOf(callback);
|
|
550
622
|
if (idx !== -1) handlers.splice(idx, 1);
|
|
@@ -552,17 +624,37 @@ var Entity = class {
|
|
|
552
624
|
return this;
|
|
553
625
|
}
|
|
554
626
|
/**
|
|
555
|
-
* Tear down this entity
|
|
556
|
-
*
|
|
627
|
+
* Tear down this entity **and its entire subtree**: recursively destroy every
|
|
628
|
+
* descendant (leaf-first), clear all animations, event listeners, and property
|
|
629
|
+
* drivers, then detach from the parent. Call before discarding an entity to
|
|
630
|
+
* prevent memory leaks.
|
|
631
|
+
*
|
|
632
|
+
* Recursing here is what frees a subtree's GPU buffers, layout workers, and DOM
|
|
633
|
+
* observers when an app does `entity.destroy()` or `scene.remove(subtree)` on a
|
|
634
|
+
* route change — without it, only the root's own state was released and every
|
|
635
|
+
* descendant (and its subclass resources) was stranded. Subclasses that own
|
|
636
|
+
* external resources override `destroy()`, free their resource, then call
|
|
637
|
+
* `super.destroy()`; because children don't depend on a parent's resource, the
|
|
638
|
+
* order (parent resource first, then descendants) is safe.
|
|
639
|
+
*
|
|
640
|
+
* Idempotent and re-entrancy safe: a second call is a no-op, and because each
|
|
641
|
+
* child is detached as it is destroyed, destroying a subtree never double-frees.
|
|
557
642
|
*/
|
|
558
643
|
destroy() {
|
|
559
|
-
this.
|
|
560
|
-
|
|
561
|
-
|
|
644
|
+
if (this._destroyed) return;
|
|
645
|
+
this._destroyed = true;
|
|
646
|
+
while (this.children.length > 0) {
|
|
647
|
+
this.children.at(-1).destroy();
|
|
648
|
+
}
|
|
649
|
+
this.animations = null;
|
|
650
|
+
if (this._drivers) {
|
|
651
|
+
for (const driver of this._drivers.values()) {
|
|
652
|
+
this._settleDriver(driver);
|
|
653
|
+
}
|
|
654
|
+
this._drivers.clear();
|
|
562
655
|
}
|
|
563
|
-
this.
|
|
564
|
-
this.
|
|
565
|
-
this.captureListeners.clear();
|
|
656
|
+
this.listeners?.clear();
|
|
657
|
+
this.captureListeners?.clear();
|
|
566
658
|
if (this.parent) {
|
|
567
659
|
this.parent.remove(this);
|
|
568
660
|
}
|
|
@@ -577,7 +669,7 @@ var Entity = class {
|
|
|
577
669
|
* @param payload - Arbitrary data forwarded to each listener.
|
|
578
670
|
*/
|
|
579
671
|
emit(event, payload) {
|
|
580
|
-
const handlers = this.listeners
|
|
672
|
+
const handlers = this.listeners?.get(event);
|
|
581
673
|
if (handlers) {
|
|
582
674
|
handlers.forEach((h) => h(payload));
|
|
583
675
|
}
|
|
@@ -603,7 +695,7 @@ var Entity = class {
|
|
|
603
695
|
}
|
|
604
696
|
/** Run one node's listeners for the event, honoring stopImmediatePropagation. */
|
|
605
697
|
fireListeners(node, map, event) {
|
|
606
|
-
const handlers = map
|
|
698
|
+
const handlers = map?.get(event.type);
|
|
607
699
|
if (!handlers) return;
|
|
608
700
|
event.currentTarget = node;
|
|
609
701
|
for (const h of handlers.slice()) {
|
|
@@ -642,10 +734,45 @@ var Entity = class {
|
|
|
642
734
|
getGlobalPosition() {
|
|
643
735
|
return this.localToWorld(0, 0);
|
|
644
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* Internal: read this entity's cached world matrix into `out` — the SAME
|
|
739
|
+
* cache {@link getWorldTransform} reads — without allocating a wrapper
|
|
740
|
+
* object. Returns `false` (leaving `out` untouched) when the cache isn't
|
|
741
|
+
* valid for `frame` (typically the caller's `scene.currentFrame`), exactly
|
|
742
|
+
* mirroring {@link getWorldTransform}'s own validity check; the caller
|
|
743
|
+
* falls back to {@link getWorldTransform}'s full walk in that case. Exists
|
|
744
|
+
* so a per-entity gather that runs every entity through this (e.g. G3's
|
|
745
|
+
* `gatherHitAABBs`) pays for six scalar reads instead of one object
|
|
746
|
+
* allocation per entity per call — the exact class of per-frame garbage
|
|
747
|
+
* the G2 integrated benchmark found dominating its own gather cost.
|
|
748
|
+
*/
|
|
749
|
+
_readWorldCache(frame, out) {
|
|
750
|
+
if (this._worldFrame < 0 || this._worldFrame !== frame) return false;
|
|
751
|
+
out.a = this._wa;
|
|
752
|
+
out.b = this._wb;
|
|
753
|
+
out.c = this._wc;
|
|
754
|
+
out.d = this._wd;
|
|
755
|
+
out.e = this._we;
|
|
756
|
+
out.f = this._wf;
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
645
759
|
/**
|
|
646
760
|
* Return the exact accumulated Canvas `T * S * R` transform for this entity.
|
|
647
761
|
*/
|
|
648
762
|
getWorldTransform() {
|
|
763
|
+
if (this._worldFrame >= 0) {
|
|
764
|
+
const s = this.scene;
|
|
765
|
+
if (s && this._worldFrame === s.currentFrame) {
|
|
766
|
+
return {
|
|
767
|
+
a: this._wa,
|
|
768
|
+
b: this._wb,
|
|
769
|
+
c: this._wc,
|
|
770
|
+
d: this._wd,
|
|
771
|
+
e: this._we,
|
|
772
|
+
f: this._wf
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
}
|
|
649
776
|
const path = [this];
|
|
650
777
|
let ancestor = this.parent;
|
|
651
778
|
while (ancestor) {
|
|
@@ -660,8 +787,9 @@ var Entity = class {
|
|
|
660
787
|
let f = 0;
|
|
661
788
|
for (let i = path.length - 1; i >= 0; i--) {
|
|
662
789
|
const node = path[i];
|
|
663
|
-
const
|
|
664
|
-
const
|
|
790
|
+
const trig = node._getTrig();
|
|
791
|
+
const cos = trig.cos;
|
|
792
|
+
const sin = trig.sin;
|
|
665
793
|
const la = node.scaleX * cos;
|
|
666
794
|
const lb = node.scaleY * sin;
|
|
667
795
|
const lc = -node.scaleX * sin;
|
|
@@ -683,6 +811,37 @@ var Entity = class {
|
|
|
683
811
|
}
|
|
684
812
|
return { a, b, c, d, e, f };
|
|
685
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* Return this entity's cached `{ cos, sin }` of its current rotation,
|
|
816
|
+
* recomputing only when `rotation` has actually changed since the last call.
|
|
817
|
+
* The same object identity is returned across calls with an unchanged
|
|
818
|
+
* rotation, so callers must treat it as read-only. Used by the render walk
|
|
819
|
+
* and {@link getWorldTransform} to avoid V8's comparatively slow Math.cos/sin
|
|
820
|
+
* (a software libm call, ~2.5x slower than other engines) per entity/frame.
|
|
821
|
+
*/
|
|
822
|
+
_getTrig() {
|
|
823
|
+
if (this._trigRotation !== this._rotation) {
|
|
824
|
+
this._trig.cos = Math.cos(this._rotation);
|
|
825
|
+
this._trig.sin = Math.sin(this._rotation);
|
|
826
|
+
this._trigRotation = this._rotation;
|
|
827
|
+
}
|
|
828
|
+
return this._trig;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Store the world matrix Scene computed for this entity during the render
|
|
832
|
+
* walk, stamped with the frame it belongs to. {@link getWorldTransform}
|
|
833
|
+
* returns it verbatim while `frame === scene.currentFrame`. Internal: called
|
|
834
|
+
* only by Scene's renderer, never by application code.
|
|
835
|
+
*/
|
|
836
|
+
_setWorldCache(a, b, c, d, e, f, frame) {
|
|
837
|
+
this._wa = a;
|
|
838
|
+
this._wb = b;
|
|
839
|
+
this._wc = c;
|
|
840
|
+
this._wd = d;
|
|
841
|
+
this._we = e;
|
|
842
|
+
this._wf = f;
|
|
843
|
+
this._worldFrame = frame;
|
|
844
|
+
}
|
|
686
845
|
/** Convert a point from this entity's local space to Scene/world space. */
|
|
687
846
|
localToWorld(localX, localY) {
|
|
688
847
|
const { a, b, c, d, e, f } = this.getWorldTransform();
|
|
@@ -712,7 +871,12 @@ var Entity = class {
|
|
|
712
871
|
* does not provide a render-specific box.
|
|
713
872
|
*/
|
|
714
873
|
getWorldBounds() {
|
|
715
|
-
const bounds = this.getBounds() ?? {
|
|
874
|
+
const bounds = this.getBounds() ?? {
|
|
875
|
+
x: 0,
|
|
876
|
+
y: 0,
|
|
877
|
+
width: this.width,
|
|
878
|
+
height: this.height
|
|
879
|
+
};
|
|
716
880
|
const { a, b, c, d, e, f } = this.getWorldTransform();
|
|
717
881
|
let minX = Infinity;
|
|
718
882
|
let minY = Infinity;
|
|
@@ -853,7 +1017,7 @@ var Entity = class {
|
|
|
853
1017
|
* @returns `true` if at least one animation or property driver remains.
|
|
854
1018
|
*/
|
|
855
1019
|
hasPendingAnimations() {
|
|
856
|
-
return this.animations
|
|
1020
|
+
return (this.animations?.length ?? 0) > 0 || (this._drivers?.size ?? 0) > 0;
|
|
857
1021
|
}
|
|
858
1022
|
};
|
|
859
1023
|
|
|
@@ -869,6 +1033,14 @@ var MSDFTextEntity = class extends Entity {
|
|
|
869
1033
|
lineHeight;
|
|
870
1034
|
maxWidth;
|
|
871
1035
|
maxHeight;
|
|
1036
|
+
textAlign;
|
|
1037
|
+
// Optional hyphenator. Runs on the MAIN thread (a function can't be
|
|
1038
|
+
// structure-cloned into the layout worker), turning each word into parts
|
|
1039
|
+
// joined by soft hyphens (U+00AD); the worker then treats those as break
|
|
1040
|
+
// opportunities. `text` keeps the original string for a11y/content
|
|
1041
|
+
// projection; `layoutText` is the soft-hyphen-annotated string sent to layout.
|
|
1042
|
+
hyphenator = null;
|
|
1043
|
+
layoutText = "";
|
|
872
1044
|
text = "";
|
|
873
1045
|
lastRenderedSeqId = 0;
|
|
874
1046
|
rgbColorCache = /* @__PURE__ */ new Map();
|
|
@@ -885,6 +1057,7 @@ var MSDFTextEntity = class extends Entity {
|
|
|
885
1057
|
this.lineHeight = options.lineHeight;
|
|
886
1058
|
this.maxWidth = options.maxWidth ?? 1e3;
|
|
887
1059
|
this.maxHeight = options.maxHeight ?? 1e3;
|
|
1060
|
+
this.textAlign = options.textAlign ?? "left";
|
|
888
1061
|
this.setText(text);
|
|
889
1062
|
}
|
|
890
1063
|
/** Change the wrap boundary and re-run layout for the current text. */
|
|
@@ -893,13 +1066,53 @@ var MSDFTextEntity = class extends Entity {
|
|
|
893
1066
|
this.maxWidth = maxWidth;
|
|
894
1067
|
this.queueLayout();
|
|
895
1068
|
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Set horizontal alignment (`'justify'` stretches wrapped lines flush to
|
|
1071
|
+
* {@link setMaxWidth}'s width; the last line stays ragged) and re-run layout.
|
|
1072
|
+
*/
|
|
1073
|
+
setTextAlign(align) {
|
|
1074
|
+
if (this.textAlign === align) return;
|
|
1075
|
+
this.textAlign = align;
|
|
1076
|
+
this.queueLayout();
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Plug a hyphenator (word → parts). Break opportunities are inserted as soft
|
|
1080
|
+
* hyphens (U+00AD) into the string sent to layout, so a word that doesn't fit
|
|
1081
|
+
* can break with a visible hyphen. Soft hyphens already present in the text
|
|
1082
|
+
* work without one. Pass `null` to disable. The original text is preserved
|
|
1083
|
+
* for accessibility — only the layout string carries the hyphens.
|
|
1084
|
+
*/
|
|
1085
|
+
setHyphenator(fn) {
|
|
1086
|
+
this.hyphenator = fn;
|
|
1087
|
+
this.rebuildLayoutText();
|
|
1088
|
+
this.queueLayout();
|
|
1089
|
+
}
|
|
896
1090
|
setText(text) {
|
|
897
1091
|
if (this.text === text && this.layoutResult) return;
|
|
898
1092
|
this.text = text;
|
|
1093
|
+
this.rebuildLayoutText();
|
|
899
1094
|
this.queueLayout();
|
|
900
1095
|
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Recompute {@link layoutText} from {@link text}: with a hyphenator active,
|
|
1098
|
+
* split each whitespace-delimited word and rejoin its parts with U+00AD so
|
|
1099
|
+
* the worker sees the break opportunities. Without one, the layout string is
|
|
1100
|
+
* the text unchanged.
|
|
1101
|
+
*/
|
|
1102
|
+
rebuildLayoutText() {
|
|
1103
|
+
if (!this.hyphenator) {
|
|
1104
|
+
this.layoutText = this.text;
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const SHY = "\xAD";
|
|
1108
|
+
this.layoutText = this.text.replace(/[^\s]+/g, (word) => {
|
|
1109
|
+
if (word.length <= 3 || word.includes(SHY)) return word;
|
|
1110
|
+
const parts = this.hyphenator(word);
|
|
1111
|
+
return parts.length > 1 ? parts.join(SHY) : word;
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
901
1114
|
queueLayout() {
|
|
902
|
-
LayoutWorkerManager.getInstance().queueLayout(this.id, this.
|
|
1115
|
+
LayoutWorkerManager.getInstance().queueLayout(this.id, this.layoutText, {
|
|
903
1116
|
fontId: this.font.id,
|
|
904
1117
|
fontSize: this.fontSize,
|
|
905
1118
|
maxWidth: this.maxWidth,
|
|
@@ -907,6 +1120,7 @@ var MSDFTextEntity = class extends Entity {
|
|
|
907
1120
|
fontData: this.font.data,
|
|
908
1121
|
letterSpacing: this.letterSpacing,
|
|
909
1122
|
lineHeight: this.lineHeight,
|
|
1123
|
+
textAlign: this.textAlign,
|
|
910
1124
|
callback: (res) => {
|
|
911
1125
|
if (res.seqId < this.lastRenderedSeqId) return;
|
|
912
1126
|
this.lastRenderedSeqId = res.seqId;
|
|
@@ -1016,7 +1230,7 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1016
1230
|
}
|
|
1017
1231
|
}
|
|
1018
1232
|
destroy() {
|
|
1019
|
-
LayoutWorkerManager.
|
|
1233
|
+
LayoutWorkerManager.cancelLayoutForEntity(this.id);
|
|
1020
1234
|
super.destroy();
|
|
1021
1235
|
}
|
|
1022
1236
|
};
|