@vectojs/core 1.19.0 → 1.21.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-IA3KW4CG.js → chunk-AGP4VLF4.js} +75 -25
- package/dist/{chunk-XWBVBXFZ.mjs → chunk-FRMLD4PP.mjs} +50 -0
- package/dist/{chunk-L4SWVP2H.js → chunk-GKSCJ6AF.js} +201 -9
- package/dist/{chunk-QS3CUV7H.mjs → chunk-RTENOAYT.mjs} +192 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +542 -228
- package/dist/index.mjs +335 -21
- package/dist/renderer/CanvasRenderer.d.ts +2 -0
- package/dist/renderer/GlyphRasterAtlas.d.ts +186 -0
- package/dist/renderer/IRenderer.d.ts +31 -0
- package/dist/renderer/index.d.ts +1 -0
- package/dist/renderer.js +4 -2
- package/dist/renderer.mjs +3 -1
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +144 -0
- package/dist/tree/Scene.d.ts +179 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanvasRenderer,
|
|
3
|
+
GlyphRasterAtlas,
|
|
3
4
|
SVGRenderer,
|
|
4
5
|
TextRasterCache,
|
|
5
6
|
WebGPUParticleSystemManager,
|
|
@@ -7,13 +8,13 @@ import {
|
|
|
7
8
|
isSafeUrl,
|
|
8
9
|
parseColorToRGBA,
|
|
9
10
|
sanitizeUrl
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-RTENOAYT.mjs";
|
|
11
12
|
import {
|
|
12
13
|
Entity,
|
|
13
14
|
MSDFTextEntity,
|
|
14
15
|
SVGEntity,
|
|
15
16
|
VectoJSEvent
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-FRMLD4PP.mjs";
|
|
17
18
|
|
|
18
19
|
// src/tree/Scene.ts
|
|
19
20
|
import { SpringDriver, TweenDriver } from "@vectojs/animation";
|
|
@@ -1538,6 +1539,70 @@ var Scene = class _Scene {
|
|
|
1538
1539
|
renderMode = "always";
|
|
1539
1540
|
/** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
|
|
1540
1541
|
static MAX_DIRTY_REASONS = 200;
|
|
1542
|
+
_phaseTiming = false;
|
|
1543
|
+
_phaseTotals = /* @__PURE__ */ new Map();
|
|
1544
|
+
/**
|
|
1545
|
+
* Start or stop per-phase render timing.
|
|
1546
|
+
*
|
|
1547
|
+
* Off by default, and the probes compile to a single boolean test when off:
|
|
1548
|
+
* these sit on the frame path, so the disabled cost has to be nothing. Enable,
|
|
1549
|
+
* run the scene, then read {@link renderPhases}.
|
|
1550
|
+
*
|
|
1551
|
+
* Exists because a frame total cannot tell you where the time went. The
|
|
1552
|
+
* markdown streaming benchmark put render at 85-99% of an append's cost, and
|
|
1553
|
+
* there was no way to decompose that number further — which is exactly the
|
|
1554
|
+
* position that led to two wrong optimisation guesses earlier
|
|
1555
|
+
* (`CodeBlock` reuse, hit-grid fusion), both of which measured as no change.
|
|
1556
|
+
*/
|
|
1557
|
+
setPhaseTiming(enabled) {
|
|
1558
|
+
this._phaseTiming = enabled;
|
|
1559
|
+
if (!enabled) this.clearRenderPhases();
|
|
1560
|
+
}
|
|
1561
|
+
/** Whether per-phase render timing is being recorded. */
|
|
1562
|
+
get phaseTiming() {
|
|
1563
|
+
return this._phaseTiming;
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Accumulate one phase sample.
|
|
1567
|
+
*
|
|
1568
|
+
* Totals rather than a per-frame log: the question is always "which phase owns
|
|
1569
|
+
* the frame", and a log of thousands of samples answers it less directly while
|
|
1570
|
+
* costing far more memory. `maxMs` is kept because a phase that is cheap on
|
|
1571
|
+
* average but spikes is a different problem from one that is uniformly slow.
|
|
1572
|
+
*/
|
|
1573
|
+
_recordPhase(phase, ms) {
|
|
1574
|
+
const existing = this._phaseTotals.get(phase);
|
|
1575
|
+
if (existing) {
|
|
1576
|
+
existing.totalMs += ms;
|
|
1577
|
+
existing.calls++;
|
|
1578
|
+
if (ms > existing.maxMs) existing.maxMs = ms;
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
this._phaseTotals.set(phase, { totalMs: ms, calls: 1, maxMs: ms });
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Recorded phase timings, most expensive first, with each phase's share of the
|
|
1585
|
+
* measured total.
|
|
1586
|
+
*
|
|
1587
|
+
* `share` is the number that matters: a phase at 4% cannot be worth optimising
|
|
1588
|
+
* however inefficient it looks in isolation.
|
|
1589
|
+
*/
|
|
1590
|
+
get renderPhases() {
|
|
1591
|
+
const entries = [...this._phaseTotals.entries()];
|
|
1592
|
+
const denominator = entries.filter(([phase]) => phase !== "render").reduce((sum, [, v]) => sum + v.totalMs, 0);
|
|
1593
|
+
return entries.map(([phase, v]) => ({
|
|
1594
|
+
phase,
|
|
1595
|
+
totalMs: +v.totalMs.toFixed(3),
|
|
1596
|
+
calls: v.calls,
|
|
1597
|
+
avgMs: +(v.totalMs / Math.max(1, v.calls)).toFixed(4),
|
|
1598
|
+
maxMs: +v.maxMs.toFixed(3),
|
|
1599
|
+
share: phase === "render" ? null : +(100 * v.totalMs / Math.max(1e-9, denominator)).toFixed(1)
|
|
1600
|
+
})).sort((a, b) => b.totalMs - a.totalMs);
|
|
1601
|
+
}
|
|
1602
|
+
/** Drop recorded phase timings, keeping timing enabled. */
|
|
1603
|
+
clearRenderPhases() {
|
|
1604
|
+
this._phaseTotals.clear();
|
|
1605
|
+
}
|
|
1541
1606
|
_dirtyTracking = false;
|
|
1542
1607
|
_dirtyReasons = /* @__PURE__ */ new Map();
|
|
1543
1608
|
dirty = true;
|
|
@@ -1623,6 +1688,29 @@ var Scene = class _Scene {
|
|
|
1623
1688
|
contentGridCalibrationFrames = /* @__PURE__ */ new Map();
|
|
1624
1689
|
/** Detached, untransformed font probes used by the cold calibration pass. */
|
|
1625
1690
|
contentGridCalibrationProbes = /* @__PURE__ */ new Map();
|
|
1691
|
+
/**
|
|
1692
|
+
* Monotonic stamp identifying the conditions grid cells were calibrated under.
|
|
1693
|
+
*
|
|
1694
|
+
* Calibration measures the difference between the advance the canvas grid assigns
|
|
1695
|
+
* a cluster and the width the browser lays it out at, then writes a per-cell
|
|
1696
|
+
* `scaleX`. That result stays valid until the font or the page scale changes, and
|
|
1697
|
+
* it lives on the cell element — so a cell carrying this stamp needs no further
|
|
1698
|
+
* work.
|
|
1699
|
+
*
|
|
1700
|
+
* The scan that feeds calibration was O(cells) on every revision bump: for a
|
|
1701
|
+
* streaming code block it re-derived a measurement key for every cell in the
|
|
1702
|
+
* block each frame in order to produce only ~20 distinct keys, costing about
|
|
1703
|
+
* 2.5 ms/frame after the `style.font` fix and still over half of `a11ySync`. Since
|
|
1704
|
+
* carrier reuse (#244) leaves untouched lines — and therefore their calibrated
|
|
1705
|
+
* transforms — in place, cells stamped with the current generation can simply be
|
|
1706
|
+
* skipped, making the scan O(new cells) instead.
|
|
1707
|
+
*
|
|
1708
|
+
* A plain incrementing integer rather than the descriptive calibration key,
|
|
1709
|
+
* because it goes into an attribute selector and must not need escaping.
|
|
1710
|
+
*/
|
|
1711
|
+
contentGridCalibrationGeneration = 0;
|
|
1712
|
+
/** The `(fontEpoch, pageScale)` pair the current generation corresponds to. */
|
|
1713
|
+
contentGridCalibrationStamp = "";
|
|
1626
1714
|
/** Invalidates grid font calibration after browser font availability changes. */
|
|
1627
1715
|
contentFontEpoch = 0;
|
|
1628
1716
|
/** Cached Canvas-to-client scale for the current font/viewport epoch. */
|
|
@@ -2731,6 +2819,29 @@ var Scene = class _Scene {
|
|
|
2731
2819
|
this.contentSelectionAnchor = null;
|
|
2732
2820
|
if (this.a11yRoot) this.a11yRoot.style.pointerEvents = "none";
|
|
2733
2821
|
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Index of the carrier line currently holding a selection inside `el`, or
|
|
2824
|
+
* `null`.
|
|
2825
|
+
*
|
|
2826
|
+
* Lets a partial re-materialization decide whether the user's selection is even
|
|
2827
|
+
* affected. Checks the tracked anchor first (it survives a drag) and falls back
|
|
2828
|
+
* to the live DOM selection.
|
|
2829
|
+
*/
|
|
2830
|
+
contentGridSelectionLine(el) {
|
|
2831
|
+
const candidates = [this.contentSelectionAnchor?.node];
|
|
2832
|
+
if (typeof window !== "undefined" && typeof window.getSelection === "function") {
|
|
2833
|
+
const selection = window.getSelection();
|
|
2834
|
+
candidates.push(selection?.anchorNode, selection?.focusNode);
|
|
2835
|
+
}
|
|
2836
|
+
for (const candidate of candidates) {
|
|
2837
|
+
if (!candidate || !el.contains(candidate)) continue;
|
|
2838
|
+
let cursor = candidate;
|
|
2839
|
+
while (cursor && cursor.parentNode !== el) cursor = cursor.parentNode;
|
|
2840
|
+
const lineIndex = cursor?.dataset?.vectoGridLine;
|
|
2841
|
+
if (lineIndex !== void 0) return Number(lineIndex);
|
|
2842
|
+
}
|
|
2843
|
+
return null;
|
|
2844
|
+
}
|
|
2734
2845
|
releaseContentSelectionForRebuild(el) {
|
|
2735
2846
|
const selection = typeof window !== "undefined" && typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
2736
2847
|
const ownsSelection = this.contentSelectionAnchor && el.contains(this.contentSelectionAnchor.node) || (selection?.anchorNode ? el.contains(selection.anchorNode) : false) || (selection?.focusNode ? el.contains(selection.focusNode) : false);
|
|
@@ -2816,7 +2927,18 @@ var Scene = class _Scene {
|
|
|
2816
2927
|
this.registerActiveDriverSubtree(entity);
|
|
2817
2928
|
return this;
|
|
2818
2929
|
}
|
|
2819
|
-
|
|
2930
|
+
/**
|
|
2931
|
+
* Reset per-grid calibration and bookkeeping before a (re)materialization.
|
|
2932
|
+
*
|
|
2933
|
+
* @param entityId - Owning entity, keyed into the calibration maps.
|
|
2934
|
+
* @param el - The projection element.
|
|
2935
|
+
* @param releaseSelection - Whether to drop a selection this element owns.
|
|
2936
|
+
* Pass `false` when carrier lines are being reused: the selection's DOM nodes
|
|
2937
|
+
* survive the pass, so tearing it down would wipe a user's selection on every
|
|
2938
|
+
* streamed chunk — the exact bug `preserveContentSelectionAcrossRebuild`
|
|
2939
|
+
* exists to prevent on the non-grid path.
|
|
2940
|
+
*/
|
|
2941
|
+
clearContentGridState(entityId, el, releaseSelection = true) {
|
|
2820
2942
|
const calibrationFrame = this.contentGridCalibrationFrames.get(entityId);
|
|
2821
2943
|
if (calibrationFrame !== void 0 && typeof cancelAnimationFrame === "function") {
|
|
2822
2944
|
cancelAnimationFrame(calibrationFrame);
|
|
@@ -2832,7 +2954,21 @@ var Scene = class _Scene {
|
|
|
2832
2954
|
delete el.dataset.vectoGridMaterializeMs;
|
|
2833
2955
|
delete el.dataset.vectoGridCalibrationSamples;
|
|
2834
2956
|
delete el.dataset.vectoGridCalibrationMs;
|
|
2835
|
-
this.releaseContentSelectionForRebuild(el);
|
|
2957
|
+
if (releaseSelection) this.releaseContentSelectionForRebuild(el);
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* Drop any projected elements under `node` without touching the entity tree.
|
|
2961
|
+
*
|
|
2962
|
+
* Used when the walk reaches an invisible subtree: the entities stay put (a
|
|
2963
|
+
* later `show()` re-projects them), but nothing under here may remain
|
|
2964
|
+
* focusable or announced while hidden.
|
|
2965
|
+
*/
|
|
2966
|
+
pruneA11ySubtree(node) {
|
|
2967
|
+
if (this.a11yElements.has(node.id) || this.contentElements.has(node.id)) {
|
|
2968
|
+
this.removeA11yRecursively(node);
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
for (const child of node.children) this.pruneA11ySubtree(child);
|
|
2836
2972
|
}
|
|
2837
2973
|
removeA11yRecursively(node) {
|
|
2838
2974
|
if (node.isDOMPortal) {
|
|
@@ -3148,10 +3284,32 @@ var Scene = class _Scene {
|
|
|
3148
3284
|
get overlayRootEntity() {
|
|
3149
3285
|
return this.overlayRoot;
|
|
3150
3286
|
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Advance and render exactly one frame, synchronously.
|
|
3289
|
+
*
|
|
3290
|
+
* This renders UNCONDITIONALLY: it consults neither {@link renderMode} nor
|
|
3291
|
+
* {@link dirty}, and it does not apply the `always`-mode idle auto-throttle.
|
|
3292
|
+
* That is deliberate — a deterministic driver (video export, a test, a
|
|
3293
|
+
* fixed-step benchmark) asks for a frame because it wants that frame, not a
|
|
3294
|
+
* scheduler opinion about whether it is needed.
|
|
3295
|
+
*
|
|
3296
|
+
* The consequence is a measurement footgun worth stating explicitly: a
|
|
3297
|
+
* benchmark that drives frames through `step()` CANNOT observe frame skipping,
|
|
3298
|
+
* so `always` and `onDemand` produce byte-identical draw counts through this
|
|
3299
|
+
* path. An investigation into whether `onDemand` skips redundant repaints once
|
|
3300
|
+
* concluded "it does not" on exactly that basis; on the real rAF loop the same
|
|
3301
|
+
* workload rendered ~1.0 frames per content change. To measure anything about
|
|
3302
|
+
* scheduling, use {@link start} and let `requestAnimationFrame` drive.
|
|
3303
|
+
*
|
|
3304
|
+
* @param dt Seconds to advance. Not clamped by `MAX_FRAME_DT` — the caller
|
|
3305
|
+
* chooses the step, since determinism is the point.
|
|
3306
|
+
*/
|
|
3151
3307
|
step(dt) {
|
|
3152
3308
|
const time = this.lastTime + dt;
|
|
3153
3309
|
this.lastTime = time;
|
|
3310
|
+
const t0 = this._phaseTiming ? performance.now() : 0;
|
|
3154
3311
|
this.render(this.renderer, dt, time);
|
|
3312
|
+
if (this._phaseTiming) this._recordPhase("render", performance.now() - t0);
|
|
3155
3313
|
this.dirty = false;
|
|
3156
3314
|
}
|
|
3157
3315
|
/**
|
|
@@ -3164,6 +3322,24 @@ var Scene = class _Scene {
|
|
|
3164
3322
|
this.dirty = true;
|
|
3165
3323
|
if (this._dirtyTracking && source) this.recordDirtyReason(source);
|
|
3166
3324
|
}
|
|
3325
|
+
/**
|
|
3326
|
+
* Increments whenever the tree's shape changes: add, remove or reparent.
|
|
3327
|
+
*
|
|
3328
|
+
* Already maintained for the resident WASM transform store (see
|
|
3329
|
+
* {@link markStructureChanged}, called from `Entity.add`/`remove`), and exposed
|
|
3330
|
+
* here because a cache of the tree's shape — a DevTools tree model, a serialized
|
|
3331
|
+
* snapshot — is valid exactly as long as this value is unchanged. Comparing it is
|
|
3332
|
+
* O(1) against re-walking the tree, which is what it replaces: DevTools rebuilt
|
|
3333
|
+
* both trees on a fixed 500 ms interval, a constant cost proportional to entity
|
|
3334
|
+
* count, purely because it had no way to ask whether the shape had changed.
|
|
3335
|
+
*
|
|
3336
|
+
* Property changes do NOT bump it. Moving or restyling an entity leaves the
|
|
3337
|
+
* shape intact, so a consumer that also cares about values must read those
|
|
3338
|
+
* directly rather than rebuilding a tree.
|
|
3339
|
+
*/
|
|
3340
|
+
get structureVersion() {
|
|
3341
|
+
return this._structureVersion;
|
|
3342
|
+
}
|
|
3167
3343
|
/**
|
|
3168
3344
|
* Record who marked the scene dirty and why.
|
|
3169
3345
|
*
|
|
@@ -3286,6 +3462,11 @@ var Scene = class _Scene {
|
|
|
3286
3462
|
if (node.isDOMPortal) {
|
|
3287
3463
|
return;
|
|
3288
3464
|
}
|
|
3465
|
+
if (node.a11yHidden) {
|
|
3466
|
+
this.pruneA11ySubtree(node);
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
const nodeStart = this._phaseTiming ? performance.now() : 0;
|
|
3289
3470
|
if (this.shouldProjectA11y(node)) {
|
|
3290
3471
|
let el = this.a11yElements.get(node.id);
|
|
3291
3472
|
const attrs = node.getA11yAttributes();
|
|
@@ -3628,7 +3809,14 @@ var Scene = class _Scene {
|
|
|
3628
3809
|
if (el.style.display !== display) el.style.display = display;
|
|
3629
3810
|
}
|
|
3630
3811
|
}
|
|
3631
|
-
this.
|
|
3812
|
+
if (this._phaseTiming) {
|
|
3813
|
+
const projectionStart = performance.now();
|
|
3814
|
+
this.syncContentProjection(node);
|
|
3815
|
+
this._recordPhase("contentProjection", performance.now() - projectionStart);
|
|
3816
|
+
this._recordPhase("a11yNodes", projectionStart - nodeStart);
|
|
3817
|
+
} else {
|
|
3818
|
+
this.syncContentProjection(node);
|
|
3819
|
+
}
|
|
3632
3820
|
for (const child of node.children) this.syncA11y(child);
|
|
3633
3821
|
if (node === this.root) {
|
|
3634
3822
|
for (const overlay of this.overlayRoot.children) this.syncA11y(overlay);
|
|
@@ -3742,7 +3930,9 @@ var Scene = class _Scene {
|
|
|
3742
3930
|
this.clearContentGridState(node.id, el);
|
|
3743
3931
|
}
|
|
3744
3932
|
if (projection.grid) {
|
|
3933
|
+
const gridSyncStart = this._phaseTiming ? performance.now() : 0;
|
|
3745
3934
|
this.syncContentGridProjection(node, el, projection, projection.grid);
|
|
3935
|
+
if (this._phaseTiming) this._recordPhase("gridSync", performance.now() - gridSyncStart);
|
|
3746
3936
|
} else if (lines && lines.length > 0) {
|
|
3747
3937
|
const signature = JSON.stringify({
|
|
3748
3938
|
lines,
|
|
@@ -3848,16 +4038,33 @@ var Scene = class _Scene {
|
|
|
3848
4038
|
const signature = `${grid.revision}`;
|
|
3849
4039
|
if (el.dataset.vectoContentGrid !== signature) {
|
|
3850
4040
|
const materializeStart = typeof performance !== "undefined" ? performance.now() : 0;
|
|
3851
|
-
this.clearContentGridState(node.id, el);
|
|
3852
|
-
el.replaceChildren();
|
|
4041
|
+
this.clearContentGridState(node.id, el, false);
|
|
3853
4042
|
const projectionLines = projection.lines ?? [];
|
|
4043
|
+
const selectionLine = this.contentGridSelectionLine(el);
|
|
4044
|
+
let rebuiltSelectionLine = false;
|
|
4045
|
+
const existingLines = el.children;
|
|
3854
4046
|
for (let lineIndex = 0; lineIndex < grid.lines.length; lineIndex++) {
|
|
3855
4047
|
const gridLine = grid.lines[lineIndex];
|
|
3856
4048
|
const projectedLine = projectionLines[lineIndex];
|
|
3857
4049
|
const lineHeight = projectedLine?.lineHeight ?? grid.lineHeight;
|
|
3858
4050
|
const baseline = projectedLine?.baseline ?? grid.baseline;
|
|
3859
4051
|
const lineFont = projectedLine?.font ?? grid.font;
|
|
4052
|
+
const lineSignature = contentGridLineSignature(
|
|
4053
|
+
grid,
|
|
4054
|
+
gridLine,
|
|
4055
|
+
projectedLine,
|
|
4056
|
+
lineHeight,
|
|
4057
|
+
baseline,
|
|
4058
|
+
lineFont,
|
|
4059
|
+
lineIndex === 0
|
|
4060
|
+
);
|
|
4061
|
+
const reusable = existingLines[lineIndex];
|
|
4062
|
+
if (reusable !== void 0 && reusable.dataset.vectoGridLineSig === lineSignature && reusable.dataset.vectoGridLine === `${lineIndex}`) {
|
|
4063
|
+
continue;
|
|
4064
|
+
}
|
|
4065
|
+
if (selectionLine !== null && selectionLine === lineIndex) rebuiltSelectionLine = true;
|
|
3860
4066
|
const lineElement = document.createElement("span");
|
|
4067
|
+
lineElement.dataset.vectoGridLineSig = lineSignature;
|
|
3861
4068
|
lineElement.dir = "ltr";
|
|
3862
4069
|
lineElement.dataset.vectoGridLine = `${lineIndex}`;
|
|
3863
4070
|
lineElement.style.position = "absolute";
|
|
@@ -3899,6 +4106,8 @@ var Scene = class _Scene {
|
|
|
3899
4106
|
cellElement.style.font = lineFont;
|
|
3900
4107
|
cellElement.style.lineHeight = `${lineHeight}px`;
|
|
3901
4108
|
cellElement.style.transformOrigin = "0 50%";
|
|
4109
|
+
cellElement.dataset.vectoGridFont = lineFont;
|
|
4110
|
+
cellElement.dataset.vectoGridLineHeight = `${lineHeight}px`;
|
|
3902
4111
|
lineElement.appendChild(cellElement);
|
|
3903
4112
|
logicalX += cell.advance;
|
|
3904
4113
|
}
|
|
@@ -3922,13 +4131,24 @@ var Scene = class _Scene {
|
|
|
3922
4131
|
lineElement.appendChild(marker);
|
|
3923
4132
|
}
|
|
3924
4133
|
}
|
|
3925
|
-
el.
|
|
4134
|
+
const occupant = el.children[lineIndex];
|
|
4135
|
+
if (occupant) el.replaceChild(lineElement, occupant);
|
|
4136
|
+
else el.appendChild(lineElement);
|
|
3926
4137
|
}
|
|
4138
|
+
while (el.children.length > grid.lines.length) {
|
|
4139
|
+
if (selectionLine !== null && selectionLine >= grid.lines.length) {
|
|
4140
|
+
rebuiltSelectionLine = true;
|
|
4141
|
+
}
|
|
4142
|
+
el.lastElementChild?.remove();
|
|
4143
|
+
}
|
|
4144
|
+
if (rebuiltSelectionLine) this.releaseContentSelectionForRebuild(el);
|
|
3927
4145
|
el.dataset.vectoProjectionLines = signature;
|
|
3928
4146
|
el.dataset.vectoContentGrid = signature;
|
|
3929
4147
|
el.dataset.vectoGridCarriers = `${el.querySelectorAll("[data-vecto-grid-cell]").length}`;
|
|
3930
4148
|
if (typeof performance !== "undefined") {
|
|
3931
|
-
|
|
4149
|
+
const materializeMs = performance.now() - materializeStart;
|
|
4150
|
+
el.dataset.vectoGridMaterializeMs = `${materializeMs}`;
|
|
4151
|
+
if (this._phaseTiming) this._recordPhase("gridMaterialize", materializeMs);
|
|
3932
4152
|
}
|
|
3933
4153
|
delete el.dataset.vectoGridCalibration;
|
|
3934
4154
|
delete el.dataset.vectoGridReady;
|
|
@@ -3936,7 +4156,11 @@ var Scene = class _Scene {
|
|
|
3936
4156
|
const pageScaleX = this.getContentMetricScaleX();
|
|
3937
4157
|
const calibrationKey = `${signature}:${this.contentFontEpoch}:${pageScaleX.toFixed(4)}`;
|
|
3938
4158
|
if (el.dataset.vectoGridCalibration !== calibrationKey) {
|
|
4159
|
+
const calibStart = this._phaseTiming ? performance.now() : 0;
|
|
3939
4160
|
this.scheduleContentGridCalibration(node.id, el, calibrationKey, pageScaleX);
|
|
4161
|
+
if (this._phaseTiming) {
|
|
4162
|
+
this._recordPhase("gridCalibrateSchedule", performance.now() - calibStart);
|
|
4163
|
+
}
|
|
3940
4164
|
}
|
|
3941
4165
|
}
|
|
3942
4166
|
getContentMetricScaleX() {
|
|
@@ -3954,6 +4178,27 @@ var Scene = class _Scene {
|
|
|
3954
4178
|
scheduleContentGridCalibration(entityId, el, calibrationKey, pageScaleX) {
|
|
3955
4179
|
if (typeof requestAnimationFrame !== "function") return;
|
|
3956
4180
|
if (el.dataset.vectoGridCalibrationPending === calibrationKey) return;
|
|
4181
|
+
const stamp = `${this.contentFontEpoch}:${pageScaleX.toFixed(4)}`;
|
|
4182
|
+
if (this.contentGridCalibrationStamp !== stamp) {
|
|
4183
|
+
this.contentGridCalibrationStamp = stamp;
|
|
4184
|
+
this.contentGridCalibrationGeneration++;
|
|
4185
|
+
}
|
|
4186
|
+
const generation = `${this.contentGridCalibrationGeneration}`;
|
|
4187
|
+
const pendingCells = el.querySelectorAll(
|
|
4188
|
+
`[data-vecto-grid-cell]:not([data-vecto-grid-calib="${generation}"])`
|
|
4189
|
+
);
|
|
4190
|
+
if (pendingCells.length === 0) {
|
|
4191
|
+
el.dataset.vectoGridCalibrationSamples = "0";
|
|
4192
|
+
delete el.dataset.vectoGridCalibrationPending;
|
|
4193
|
+
const readyFrame = requestAnimationFrame(() => {
|
|
4194
|
+
this.contentGridCalibrationFrames.delete(entityId);
|
|
4195
|
+
if (!el.isConnected) return;
|
|
4196
|
+
el.dataset.vectoGridCalibration = calibrationKey;
|
|
4197
|
+
el.dataset.vectoGridReady = "true";
|
|
4198
|
+
});
|
|
4199
|
+
this.contentGridCalibrationFrames.set(entityId, readyFrame);
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
3957
4202
|
const previous = this.contentGridCalibrationFrames.get(entityId);
|
|
3958
4203
|
if (previous !== void 0 && typeof cancelAnimationFrame === "function") {
|
|
3959
4204
|
cancelAnimationFrame(previous);
|
|
@@ -3984,18 +4229,22 @@ var Scene = class _Scene {
|
|
|
3984
4229
|
probe.append(probeOrigin, probeX);
|
|
3985
4230
|
const measurements = [];
|
|
3986
4231
|
const measurementsByKey = /* @__PURE__ */ new Map();
|
|
3987
|
-
|
|
4232
|
+
const scanStart = this._phaseTiming ? performance.now() : 0;
|
|
4233
|
+
for (const target of pendingCells) {
|
|
3988
4234
|
const sourceLength = Number(target.dataset.vectoGridSourceLength ?? 0);
|
|
3989
4235
|
const targetWidth = Number(target.dataset.vectoGridAdvance ?? 0);
|
|
3990
|
-
if (sourceLength <= 0 || targetWidth <= 0)
|
|
4236
|
+
if (sourceLength <= 0 || targetWidth <= 0) {
|
|
4237
|
+
target.dataset.vectoGridCalib = generation;
|
|
4238
|
+
continue;
|
|
4239
|
+
}
|
|
3991
4240
|
const sourceText = target.textContent?.slice(0, sourceLength) ?? "";
|
|
3992
|
-
if (!sourceText)
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
]);
|
|
4241
|
+
if (!sourceText) {
|
|
4242
|
+
target.dataset.vectoGridCalib = generation;
|
|
4243
|
+
continue;
|
|
4244
|
+
}
|
|
4245
|
+
const cellFont = target.dataset.vectoGridFont ?? "";
|
|
4246
|
+
const cellLineHeight = target.dataset.vectoGridLineHeight ?? "";
|
|
4247
|
+
const measurementKey = JSON.stringify([cellFont, cellLineHeight, targetWidth, sourceText]);
|
|
3999
4248
|
const shared = measurementsByKey.get(measurementKey);
|
|
4000
4249
|
if (shared) {
|
|
4001
4250
|
shared.targets.push(target);
|
|
@@ -4007,8 +4256,8 @@ var Scene = class _Scene {
|
|
|
4007
4256
|
carrier.style.left = "0";
|
|
4008
4257
|
carrier.style.top = "0";
|
|
4009
4258
|
carrier.style.whiteSpace = "pre";
|
|
4010
|
-
carrier.style.font =
|
|
4011
|
-
carrier.style.lineHeight =
|
|
4259
|
+
carrier.style.font = cellFont;
|
|
4260
|
+
carrier.style.lineHeight = cellLineHeight;
|
|
4012
4261
|
carrier.style.fontVariantLigatures = "none";
|
|
4013
4262
|
carrier.style.fontKerning = "none";
|
|
4014
4263
|
const source = document.createTextNode(sourceText);
|
|
@@ -4023,7 +4272,17 @@ var Scene = class _Scene {
|
|
|
4023
4272
|
measurements.push(measurement);
|
|
4024
4273
|
measurementsByKey.set(measurementKey, measurement);
|
|
4025
4274
|
}
|
|
4275
|
+
if (this._phaseTiming) this._recordPhase("calibScan", performance.now() - scanStart);
|
|
4276
|
+
if (measurements.length === 0) {
|
|
4277
|
+
el.dataset.vectoGridCalibration = calibrationKey;
|
|
4278
|
+
el.dataset.vectoGridReady = "true";
|
|
4279
|
+
el.dataset.vectoGridCalibrationSamples = "0";
|
|
4280
|
+
delete el.dataset.vectoGridCalibrationPending;
|
|
4281
|
+
return;
|
|
4282
|
+
}
|
|
4283
|
+
const appendStart = this._phaseTiming ? performance.now() : 0;
|
|
4026
4284
|
(this.a11yRoot ?? document.body ?? document.documentElement).appendChild(probe);
|
|
4285
|
+
if (this._phaseTiming) this._recordPhase("calibProbeBuild", performance.now() - appendStart);
|
|
4027
4286
|
el.dataset.vectoGridCalibrationSamples = `${measurements.length}`;
|
|
4028
4287
|
this.contentGridCalibrationProbes.set(entityId, probe);
|
|
4029
4288
|
el.dataset.vectoGridCalibrationPending = calibrationKey;
|
|
@@ -4067,6 +4326,7 @@ var Scene = class _Scene {
|
|
|
4067
4326
|
}
|
|
4068
4327
|
for (const { element, scale } of updates) {
|
|
4069
4328
|
element.style.transform = Math.abs(scale - 1) <= 1e-3 ? "" : `scaleX(${scale})`;
|
|
4329
|
+
element.dataset.vectoGridCalib = generation;
|
|
4070
4330
|
}
|
|
4071
4331
|
el.dataset.vectoGridCalibration = calibrationKey;
|
|
4072
4332
|
el.dataset.vectoGridReady = "true";
|
|
@@ -4372,7 +4632,9 @@ var Scene = class _Scene {
|
|
|
4372
4632
|
}
|
|
4373
4633
|
this._lastRenderTick = time;
|
|
4374
4634
|
this._lastDt = dt;
|
|
4635
|
+
const phaseClock = this._phaseTiming ? performance.now() : 0;
|
|
4375
4636
|
this.render(this.renderer, dt, time);
|
|
4637
|
+
if (this._phaseTiming) this._recordPhase("render", performance.now() - phaseClock);
|
|
4376
4638
|
this._lastFrameMs = (typeof performance !== "undefined" ? performance.now() : time) - now;
|
|
4377
4639
|
this._renderedFrames++;
|
|
4378
4640
|
const hasActiveAnimation = this.frameHadAnimation;
|
|
@@ -4382,9 +4644,13 @@ var Scene = class _Scene {
|
|
|
4382
4644
|
if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
|
|
4383
4645
|
this.lastA11ySync = time;
|
|
4384
4646
|
if (hasInteractive || wantsContentSync) {
|
|
4647
|
+
const t0 = this._phaseTiming ? performance.now() : 0;
|
|
4385
4648
|
this.syncA11y(this.root);
|
|
4649
|
+
if (this._phaseTiming) this._recordPhase("a11ySync", performance.now() - t0);
|
|
4386
4650
|
}
|
|
4651
|
+
const t1 = this._phaseTiming ? performance.now() : 0;
|
|
4387
4652
|
this.enforceA11yDomOrder();
|
|
4653
|
+
if (this._phaseTiming) this._recordPhase("a11yOrder", performance.now() - t1);
|
|
4388
4654
|
this.a11yPendingSyncAfterAnimation = hasActiveAnimation;
|
|
4389
4655
|
} else if (hasActiveAnimation) {
|
|
4390
4656
|
this.a11yPendingSyncAfterAnimation = true;
|
|
@@ -4558,7 +4824,9 @@ var Scene = class _Scene {
|
|
|
4558
4824
|
updateWalk(this.root);
|
|
4559
4825
|
for (const overlay of this.overlayRoot.children) updateWalk(overlay);
|
|
4560
4826
|
}
|
|
4827
|
+
const wasmT0 = this._phaseTiming ? performance.now() : 0;
|
|
4561
4828
|
const wasmWorld = wasmMain ? this._syncWasmStore() : null;
|
|
4829
|
+
if (this._phaseTiming) this._recordPhase("transform", performance.now() - wasmT0);
|
|
4562
4830
|
const wasmSlotEntity = this._slotEntity;
|
|
4563
4831
|
const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
|
|
4564
4832
|
if (isMainRenderer && !wasmMain) {
|
|
@@ -4688,7 +4956,13 @@ var Scene = class _Scene {
|
|
|
4688
4956
|
);
|
|
4689
4957
|
}
|
|
4690
4958
|
} else {
|
|
4691
|
-
|
|
4959
|
+
if (this._phaseTiming) {
|
|
4960
|
+
const t0 = performance.now();
|
|
4961
|
+
node.render(renderer);
|
|
4962
|
+
this._recordPhase("entityPaint", performance.now() - t0);
|
|
4963
|
+
} else {
|
|
4964
|
+
node.render(renderer);
|
|
4965
|
+
}
|
|
4692
4966
|
}
|
|
4693
4967
|
}
|
|
4694
4968
|
if (node.clipChildren) {
|
|
@@ -4700,20 +4974,24 @@ var Scene = class _Scene {
|
|
|
4700
4974
|
renderer.flush();
|
|
4701
4975
|
renderer.restore();
|
|
4702
4976
|
};
|
|
4977
|
+
const drawT0 = this._phaseTiming ? performance.now() : 0;
|
|
4703
4978
|
renderNode(this.root, 1, 0, 0, 1, 0, 0, 1);
|
|
4704
4979
|
for (const overlay of this.overlayRoot.children) {
|
|
4705
4980
|
renderNode(overlay, 1, 0, 0, 1, 0, 0, 1);
|
|
4706
4981
|
}
|
|
4982
|
+
if (this._phaseTiming) this._recordPhase("drawWalk", performance.now() - drawT0);
|
|
4707
4983
|
if (isMainRenderer) {
|
|
4708
4984
|
this.frameHadAnimation = walkHadAnimation;
|
|
4709
4985
|
this.frameHadInteractive = walkHadInteractive;
|
|
4710
4986
|
this.reconcilePortals();
|
|
4711
4987
|
}
|
|
4988
|
+
const flushT0 = this._phaseTiming ? performance.now() : 0;
|
|
4712
4989
|
renderer.flush();
|
|
4713
4990
|
if (isMainRenderer) {
|
|
4714
4991
|
this.pointRenderer?.flush();
|
|
4715
4992
|
}
|
|
4716
4993
|
renderer.present?.();
|
|
4994
|
+
if (this._phaseTiming) this._recordPhase("flush", performance.now() - flushT0);
|
|
4717
4995
|
if (this._devActive) {
|
|
4718
4996
|
this._devFrameCount++;
|
|
4719
4997
|
this._devRunChecks();
|
|
@@ -4989,6 +5267,41 @@ function intersectBounds(a, b) {
|
|
|
4989
5267
|
function pointInBounds(b, x, y) {
|
|
4990
5268
|
return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
|
|
4991
5269
|
}
|
|
5270
|
+
function contentGridLineSignature(grid, line, projected, lineHeight, baseline, font, isFirstLine) {
|
|
5271
|
+
const parts = [
|
|
5272
|
+
// Line box: position, size, and the font that resolves its baseline.
|
|
5273
|
+
`${projected?.x ?? 0}`,
|
|
5274
|
+
`${projected?.y ?? ""}`,
|
|
5275
|
+
`${lineHeight}`,
|
|
5276
|
+
`${baseline}`,
|
|
5277
|
+
font,
|
|
5278
|
+
`${line.width}`,
|
|
5279
|
+
// The trailing hard break belongs to this line and lands in the DOM text.
|
|
5280
|
+
grid.source.slice(line.sourceEnd, line.nextSourceStart),
|
|
5281
|
+
// The basis markers are appended only to line 0, so a line moving to or from
|
|
5282
|
+
// index 0 changes its DOM even when nothing else does.
|
|
5283
|
+
isFirstLine ? "1" : "0"
|
|
5284
|
+
];
|
|
5285
|
+
if (line.cells.length === 0) {
|
|
5286
|
+
parts.push("empty");
|
|
5287
|
+
} else {
|
|
5288
|
+
for (const cell of line.cells) {
|
|
5289
|
+
parts.push(
|
|
5290
|
+
`${cell.sourceStart}`,
|
|
5291
|
+
`${cell.sourceEnd}`,
|
|
5292
|
+
`${cell.x}`,
|
|
5293
|
+
`${cell.advance}`,
|
|
5294
|
+
`${cell.level}`,
|
|
5295
|
+
cell.sourceCaretOffsets.join("."),
|
|
5296
|
+
// Source text, not `cell.glyph`: the carrier holds the original characters
|
|
5297
|
+
// (the shaped glyph is the canvas's business), so a change in shaping alone
|
|
5298
|
+
// must not invalidate a carrier, and a change in source must.
|
|
5299
|
+
grid.source.slice(cell.sourceStart, cell.sourceEnd)
|
|
5300
|
+
);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
return parts.join("");
|
|
5304
|
+
}
|
|
4992
5305
|
|
|
4993
5306
|
// src/components/TextEntity.ts
|
|
4994
5307
|
import {
|
|
@@ -5757,6 +6070,7 @@ export {
|
|
|
5757
6070
|
ComputeParticleEntity,
|
|
5758
6071
|
DOMPortalEntity,
|
|
5759
6072
|
Entity,
|
|
6073
|
+
GlyphRasterAtlas,
|
|
5760
6074
|
GridTextEntity,
|
|
5761
6075
|
Group,
|
|
5762
6076
|
MSDFTextEntity,
|
|
@@ -111,6 +111,8 @@ export declare class CanvasRenderer implements IRenderer {
|
|
|
111
111
|
/** @inheritdoc */
|
|
112
112
|
drawImage(source: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
|
|
113
113
|
/** @inheritdoc */
|
|
114
|
+
drawImageRect(source: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
|
|
115
|
+
/** @inheritdoc */
|
|
114
116
|
fillCircle(cx: number, cy: number, radius: number, color: string, alpha?: number): void;
|
|
115
117
|
/** @inheritdoc */
|
|
116
118
|
flush(): void;
|