@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.
- package/dist/{chunk-XIEQHSBB.mjs → chunk-AQTO7OSU.mjs} +253 -496
- package/dist/{chunk-2Z23LTH3.js → chunk-DUYB4GX4.js} +310 -551
- package/dist/{chunk-BEUIB3U7.js → chunk-IYPLG4Z4.js} +93 -43
- package/dist/{chunk-L5BCKFQE.mjs → chunk-JFU56BX4.mjs} +48 -0
- package/dist/index.d.ts +4 -12
- package/dist/index.js +2155 -456
- package/dist/index.mjs +1977 -279
- package/dist/layout/index.d.ts +1 -3
- package/dist/layout.js +3 -15
- package/dist/layout.mjs +2 -16
- 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 +30 -1
- package/dist/text/index.d.ts +1 -5
- package/dist/text.js +5 -15
- package/dist/text.mjs +6 -17
- package/dist/tree/ComputeParticleEntity.d.ts +18 -0
- package/dist/tree/DOMPortalEntity.d.ts +18 -0
- package/dist/tree/Entity.d.ts +122 -6
- 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 +17 -8
- package/dist/animation/drivers.d.ts +0 -48
- package/dist/animation/easing.d.ts +0 -16
- package/dist/chunk-4AR425AR.js +0 -1121
- package/dist/chunk-BA5HUUDF.js +0 -760
- package/dist/chunk-IESDTEJ4.mjs +0 -1121
- package/dist/chunk-X7I465AQ.mjs +0 -760
- package/dist/layout/LayoutEngine.d.ts +0 -289
- package/dist/layout/LayoutWorker.d.ts +0 -23
- package/dist/layout/LayoutWorkerManager.d.ts +0 -26
- package/dist/layout/LayoutWorkerSource.d.ts +0 -1
- package/dist/layout/measure.d.ts +0 -20
- package/dist/math/SpatialHashGrid.d.ts +0 -53
- package/dist/math/SpringPhysics.d.ts +0 -13
- package/dist/text/ArabicShaper.d.ts +0 -10
- package/dist/text/BidiResolver.d.ts +0 -5
- package/dist/text/MSDFFont.d.ts +0 -129
- package/dist/text/PreparedContentGrid.d.ts +0 -60
- package/dist/text/Typography.d.ts +0 -11
|
@@ -1,137 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ArabicShaper,
|
|
3
|
-
BidiResolver,
|
|
4
|
-
LayoutWorkerManager
|
|
5
|
-
} from "./chunk-IESDTEJ4.mjs";
|
|
6
|
-
|
|
7
|
-
// src/math/SpringPhysics.ts
|
|
8
|
-
var MAX_FRAME_DT = 0.25;
|
|
9
|
-
var MAX_STEP_DT = 1 / 120;
|
|
10
|
-
var SpringPhysics = class {
|
|
11
|
-
value;
|
|
12
|
-
target;
|
|
13
|
-
velocity = 0;
|
|
14
|
-
stiffness = 180;
|
|
15
|
-
damping = 12;
|
|
16
|
-
mass = 1;
|
|
17
|
-
valEpsilon = 5e-3;
|
|
18
|
-
velEpsilon = 5e-3;
|
|
19
|
-
constructor(initial) {
|
|
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
|
-
};
|
|
50
|
-
|
|
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
|
-
|
|
69
|
-
// src/animation/drivers.ts
|
|
70
|
-
function isTweenConfig(c) {
|
|
71
|
-
return typeof c === "object" && "duration" in c;
|
|
72
|
-
}
|
|
73
|
-
var TweenDriver = class {
|
|
74
|
-
value;
|
|
75
|
-
from;
|
|
76
|
-
to;
|
|
77
|
-
elapsed = 0;
|
|
78
|
-
duration;
|
|
79
|
-
delay;
|
|
80
|
-
ease;
|
|
81
|
-
constructor(from, to, cfg) {
|
|
82
|
-
this.value = from;
|
|
83
|
-
this.from = from;
|
|
84
|
-
this.to = to;
|
|
85
|
-
this.duration = Math.max(1, cfg.duration);
|
|
86
|
-
this.delay = cfg.delay ?? 0;
|
|
87
|
-
this.ease = typeof cfg.easing === "function" ? cfg.easing : Easing[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
|
-
};
|
|
108
|
-
var SpringDriver = class {
|
|
109
|
-
spring;
|
|
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
1
|
// src/tree/Entity.ts
|
|
2
|
+
import {
|
|
3
|
+
TweenDriver,
|
|
4
|
+
SpringDriver,
|
|
5
|
+
isTweenConfig
|
|
6
|
+
} from "@vectojs/animation";
|
|
135
7
|
var ANIMATABLE_PROPS = /* @__PURE__ */ new Set([
|
|
136
8
|
"x",
|
|
137
9
|
"y",
|
|
@@ -274,8 +146,42 @@ var Entity = class {
|
|
|
274
146
|
// Danmaku hot loop), so a bare `entity.x = v` is one boolean check + field write.
|
|
275
147
|
_hasTransitions = false;
|
|
276
148
|
_transitions = null;
|
|
277
|
-
|
|
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;
|
|
278
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;
|
|
279
185
|
get x() {
|
|
280
186
|
return this._x;
|
|
281
187
|
}
|
|
@@ -352,10 +258,12 @@ var Entity = class {
|
|
|
352
258
|
* fixed viewport. Off by default (children render unclipped). Canvas2D only.
|
|
353
259
|
*/
|
|
354
260
|
clipChildren = false;
|
|
355
|
-
|
|
261
|
+
// Lazily allocated (see _drivers above). Most entities never register a
|
|
262
|
+
// listener or an imperative animate() tween.
|
|
263
|
+
listeners = null;
|
|
356
264
|
/** Capture-phase listeners (fired root→target before bubble). */
|
|
357
|
-
captureListeners =
|
|
358
|
-
animations =
|
|
265
|
+
captureListeners = null;
|
|
266
|
+
animations = null;
|
|
359
267
|
constructor(id) {
|
|
360
268
|
this.id = id || `entity_${Math.random().toString(36).substring(2, 9)}`;
|
|
361
269
|
}
|
|
@@ -390,6 +298,7 @@ var Entity = class {
|
|
|
390
298
|
const s = this.scene;
|
|
391
299
|
if (s) {
|
|
392
300
|
s.a11yNeedsReorder = true;
|
|
301
|
+
s.markStructureChanged?.();
|
|
393
302
|
s.markDirty();
|
|
394
303
|
child._notifyMounted();
|
|
395
304
|
}
|
|
@@ -419,6 +328,7 @@ var Entity = class {
|
|
|
419
328
|
if (s) {
|
|
420
329
|
s.detachA11y(child);
|
|
421
330
|
s.a11yNeedsReorder = true;
|
|
331
|
+
s.markStructureChanged?.();
|
|
422
332
|
s.markDirty();
|
|
423
333
|
}
|
|
424
334
|
}
|
|
@@ -467,7 +377,7 @@ var Entity = class {
|
|
|
467
377
|
* @example entity.animate({ x: 400, opacity: 0 }, 500);
|
|
468
378
|
*/
|
|
469
379
|
animate(targetProps, durationMs) {
|
|
470
|
-
this.animations.push({
|
|
380
|
+
(this.animations ??= []).push({
|
|
471
381
|
target: targetProps,
|
|
472
382
|
duration: durationMs,
|
|
473
383
|
startTime: -1,
|
|
@@ -520,9 +430,9 @@ var Entity = class {
|
|
|
520
430
|
* that need to seed a starting state (e.g. the presence helper's enter `from`).
|
|
521
431
|
*/
|
|
522
432
|
setImmediate(prop, v) {
|
|
523
|
-
const existing = this._drivers
|
|
433
|
+
const existing = this._drivers?.get(prop);
|
|
524
434
|
if (existing) this._settleDriver(existing);
|
|
525
|
-
this._drivers
|
|
435
|
+
this._drivers?.delete(prop);
|
|
526
436
|
this._applyAnimated(prop, v);
|
|
527
437
|
}
|
|
528
438
|
_settleDriver(driver) {
|
|
@@ -533,13 +443,13 @@ var Entity = class {
|
|
|
533
443
|
}
|
|
534
444
|
_spawnDriver(prop, to, cfg) {
|
|
535
445
|
if (prop !== "opacity" && this.scene?.prefersReducedMotion) {
|
|
536
|
-
const existing2 = this._drivers
|
|
446
|
+
const existing2 = this._drivers?.get(prop);
|
|
537
447
|
if (existing2) this._settleDriver(existing2);
|
|
538
|
-
this._drivers
|
|
448
|
+
this._drivers?.delete(prop);
|
|
539
449
|
this._applyAnimated(prop, to);
|
|
540
450
|
return;
|
|
541
451
|
}
|
|
542
|
-
const existing = this._drivers
|
|
452
|
+
const existing = this._drivers?.get(prop);
|
|
543
453
|
if (existing) {
|
|
544
454
|
this._settleDriver(existing);
|
|
545
455
|
existing.retarget(to);
|
|
@@ -547,8 +457,9 @@ var Entity = class {
|
|
|
547
457
|
}
|
|
548
458
|
const from = this._currentOf(prop);
|
|
549
459
|
const driver = isTweenConfig(cfg) ? new TweenDriver(from, to, cfg) : new SpringDriver(from, to, cfg === "spring" ? {} : cfg);
|
|
550
|
-
this._drivers.set(prop, driver);
|
|
460
|
+
(this._drivers ??= /* @__PURE__ */ new Map()).set(prop, driver);
|
|
551
461
|
this.scene?.markDirty();
|
|
462
|
+
this.scene?._registerActiveDriverEntity(this);
|
|
552
463
|
}
|
|
553
464
|
/** Assignment path when a declarative transition is configured for `prop`. */
|
|
554
465
|
_animateProp(prop, to) {
|
|
@@ -581,7 +492,7 @@ var Entity = class {
|
|
|
581
492
|
entries.map(
|
|
582
493
|
(e) => new Promise((resolve) => {
|
|
583
494
|
this._spawnDriver(e[0], e[1], cfg);
|
|
584
|
-
const d = this._drivers
|
|
495
|
+
const d = this._drivers?.get(e[0]);
|
|
585
496
|
if (!d)
|
|
586
497
|
resolve();
|
|
587
498
|
else d.onDone = resolve;
|
|
@@ -591,7 +502,8 @@ var Entity = class {
|
|
|
591
502
|
}
|
|
592
503
|
/** Advance active property drivers one frame. Call from update(). */
|
|
593
504
|
tickDrivers(dt) {
|
|
594
|
-
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;
|
|
595
507
|
for (const [prop, driver] of this._drivers) {
|
|
596
508
|
driver.tick(dt);
|
|
597
509
|
if (driver.isDone()) {
|
|
@@ -604,6 +516,38 @@ var Entity = class {
|
|
|
604
516
|
}
|
|
605
517
|
this.scene?.markDirty();
|
|
606
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
|
+
}
|
|
607
551
|
/**
|
|
608
552
|
* Advance the entity's internal state for one frame.
|
|
609
553
|
*
|
|
@@ -615,7 +559,7 @@ var Entity = class {
|
|
|
615
559
|
*/
|
|
616
560
|
update(dt, time) {
|
|
617
561
|
this.tickDrivers(dt);
|
|
618
|
-
if (this.animations.length > 0) {
|
|
562
|
+
if (this.animations && this.animations.length > 0) {
|
|
619
563
|
const anim = this.animations[0];
|
|
620
564
|
if (anim.startTime === -1) {
|
|
621
565
|
anim.startTime = time;
|
|
@@ -656,7 +600,7 @@ var Entity = class {
|
|
|
656
600
|
* @example entity.on('click', (e) => console.log('clicked', e));
|
|
657
601
|
*/
|
|
658
602
|
on(event, callback, options) {
|
|
659
|
-
const map = options?.capture ? this.captureListeners : this.listeners;
|
|
603
|
+
const map = options?.capture ? this.captureListeners ??= /* @__PURE__ */ new Map() : this.listeners ??= /* @__PURE__ */ new Map();
|
|
660
604
|
if (!map.has(event)) {
|
|
661
605
|
map.set(event, []);
|
|
662
606
|
}
|
|
@@ -672,7 +616,7 @@ var Entity = class {
|
|
|
672
616
|
* @returns `this` for method chaining.
|
|
673
617
|
*/
|
|
674
618
|
off(event, callback, options) {
|
|
675
|
-
const handlers = (options?.capture ? this.captureListeners : this.listeners)
|
|
619
|
+
const handlers = (options?.capture ? this.captureListeners : this.listeners)?.get(event);
|
|
676
620
|
if (handlers) {
|
|
677
621
|
const idx = handlers.indexOf(callback);
|
|
678
622
|
if (idx !== -1) handlers.splice(idx, 1);
|
|
@@ -680,17 +624,37 @@ var Entity = class {
|
|
|
680
624
|
return this;
|
|
681
625
|
}
|
|
682
626
|
/**
|
|
683
|
-
* Tear down this entity
|
|
684
|
-
*
|
|
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.
|
|
685
642
|
*/
|
|
686
643
|
destroy() {
|
|
687
|
-
this.
|
|
688
|
-
|
|
689
|
-
|
|
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();
|
|
690
655
|
}
|
|
691
|
-
this.
|
|
692
|
-
this.
|
|
693
|
-
this.captureListeners.clear();
|
|
656
|
+
this.listeners?.clear();
|
|
657
|
+
this.captureListeners?.clear();
|
|
694
658
|
if (this.parent) {
|
|
695
659
|
this.parent.remove(this);
|
|
696
660
|
}
|
|
@@ -705,7 +669,7 @@ var Entity = class {
|
|
|
705
669
|
* @param payload - Arbitrary data forwarded to each listener.
|
|
706
670
|
*/
|
|
707
671
|
emit(event, payload) {
|
|
708
|
-
const handlers = this.listeners
|
|
672
|
+
const handlers = this.listeners?.get(event);
|
|
709
673
|
if (handlers) {
|
|
710
674
|
handlers.forEach((h) => h(payload));
|
|
711
675
|
}
|
|
@@ -731,7 +695,7 @@ var Entity = class {
|
|
|
731
695
|
}
|
|
732
696
|
/** Run one node's listeners for the event, honoring stopImmediatePropagation. */
|
|
733
697
|
fireListeners(node, map, event) {
|
|
734
|
-
const handlers = map
|
|
698
|
+
const handlers = map?.get(event.type);
|
|
735
699
|
if (!handlers) return;
|
|
736
700
|
event.currentTarget = node;
|
|
737
701
|
for (const h of handlers.slice()) {
|
|
@@ -770,10 +734,45 @@ var Entity = class {
|
|
|
770
734
|
getGlobalPosition() {
|
|
771
735
|
return this.localToWorld(0, 0);
|
|
772
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
|
+
}
|
|
773
759
|
/**
|
|
774
760
|
* Return the exact accumulated Canvas `T * S * R` transform for this entity.
|
|
775
761
|
*/
|
|
776
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
|
+
}
|
|
777
776
|
const path = [this];
|
|
778
777
|
let ancestor = this.parent;
|
|
779
778
|
while (ancestor) {
|
|
@@ -788,8 +787,9 @@ var Entity = class {
|
|
|
788
787
|
let f = 0;
|
|
789
788
|
for (let i = path.length - 1; i >= 0; i--) {
|
|
790
789
|
const node = path[i];
|
|
791
|
-
const
|
|
792
|
-
const
|
|
790
|
+
const trig = node._getTrig();
|
|
791
|
+
const cos = trig.cos;
|
|
792
|
+
const sin = trig.sin;
|
|
793
793
|
const la = node.scaleX * cos;
|
|
794
794
|
const lb = node.scaleY * sin;
|
|
795
795
|
const lc = -node.scaleX * sin;
|
|
@@ -811,6 +811,37 @@ var Entity = class {
|
|
|
811
811
|
}
|
|
812
812
|
return { a, b, c, d, e, f };
|
|
813
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
|
+
}
|
|
814
845
|
/** Convert a point from this entity's local space to Scene/world space. */
|
|
815
846
|
localToWorld(localX, localY) {
|
|
816
847
|
const { a, b, c, d, e, f } = this.getWorldTransform();
|
|
@@ -840,7 +871,12 @@ var Entity = class {
|
|
|
840
871
|
* does not provide a render-specific box.
|
|
841
872
|
*/
|
|
842
873
|
getWorldBounds() {
|
|
843
|
-
const bounds = this.getBounds() ?? {
|
|
874
|
+
const bounds = this.getBounds() ?? {
|
|
875
|
+
x: 0,
|
|
876
|
+
y: 0,
|
|
877
|
+
width: this.width,
|
|
878
|
+
height: this.height
|
|
879
|
+
};
|
|
844
880
|
const { a, b, c, d, e, f } = this.getWorldTransform();
|
|
845
881
|
let minX = Infinity;
|
|
846
882
|
let minY = Infinity;
|
|
@@ -981,133 +1017,12 @@ var Entity = class {
|
|
|
981
1017
|
* @returns `true` if at least one animation or property driver remains.
|
|
982
1018
|
*/
|
|
983
1019
|
hasPendingAnimations() {
|
|
984
|
-
return this.animations
|
|
985
|
-
}
|
|
986
|
-
};
|
|
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 = class _MSDFFont {
|
|
1018
|
-
static idCounter = 0;
|
|
1019
|
-
id;
|
|
1020
|
-
data;
|
|
1021
|
-
byCode = /* @__PURE__ */ new Map();
|
|
1022
|
-
kern = /* @__PURE__ */ new Map();
|
|
1023
|
-
constructor(data) {
|
|
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 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
|
-
};
|
|
1020
|
+
return (this.animations?.length ?? 0) > 0 || (this._drivers?.size ?? 0) > 0;
|
|
1107
1021
|
}
|
|
1108
1022
|
};
|
|
1109
1023
|
|
|
1110
1024
|
// src/text/MSDFTextEntity.ts
|
|
1025
|
+
import { LayoutWorkerManager } from "@vectojs/layout";
|
|
1111
1026
|
var MSDFTextEntity = class extends Entity {
|
|
1112
1027
|
font;
|
|
1113
1028
|
texture;
|
|
@@ -1118,6 +1033,14 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1118
1033
|
lineHeight;
|
|
1119
1034
|
maxWidth;
|
|
1120
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 = "";
|
|
1121
1044
|
text = "";
|
|
1122
1045
|
lastRenderedSeqId = 0;
|
|
1123
1046
|
rgbColorCache = /* @__PURE__ */ new Map();
|
|
@@ -1134,6 +1057,7 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1134
1057
|
this.lineHeight = options.lineHeight;
|
|
1135
1058
|
this.maxWidth = options.maxWidth ?? 1e3;
|
|
1136
1059
|
this.maxHeight = options.maxHeight ?? 1e3;
|
|
1060
|
+
this.textAlign = options.textAlign ?? "left";
|
|
1137
1061
|
this.setText(text);
|
|
1138
1062
|
}
|
|
1139
1063
|
/** Change the wrap boundary and re-run layout for the current text. */
|
|
@@ -1142,13 +1066,53 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1142
1066
|
this.maxWidth = maxWidth;
|
|
1143
1067
|
this.queueLayout();
|
|
1144
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
|
+
}
|
|
1145
1090
|
setText(text) {
|
|
1146
1091
|
if (this.text === text && this.layoutResult) return;
|
|
1147
1092
|
this.text = text;
|
|
1093
|
+
this.rebuildLayoutText();
|
|
1148
1094
|
this.queueLayout();
|
|
1149
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
|
+
}
|
|
1150
1114
|
queueLayout() {
|
|
1151
|
-
LayoutWorkerManager.getInstance().queueLayout(this.id, this.
|
|
1115
|
+
LayoutWorkerManager.getInstance().queueLayout(this.id, this.layoutText, {
|
|
1152
1116
|
fontId: this.font.id,
|
|
1153
1117
|
fontSize: this.fontSize,
|
|
1154
1118
|
maxWidth: this.maxWidth,
|
|
@@ -1156,6 +1120,7 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1156
1120
|
fontData: this.font.data,
|
|
1157
1121
|
letterSpacing: this.letterSpacing,
|
|
1158
1122
|
lineHeight: this.lineHeight,
|
|
1123
|
+
textAlign: this.textAlign,
|
|
1159
1124
|
callback: (res) => {
|
|
1160
1125
|
if (res.seqId < this.lastRenderedSeqId) return;
|
|
1161
1126
|
this.lastRenderedSeqId = res.seqId;
|
|
@@ -1265,7 +1230,7 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1265
1230
|
}
|
|
1266
1231
|
}
|
|
1267
1232
|
destroy() {
|
|
1268
|
-
LayoutWorkerManager.
|
|
1233
|
+
LayoutWorkerManager.cancelLayoutForEntity(this.id);
|
|
1269
1234
|
super.destroy();
|
|
1270
1235
|
}
|
|
1271
1236
|
};
|
|
@@ -1505,217 +1470,9 @@ var SVGEntity = class extends Entity {
|
|
|
1505
1470
|
}
|
|
1506
1471
|
};
|
|
1507
1472
|
|
|
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 = 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 = 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 = 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 = ArabicShaper.shapeArabic(rawLine);
|
|
1642
|
-
const shapedParts = graphemes(shaped.shapedText);
|
|
1643
|
-
const levels = 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 = shaped.indexMap[part.index] ?? part.index;
|
|
1649
|
-
const nextPart = shapedParts[index + 1];
|
|
1650
|
-
const sourceOffsetEnd = nextPart ? 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: levels[part.index] ?? 0,
|
|
1672
|
-
char: part.segment
|
|
1673
|
-
});
|
|
1674
|
-
column += columns;
|
|
1675
|
-
}
|
|
1676
|
-
const visualCells = [...cells];
|
|
1677
|
-
BidiResolver.reorderVisual(visualCells, 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
1473
|
export {
|
|
1708
|
-
SpringPhysics,
|
|
1709
|
-
Easing,
|
|
1710
|
-
isTweenConfig,
|
|
1711
|
-
TweenDriver,
|
|
1712
|
-
SpringDriver,
|
|
1713
1474
|
VectoJSEvent,
|
|
1714
1475
|
Entity,
|
|
1715
|
-
cssLineBoxBaseline,
|
|
1716
|
-
clearCssLineBoxMetrics,
|
|
1717
|
-
MSDFFont,
|
|
1718
1476
|
MSDFTextEntity,
|
|
1719
|
-
SVGEntity
|
|
1720
|
-
prepareContentGrid
|
|
1477
|
+
SVGEntity
|
|
1721
1478
|
};
|