@vectojs/core 1.30.0 → 1.32.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-2WXZCPYQ.mjs → chunk-RWN3ITBD.mjs} +34 -0
- package/dist/{chunk-7PI7LVWW.js → chunk-TRT2TRCW.js} +55 -21
- package/dist/components/TextEntity.d.ts +3 -0
- package/dist/index.js +339 -196
- package/dist/index.mjs +158 -15
- package/dist/text/MSDFTextEntity.d.ts +3 -0
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +26 -0
- package/dist/tree/Scene.d.ts +153 -2
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
SVGEntity,
|
|
19
19
|
VectoJSEvent,
|
|
20
20
|
contentLineInHint
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-RWN3ITBD.mjs";
|
|
22
22
|
|
|
23
23
|
// src/tree/Scene.ts
|
|
24
24
|
import { SpringDriver, TweenDriver } from "@vectojs/animation";
|
|
@@ -1379,6 +1379,8 @@ var SCENE_OPTION_KEYS = [
|
|
|
1379
1379
|
"autoThrottle",
|
|
1380
1380
|
"contentProjection",
|
|
1381
1381
|
"contentProjectionMargin",
|
|
1382
|
+
"contentSemanticBudget",
|
|
1383
|
+
"contentSemanticMargin",
|
|
1382
1384
|
"debugA11y",
|
|
1383
1385
|
"disableWindowResize",
|
|
1384
1386
|
"maxDPR",
|
|
@@ -1431,6 +1433,7 @@ function parseInlinePx(value) {
|
|
|
1431
1433
|
const n = parseFloat(value);
|
|
1432
1434
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
1433
1435
|
}
|
|
1436
|
+
var DEFAULT_CONTENT_SEMANTIC_BUDGET = 256;
|
|
1434
1437
|
function projectionGridLineWindow(grid, projectionLines, band) {
|
|
1435
1438
|
const count = grid.lines.length;
|
|
1436
1439
|
const all = { start: 0, end: count, gated: false };
|
|
@@ -1956,6 +1959,15 @@ var Scene = class _Scene {
|
|
|
1956
1959
|
a11yElements = /* @__PURE__ */ new Map();
|
|
1957
1960
|
/** DOM nodes mirroring static text content, keyed by entity id. */
|
|
1958
1961
|
contentElements = /* @__PURE__ */ new Map();
|
|
1962
|
+
/**
|
|
1963
|
+
* What the last completed content-projection sync was built from, per entity.
|
|
1964
|
+
*
|
|
1965
|
+
* Compared at the top of {@link syncContentProjection} to skip a block whose
|
|
1966
|
+
* content AND geometry are both unchanged, before the O(glyphs) projection
|
|
1967
|
+
* build. Only populated for entities that opt in via
|
|
1968
|
+
* {@link Entity.getContentEpoch}. (carryctx CTX-0199)
|
|
1969
|
+
*/
|
|
1970
|
+
contentSyncState = /* @__PURE__ */ new Map();
|
|
1959
1971
|
/** Pending cold font-calibration frame per projected grid entity. */
|
|
1960
1972
|
contentGridCalibrationFrames = /* @__PURE__ */ new Map();
|
|
1961
1973
|
/** Detached, untransformed font probes used by the cold calibration pass. */
|
|
@@ -1989,9 +2001,49 @@ var Scene = class _Scene {
|
|
|
1989
2001
|
contentMetricScaleEpoch = -1;
|
|
1990
2002
|
contentMetricScaleX = 1;
|
|
1991
2003
|
contentProjectionEnabled = true;
|
|
1992
|
-
// Virtualization margin (px) for content
|
|
1993
|
-
// viewport height, resolved at sync time. `Infinity`
|
|
2004
|
+
// Virtualization margin (px) for the content-projection CARRIER band;
|
|
2005
|
+
// `undefined` → one viewport height, resolved at sync time. `Infinity` is
|
|
2006
|
+
// unsupported here: it unwindows every carrier, which is O(total glyphs).
|
|
1994
2007
|
contentProjectionMargin = void 0;
|
|
2008
|
+
// Virtualization margin (px) for the SEMANTIC tier — whether a block has
|
|
2009
|
+
// any projected DOM. `undefined` → falls back to contentProjectionMargin, so
|
|
2010
|
+
// the default keeps one gate. `Infinity` = every block keeps resident text.
|
|
2011
|
+
contentSemanticMargin = void 0;
|
|
2012
|
+
// How many coarse-tier blocks may be MATERIALIZED per sync, spreading the
|
|
2013
|
+
// resident tier's document-open cost across frames. `Infinity` = one
|
|
2014
|
+
// synchronous pass.
|
|
2015
|
+
contentSemanticBudget = DEFAULT_CONTENT_SEMANTIC_BUDGET;
|
|
2016
|
+
// Remaining materializations in the CURRENT sync. Reset at the start of each
|
|
2017
|
+
// a11y walk; decremented per coarse block that creates its element.
|
|
2018
|
+
contentSemanticBudgetLeft = 0;
|
|
2019
|
+
// Set when a sync deferred at least one block, so the scene knows to keep
|
|
2020
|
+
// drawing frames until the resident tier is complete. Without it a static
|
|
2021
|
+
// scene in `onDemand` mode would stop rendering with the document half
|
|
2022
|
+
// materialized and never finish.
|
|
2023
|
+
contentSemanticDeferred = false;
|
|
2024
|
+
/**
|
|
2025
|
+
* Per-sync memo of "does the document hold a selection at all".
|
|
2026
|
+
*
|
|
2027
|
+
* Reading ANY property of a `Selection` (`anchorNode`, `rangeCount`, `type`,
|
|
2028
|
+
* `isCollapsed`) forces a synchronous layout, because Blink validates the
|
|
2029
|
+
* selection against current box geometry before answering. Measured in real
|
|
2030
|
+
* Chrome against a 1000-carrier subtree with layout dirtied between reads:
|
|
2031
|
+
* `anchorNode` 0.5ms, `rangeCount` 0.4ms, `type` 0.5ms, `isCollapsed` 0.5ms —
|
|
2032
|
+
* all indistinguishable from `offsetHeight` (0.5ms), against a 0ms floor for
|
|
2033
|
+
* mutating without reading. So there is no cheap property to probe with; the
|
|
2034
|
+
* only way to avoid the layout is to not touch the object at all.
|
|
2035
|
+
*
|
|
2036
|
+
* Materializing a block rebuilds its carriers, which asks whether the rebuild
|
|
2037
|
+
* would destroy a selection. Once per block, that read cost a forced layout
|
|
2038
|
+
* over the whole (and growing) projection subtree, which is what made
|
|
2039
|
+
* per-block cost rise with resident count: profiled at 1973 forced layouts
|
|
2040
|
+
* totalling 633ms of an 847ms 1000-block drain (75%).
|
|
2041
|
+
*
|
|
2042
|
+
* A selection is a single document-wide object and a sync walk cannot yield to
|
|
2043
|
+
* the user, so its presence cannot change mid-walk. Resolving it once per walk
|
|
2044
|
+
* turns O(blocks) forced layouts into O(1). `null` = not yet resolved.
|
|
2045
|
+
*/
|
|
2046
|
+
contentSelectionPresentThisSync = null;
|
|
1995
2047
|
/**
|
|
1996
2048
|
* True while a text-selection drag that started on a projection's blank
|
|
1997
2049
|
* region (no text node under the press) is being driven manually — the
|
|
@@ -3027,6 +3079,8 @@ var Scene = class _Scene {
|
|
|
3027
3079
|
this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
|
|
3028
3080
|
this.contentProjectionEnabled = options.contentProjection ?? true;
|
|
3029
3081
|
this.contentProjectionMargin = options.contentProjectionMargin;
|
|
3082
|
+
this.contentSemanticMargin = options.contentSemanticMargin;
|
|
3083
|
+
this.contentSemanticBudget = options.contentSemanticBudget ?? DEFAULT_CONTENT_SEMANTIC_BUDGET;
|
|
3030
3084
|
this.readingDirection = options.readingDirection ?? "ltr";
|
|
3031
3085
|
this.renderMode = options.renderMode ?? "always";
|
|
3032
3086
|
this._devActive = _Scene._devModeDetected();
|
|
@@ -3308,12 +3362,31 @@ var Scene = class _Scene {
|
|
|
3308
3362
|
}
|
|
3309
3363
|
return null;
|
|
3310
3364
|
}
|
|
3365
|
+
/**
|
|
3366
|
+
* Does the document hold a selection right now, memoized for this sync walk?
|
|
3367
|
+
*
|
|
3368
|
+
* Pays one forced layout per walk instead of one per rebuilt element — see
|
|
3369
|
+
* {@link Scene.contentSelectionPresentThisSync} for the measurements. When the
|
|
3370
|
+
* answer is `false` no element can own a selection, so every per-element
|
|
3371
|
+
* ownership test can be skipped without touching the object.
|
|
3372
|
+
*/
|
|
3373
|
+
contentSelectionPresent() {
|
|
3374
|
+
if (this.contentSelectionPresentThisSync !== null) {
|
|
3375
|
+
return this.contentSelectionPresentThisSync;
|
|
3376
|
+
}
|
|
3377
|
+
const selection = typeof window !== "undefined" && typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
3378
|
+
const present = !!selection && (!!selection.anchorNode || !!selection.focusNode);
|
|
3379
|
+
this.contentSelectionPresentThisSync = present;
|
|
3380
|
+
return present;
|
|
3381
|
+
}
|
|
3311
3382
|
releaseContentSelectionForRebuild(el) {
|
|
3383
|
+
if (!this.contentSelectionAnchor && !this.contentSelectionPresent()) return;
|
|
3312
3384
|
const selection = typeof window !== "undefined" && typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
3313
3385
|
const ownsSelection = this.contentSelectionAnchor && el.contains(this.contentSelectionAnchor.node) || (selection?.anchorNode ? el.contains(selection.anchorNode) : false) || (selection?.focusNode ? el.contains(selection.focusNode) : false);
|
|
3314
3386
|
if (!ownsSelection) return;
|
|
3315
3387
|
this.endContentSelectionDrag();
|
|
3316
3388
|
selection?.removeAllRanges();
|
|
3389
|
+
this.contentSelectionPresentThisSync = null;
|
|
3317
3390
|
}
|
|
3318
3391
|
/**
|
|
3319
3392
|
* Rebuild a content-projection element's DOM (`rebuild`) while preserving a
|
|
@@ -3331,6 +3404,10 @@ var Scene = class _Scene {
|
|
|
3331
3404
|
* restore against).
|
|
3332
3405
|
*/
|
|
3333
3406
|
preserveContentSelectionAcrossRebuild(el, rebuild) {
|
|
3407
|
+
if (!this.contentSelectionAnchor && !this.contentSelectionPresent()) {
|
|
3408
|
+
rebuild();
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3334
3411
|
const selection = typeof window !== "undefined" && typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
3335
3412
|
const owns = !!selection && !this.blankRegionSelectionDrag && ((selection.anchorNode ? el.contains(selection.anchorNode) : false) || (selection.focusNode ? el.contains(selection.focusNode) : false));
|
|
3336
3413
|
if (!owns || !selection.anchorNode || !selection.focusNode) {
|
|
@@ -3449,6 +3526,7 @@ var Scene = class _Scene {
|
|
|
3449
3526
|
this.clearContentGridState(node.id, contentEl);
|
|
3450
3527
|
contentEl.remove();
|
|
3451
3528
|
this.contentElements.delete(node.id);
|
|
3529
|
+
this.contentSyncState.delete(node.id);
|
|
3452
3530
|
this.a11yNeedsReorder = true;
|
|
3453
3531
|
}
|
|
3454
3532
|
const el = this.a11yElements.get(node.id);
|
|
@@ -3593,6 +3671,7 @@ var Scene = class _Scene {
|
|
|
3593
3671
|
this.a11yElements.clear();
|
|
3594
3672
|
for (const el of this.contentElements.values()) el.remove();
|
|
3595
3673
|
this.contentElements.clear();
|
|
3674
|
+
this.contentSyncState.clear();
|
|
3596
3675
|
if (typeof cancelAnimationFrame === "function") {
|
|
3597
3676
|
for (const frame of this.contentGridCalibrationFrames.values()) {
|
|
3598
3677
|
cancelAnimationFrame(frame);
|
|
@@ -4024,6 +4103,11 @@ var Scene = class _Scene {
|
|
|
4024
4103
|
}
|
|
4025
4104
|
syncA11y(node, container = null) {
|
|
4026
4105
|
if (!this.a11yRoot) return;
|
|
4106
|
+
if (node === this.root) {
|
|
4107
|
+
this.contentSemanticBudgetLeft = this.contentSemanticBudget;
|
|
4108
|
+
this.contentSemanticDeferred = false;
|
|
4109
|
+
this.contentSelectionPresentThisSync = null;
|
|
4110
|
+
}
|
|
4027
4111
|
if (node.isDOMPortal) {
|
|
4028
4112
|
return;
|
|
4029
4113
|
}
|
|
@@ -4442,7 +4526,9 @@ var Scene = class _Scene {
|
|
|
4442
4526
|
* transparent DOM node positioned over the drawn glyphs. Runs on the a11y
|
|
4443
4527
|
* sync cadence; all writes are dirty-checked. Off-viewport projections are
|
|
4444
4528
|
* hidden (`display: none`) so text-heavy scenes only materialize what is
|
|
4445
|
-
* visible to the browser's text machinery anyway
|
|
4529
|
+
* visible to the browser's text machinery anyway — except in the coarse
|
|
4530
|
+
* (resident) tier, which stays displayed because hiding it would make its text
|
|
4531
|
+
* unfindable and remove it from the accessibility tree, defeating the tier.
|
|
4446
4532
|
*/
|
|
4447
4533
|
/**
|
|
4448
4534
|
* Whether `node`'s world-space box, expanded by `margin` px on every side,
|
|
@@ -4451,8 +4537,15 @@ var Scene = class _Scene {
|
|
|
4451
4537
|
* at `margin = contentProjectionMargin`) and for the exact `display:none`
|
|
4452
4538
|
* visibility test (`margin = 0`). Boundless nodes (width/height 0) opt out of
|
|
4453
4539
|
* culling and always count as visible, matching the legacy behavior.
|
|
4540
|
+
*
|
|
4541
|
+
* `viewportOnly` skips the `clipChildren` ancestor walk, answering the narrower
|
|
4542
|
+
* question "does this box overlap the viewport at all". The coarse content tier
|
|
4543
|
+
* needs the two apart: text that is merely off-viewport is clipped by
|
|
4544
|
+
* `a11yRoot`'s own `overflow: hidden` and can safely stay displayed, while text
|
|
4545
|
+
* rejected by an ancestor clip box that itself overlaps the viewport would sit
|
|
4546
|
+
* transparently on top of whatever is really drawn there.
|
|
4454
4547
|
*/
|
|
4455
|
-
projectionBoxVisible(node, tf, margin) {
|
|
4548
|
+
projectionBoxVisible(node, tf, margin, viewportOnly = false) {
|
|
4456
4549
|
if (!(node.width > 0 && node.height > 0)) return true;
|
|
4457
4550
|
const { a, b, c, d, e, f } = tf;
|
|
4458
4551
|
const worldCorners = [];
|
|
@@ -4474,6 +4567,7 @@ var Scene = class _Scene {
|
|
|
4474
4567
|
if (!(maxX >= -margin && minX <= this.width + margin && maxY >= -margin && minY <= this.height + margin)) {
|
|
4475
4568
|
return false;
|
|
4476
4569
|
}
|
|
4570
|
+
if (viewportOnly) return true;
|
|
4477
4571
|
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
4478
4572
|
if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
|
|
4479
4573
|
let localMinX = Infinity;
|
|
@@ -4555,14 +4649,31 @@ var Scene = class _Scene {
|
|
|
4555
4649
|
this.contentElements.delete(node.id);
|
|
4556
4650
|
this.a11yNeedsReorder = true;
|
|
4557
4651
|
}
|
|
4652
|
+
this.contentSyncState.delete(node.id);
|
|
4558
4653
|
};
|
|
4559
4654
|
const worldTf = node.getWorldTransform();
|
|
4560
|
-
const
|
|
4561
|
-
|
|
4655
|
+
const interactionMargin = this.contentProjectionMargin ?? this.height;
|
|
4656
|
+
const semanticMargin = this.contentSemanticMargin ?? interactionMargin;
|
|
4657
|
+
if (Number.isFinite(semanticMargin) && !this.projectionBoxVisible(node, worldTf, semanticMargin)) {
|
|
4562
4658
|
releaseProjectionEl();
|
|
4563
4659
|
return;
|
|
4564
4660
|
}
|
|
4565
|
-
const
|
|
4661
|
+
const inInteractionBand = !Number.isFinite(interactionMargin) || this.projectionBoxVisible(node, worldTf, interactionMargin);
|
|
4662
|
+
const tier = inInteractionBand ? "fine" : "coarse";
|
|
4663
|
+
const lineBand = tier === "coarse" ? null : Number.isFinite(interactionMargin) ? this.projectionVisibleLocalYBand(node, worldTf, interactionMargin) : null;
|
|
4664
|
+
const visible = this.projectionBoxVisible(node, worldTf, 0);
|
|
4665
|
+
const epoch = node.getContentEpoch();
|
|
4666
|
+
const prior = this.contentSyncState.get(node.id);
|
|
4667
|
+
if (epoch !== null && el && prior !== void 0) {
|
|
4668
|
+
const { a: a2, b: b2, c: c2, d: d2, e: e2, f: f2 } = worldTf;
|
|
4669
|
+
if (prior.epoch === epoch && prior.fontEpoch === this.contentFontEpoch && prior.a === a2 && prior.b === b2 && prior.c === c2 && prior.d === d2 && prior.e === e2 && prior.f === f2 && prior.tier === tier && prior.hasBand === (lineBand !== null) && (lineBand === null || prior.bandMin === lineBand.minY && prior.bandMax === lineBand.maxY) && prior.visible === visible && prior.width === node.width && prior.height === node.height && prior.interactive === node.interactive) {
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
if (tier === "coarse" && !el && this.contentSemanticBudgetLeft <= 0) {
|
|
4674
|
+
this.contentSemanticDeferred = true;
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4566
4677
|
const projection = node.getContentProjection(
|
|
4567
4678
|
lineBand ? { minY: lineBand.minY, maxY: lineBand.maxY } : void 0
|
|
4568
4679
|
);
|
|
@@ -4571,6 +4682,7 @@ var Scene = class _Scene {
|
|
|
4571
4682
|
return;
|
|
4572
4683
|
}
|
|
4573
4684
|
if (!el) {
|
|
4685
|
+
if (tier === "coarse") this.contentSemanticBudgetLeft--;
|
|
4574
4686
|
el = document.createElement("div");
|
|
4575
4687
|
el.setAttribute("data-vecto-content", node.id);
|
|
4576
4688
|
const s = el.style;
|
|
@@ -4595,14 +4707,15 @@ var Scene = class _Scene {
|
|
|
4595
4707
|
this.a11yNeedsReorder = true;
|
|
4596
4708
|
}
|
|
4597
4709
|
const lines = projection.lines;
|
|
4598
|
-
|
|
4710
|
+
const useCarriers = tier === "fine";
|
|
4711
|
+
if ((!projection.grid || !useCarriers) && el.dataset.vectoContentGrid !== void 0) {
|
|
4599
4712
|
this.clearContentGridState(node.id, el);
|
|
4600
4713
|
}
|
|
4601
|
-
if (projection.grid) {
|
|
4714
|
+
if (projection.grid && useCarriers) {
|
|
4602
4715
|
const gridSyncStart = this._phaseTiming ? performance.now() : 0;
|
|
4603
4716
|
this.syncContentGridProjection(node, el, projection, projection.grid, lineBand);
|
|
4604
4717
|
if (this._phaseTiming) this._recordPhase("gridSync", performance.now() - gridSyncStart);
|
|
4605
|
-
} else if (lines && lines.length > 0) {
|
|
4718
|
+
} else if (useCarriers && lines && lines.length > 0) {
|
|
4606
4719
|
const lineWindow = projectionLineWindow(lines, lineBand, projection.lineHeight ?? 16);
|
|
4607
4720
|
const signature = JSON.stringify({
|
|
4608
4721
|
lines,
|
|
@@ -4665,11 +4778,13 @@ var Scene = class _Scene {
|
|
|
4665
4778
|
}
|
|
4666
4779
|
}
|
|
4667
4780
|
} else {
|
|
4668
|
-
|
|
4781
|
+
const demoted = tier === "coarse" && el.children.length > 0;
|
|
4782
|
+
if (el.textContent !== projection.text || demoted) {
|
|
4669
4783
|
this.releaseContentSelectionForRebuild(el);
|
|
4670
4784
|
el.textContent = projection.text;
|
|
4671
4785
|
}
|
|
4672
4786
|
delete el.dataset.vectoProjectionLines;
|
|
4787
|
+
if (tier === "coarse") delete el.dataset.vectoProjectionWindow;
|
|
4673
4788
|
}
|
|
4674
4789
|
const font = projection.font ?? "";
|
|
4675
4790
|
if (el.style.font !== font) el.style.font = font;
|
|
@@ -4702,9 +4817,30 @@ var Scene = class _Scene {
|
|
|
4702
4817
|
if (node.width > 0) el.style.width = `${node.width}px`;
|
|
4703
4818
|
if (node.height > 0) el.style.height = `${node.height}px`;
|
|
4704
4819
|
el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
|
|
4705
|
-
const
|
|
4706
|
-
const display = visible ? "" : "none";
|
|
4820
|
+
const residentTier = semanticMargin > interactionMargin;
|
|
4821
|
+
const display = visible || residentTier && !this.projectionBoxVisible(node, worldTf, 0, true) ? "" : "none";
|
|
4707
4822
|
if (el.style.display !== display) el.style.display = display;
|
|
4823
|
+
if (epoch !== null) {
|
|
4824
|
+
const { a: a2, b: b2, c: c2, d: d2, e: e2, f: f2 } = worldTf;
|
|
4825
|
+
const next = prior ?? {};
|
|
4826
|
+
next.epoch = epoch;
|
|
4827
|
+
next.fontEpoch = this.contentFontEpoch;
|
|
4828
|
+
next.a = a2;
|
|
4829
|
+
next.b = b2;
|
|
4830
|
+
next.c = c2;
|
|
4831
|
+
next.d = d2;
|
|
4832
|
+
next.e = e2;
|
|
4833
|
+
next.f = f2;
|
|
4834
|
+
next.tier = tier;
|
|
4835
|
+
next.hasBand = lineBand !== null;
|
|
4836
|
+
next.bandMin = lineBand?.minY ?? 0;
|
|
4837
|
+
next.bandMax = lineBand?.maxY ?? 0;
|
|
4838
|
+
next.visible = visible;
|
|
4839
|
+
next.width = node.width;
|
|
4840
|
+
next.height = node.height;
|
|
4841
|
+
next.interactive = node.interactive;
|
|
4842
|
+
if (prior === void 0) this.contentSyncState.set(node.id, next);
|
|
4843
|
+
}
|
|
4708
4844
|
}
|
|
4709
4845
|
/**
|
|
4710
4846
|
* Materialize a prepared grid in logical source order while positioning each
|
|
@@ -5325,7 +5461,7 @@ var Scene = class _Scene {
|
|
|
5325
5461
|
if (!this.isRunning) return;
|
|
5326
5462
|
if (!this._canvasOnScreen) return;
|
|
5327
5463
|
let cap = this.effectiveMaxFPS();
|
|
5328
|
-
const isIdle = !this.dirty && !this.frameHadAnimation;
|
|
5464
|
+
const isIdle = !this.dirty && !this.frameHadAnimation && !this.contentSemanticDeferred;
|
|
5329
5465
|
if (isIdle && this.autoThrottle && this.renderMode === "always" && this.maxFPS > 0) {
|
|
5330
5466
|
cap = Math.min(cap, 2);
|
|
5331
5467
|
}
|
|
@@ -6107,6 +6243,8 @@ var TextEntity = class extends Entity {
|
|
|
6107
6243
|
hoveredFillStyle = "#ffffff";
|
|
6108
6244
|
lineWidth = 1;
|
|
6109
6245
|
isHovered = false;
|
|
6246
|
+
/** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
|
|
6247
|
+
contentEpoch = 0;
|
|
6110
6248
|
constructor(text, atlas, maxWidth, fontSize = 24) {
|
|
6111
6249
|
super();
|
|
6112
6250
|
this.text = text;
|
|
@@ -6127,6 +6265,9 @@ var TextEntity = class extends Entity {
|
|
|
6127
6265
|
if (!this.text) return null;
|
|
6128
6266
|
return { text: this.text, font: `${this.fontSize}px sans-serif` };
|
|
6129
6267
|
}
|
|
6268
|
+
getContentEpoch() {
|
|
6269
|
+
return this.contentEpoch;
|
|
6270
|
+
}
|
|
6130
6271
|
/**
|
|
6131
6272
|
* Replace the text content. Runs the **cold** measurement pass (re-segment +
|
|
6132
6273
|
* re-measure) since the glyphs changed, then re-lays out.
|
|
@@ -6173,6 +6314,7 @@ var TextEntity = class extends Entity {
|
|
|
6173
6314
|
}
|
|
6174
6315
|
/** Hot pass: place the cached {@link PreparedText} and refresh the a11y box. */
|
|
6175
6316
|
applyLayout() {
|
|
6317
|
+
this.contentEpoch++;
|
|
6176
6318
|
const result = this.layout.layoutPrepared(this.prepared);
|
|
6177
6319
|
this.nodes = result.nodes;
|
|
6178
6320
|
this.width = result.totalWidth;
|
|
@@ -6850,6 +6992,7 @@ export {
|
|
|
6850
6992
|
CanvasRenderer,
|
|
6851
6993
|
Circle,
|
|
6852
6994
|
ComputeParticleEntity,
|
|
6995
|
+
DEFAULT_CONTENT_SEMANTIC_BUDGET,
|
|
6853
6996
|
DOMPortalEntity,
|
|
6854
6997
|
Entity,
|
|
6855
6998
|
GlyphRasterAtlas,
|
|
@@ -34,6 +34,8 @@ export declare class MSDFTextEntity extends Entity {
|
|
|
34
34
|
private layoutText;
|
|
35
35
|
private text;
|
|
36
36
|
private lastRenderedSeqId;
|
|
37
|
+
/** Bumped by {@link queueLayout}; read by `Scene` to skip an unchanged sync. */
|
|
38
|
+
private contentEpoch;
|
|
37
39
|
private atlasDecodeTarget;
|
|
38
40
|
private atlasDecodeHandler;
|
|
39
41
|
private rgbColorCache;
|
|
@@ -91,6 +93,7 @@ export declare class MSDFTextEntity extends Entity {
|
|
|
91
93
|
* readers, crawlers, and translation see the same string the canvas draws.
|
|
92
94
|
*/
|
|
93
95
|
getContentProjection(): ContentProjection | null;
|
|
96
|
+
getContentEpoch(): number;
|
|
94
97
|
isPointInside(globalX: number, globalY: number): boolean;
|
|
95
98
|
render(renderer: any): void;
|
|
96
99
|
destroy(): void;
|
package/dist/text.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
var
|
|
5
|
+
var _chunkTRT2TRCWjs = require('./chunk-TRT2TRCW.js');
|
|
6
6
|
|
|
7
7
|
// src/text/index.ts
|
|
8
8
|
var _text = require('@vectojs/text'); _createStarExport(_text);
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
exports.MSDFTextEntity =
|
|
12
|
+
exports.MSDFTextEntity = _chunkTRT2TRCWjs.MSDFTextEntity; exports.SVGEntity = _chunkTRT2TRCWjs.SVGEntity;
|
package/dist/text.mjs
CHANGED
package/dist/tree/Entity.d.ts
CHANGED
|
@@ -1000,6 +1000,32 @@ export declare abstract class Entity {
|
|
|
1000
1000
|
* @returns The projection descriptor, or `null` to project nothing.
|
|
1001
1001
|
*/
|
|
1002
1002
|
getContentProjection(hint?: ContentProjectionHint): ContentProjection | null;
|
|
1003
|
+
/**
|
|
1004
|
+
* A cheap, monotonically-increasing stamp of this entity's projected content.
|
|
1005
|
+
*
|
|
1006
|
+
* Purely an optimization, and opt-in: returning `null` (the default) means
|
|
1007
|
+
* "I cannot cheaply tell whether my content changed", and the {@link Scene}
|
|
1008
|
+
* then rebuilds the projection every synced frame exactly as before. An
|
|
1009
|
+
* implementation must bump the value whenever anything
|
|
1010
|
+
* {@link getContentProjection} would report changes — text, fonts, line
|
|
1011
|
+
* geometry, `selectable`, grid revision.
|
|
1012
|
+
*
|
|
1013
|
+
* When two consecutive syncs report the same epoch AND the entity's geometry
|
|
1014
|
+
* is unchanged, `Scene` skips the block *before* calling
|
|
1015
|
+
* {@link getContentProjection}. That matters because the projection call is
|
|
1016
|
+
* O(glyphs-in-block) and the DOM diff around it costs about the same again:
|
|
1017
|
+
* measured on a 1500-resident-block document, a sync in which the projected
|
|
1018
|
+
* text was byte-identical before and after still cost 17.875 ms, and skipping
|
|
1019
|
+
* unchanged blocks took that to 0.475 ms (carryctx CTX-0199, vectojs#343).
|
|
1020
|
+
*
|
|
1021
|
+
* Correctness is entirely on the implementer: a stale epoch means stale DOM,
|
|
1022
|
+
* so bump it in the same place the content is invalidated rather than trying
|
|
1023
|
+
* to enumerate mutation sites afterwards. Any monotonic counter works; the
|
|
1024
|
+
* value is only ever compared for equality with the previous sync's.
|
|
1025
|
+
*
|
|
1026
|
+
* @returns The current content epoch, or `null` to disable skipping.
|
|
1027
|
+
*/
|
|
1028
|
+
getContentEpoch(): number | null;
|
|
1003
1029
|
/**
|
|
1004
1030
|
* Whether this entity still has a queued/running tween animation, or an
|
|
1005
1031
|
* active {@link setTransition}/{@link animateTo}/{@link springTo} property
|
package/dist/tree/Scene.d.ts
CHANGED
|
@@ -201,6 +201,64 @@ export interface SceneOptions {
|
|
|
201
201
|
* height (`undefined` → resolved to `Scene.height` at sync time).
|
|
202
202
|
*/
|
|
203
203
|
contentProjectionMargin?: number;
|
|
204
|
+
/**
|
|
205
|
+
* Virtualization margin (px) for the *semantic* tier of content projection —
|
|
206
|
+
* whether a block has **any** projected DOM at all, as opposed to
|
|
207
|
+
* {@link SceneOptions.contentProjectionMargin}, which decides whether that
|
|
208
|
+
* block's per-line **carriers** are windowed.
|
|
209
|
+
*
|
|
210
|
+
* Splitting the two makes a coarse resident tier expressible: with
|
|
211
|
+
* `contentSemanticMargin: Infinity` and a finite `contentProjectionMargin`,
|
|
212
|
+
* every block in the document keeps an element holding its full text — so
|
|
213
|
+
* find-in-page and screen-reader read-ahead see the whole document — while
|
|
214
|
+
* only blocks near the viewport pay for per-line carriers. One scalar could
|
|
215
|
+
* not express that, because a finite value freed off-band blocks entirely and
|
|
216
|
+
* `Infinity` also unwindowed every carrier, which is O(total document glyphs).
|
|
217
|
+
*
|
|
218
|
+
* `Infinity` is safe **here** and remains unsupported for
|
|
219
|
+
* `contentProjectionMargin`: the cost that made it unsupported comes from an
|
|
220
|
+
* unwindowed carrier band, not from resident text.
|
|
221
|
+
*
|
|
222
|
+
* Note the one-time cost. A resident tier materializes one element per block
|
|
223
|
+
* on the first sync — measured unbudgeted at 21.3ms for 1000 blocks and 139.5ms
|
|
224
|
+
* for 10000 on Chrome — as one synchronous block. Steady state is cheap
|
|
225
|
+
* (unchanged blocks skip via {@link Entity.getContentEpoch}), so this is a
|
|
226
|
+
* document-open stall, not a per-frame cost. That stall is what
|
|
227
|
+
* {@link SceneOptions.contentSemanticBudget} spreads across frames.
|
|
228
|
+
*
|
|
229
|
+
* Default: whatever `contentProjectionMargin` resolves to, so omitting this
|
|
230
|
+
* leaves behaviour unchanged.
|
|
231
|
+
*/
|
|
232
|
+
contentSemanticMargin?: number;
|
|
233
|
+
/**
|
|
234
|
+
* How many resident (coarse-tier) blocks may be materialized in **one** sync,
|
|
235
|
+
* bounding the document-open stall a wide {@link
|
|
236
|
+
* SceneOptions.contentSemanticMargin} otherwise pays all at once.
|
|
237
|
+
*
|
|
238
|
+
* The cost of a resident tier is per node **created**, not per node held: 10000
|
|
239
|
+
* resident blocks cost ~3.0 ms/sync at steady state, while creating them costs
|
|
240
|
+
* ~0.03 ms each plus a per-pass floor that grows with how many are already
|
|
241
|
+
* resident. So the front-load is a *scheduling* problem, and this is the
|
|
242
|
+
* schedule — remaining blocks materialize on subsequent syncs, a few per frame,
|
|
243
|
+
* until the document is fully resident.
|
|
244
|
+
*
|
|
245
|
+
* What it does **not** change is the end state: the same blocks end up with the
|
|
246
|
+
* same DOM, only later. Nothing is dropped, so the reachability the semantic
|
|
247
|
+
* tier exists for is preserved; a block still waiting is simply not yet in the
|
|
248
|
+
* DOM, exactly as a block beyond the margin is not.
|
|
249
|
+
*
|
|
250
|
+
* Applies **only** to the coarse tier. A block inside the interaction margin is
|
|
251
|
+
* on screen and materializes immediately regardless of this budget — deferring
|
|
252
|
+
* visible text would make it briefly unselectable, which is a user-visible
|
|
253
|
+
* regression rather than a cost saving.
|
|
254
|
+
*
|
|
255
|
+
* `Infinity` disables the budget and restores one synchronous pass. Default:
|
|
256
|
+
* {@link DEFAULT_CONTENT_SEMANTIC_BUDGET}. Because the coarse tier exists only
|
|
257
|
+
* when `contentSemanticMargin` is wider than `contentProjectionMargin`, a scene
|
|
258
|
+
* that does not opt into a resident tier has no coarse blocks and is therefore
|
|
259
|
+
* unaffected by any value here.
|
|
260
|
+
*/
|
|
261
|
+
contentSemanticBudget?: number;
|
|
204
262
|
/**
|
|
205
263
|
* Reading direction used to order the accessibility/automation shadow tree so
|
|
206
264
|
* keyboard **tab order** and screen-reader traversal follow the *visual*
|
|
@@ -240,7 +298,7 @@ export interface SceneOptions {
|
|
|
240
298
|
* against. A new option must be added here too — the test suite asserts the two
|
|
241
299
|
* stay in sync.
|
|
242
300
|
*/
|
|
243
|
-
export declare const SCENE_OPTION_KEYS: readonly ['a11ySyncInterval', 'autoThrottle', 'contentProjection', 'contentProjectionMargin', 'debugA11y', 'disableWindowResize', 'maxDPR', 'maxFPS', 'particleBackend', 'pointBackend', 'readingDirection', 'renderer', 'renderMode', 'respectReducedMotion', 'userTiming'];
|
|
301
|
+
export declare const SCENE_OPTION_KEYS: readonly ['a11ySyncInterval', 'autoThrottle', 'contentProjection', 'contentProjectionMargin', 'contentSemanticBudget', 'contentSemanticMargin', 'debugA11y', 'disableWindowResize', 'maxDPR', 'maxFPS', 'particleBackend', 'pointBackend', 'readingDirection', 'renderer', 'renderMode', 'respectReducedMotion', 'userTiming'];
|
|
244
302
|
/** Frame-rate the loop is capped to when the OS requests reduced motion. */
|
|
245
303
|
export declare const REDUCED_MOTION_FPS = 30;
|
|
246
304
|
/**
|
|
@@ -331,6 +389,45 @@ export interface A11yTreeNode {
|
|
|
331
389
|
valuemax?: string;
|
|
332
390
|
children: A11yTreeNode[];
|
|
333
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Default {@link SceneOptions.contentSemanticBudget}: resident blocks
|
|
394
|
+
* materialized per sync.
|
|
395
|
+
*
|
|
396
|
+
* Sized against the two costs a pass actually pays, both measured in real headed
|
|
397
|
+
* Chrome on a 240Hz panel. Per created block is cheap and flat (~0.03ms). What
|
|
398
|
+
* dominates is style+layout of the projection subtree, which scales with how many
|
|
399
|
+
* blocks are already RESIDENT and is paid once per pass: traced at 10000 blocks,
|
|
400
|
+
* `UpdateLayoutTree` 391.7ms + `Layout` 305.8ms over 40 passes (~17ms each), with
|
|
401
|
+
* per-pass cost roughly doubling from the first pass to the last while the number
|
|
402
|
+
* created stayed constant.
|
|
403
|
+
*
|
|
404
|
+
* So total drain cost is approximately `passes × f(resident)`, and a SMALLER
|
|
405
|
+
* budget multiplies the term that does not shrink. Measured to completion, 3
|
|
406
|
+
* repeats, medians:
|
|
407
|
+
*
|
|
408
|
+
* ```text
|
|
409
|
+
* 1000 blocks budget 32 → 67.1ms total, 4.3ms worst pass
|
|
410
|
+
* budget 64 → 54.0ms total, 5.1ms worst pass
|
|
411
|
+
* budget 256 → 27.7ms total, 7.7ms worst pass
|
|
412
|
+
* Infinity → 24.1ms total, 23.6ms worst pass
|
|
413
|
+
* 10000 blocks budget 32 → 3773.2ms total, 42.6ms worst pass
|
|
414
|
+
* budget 64 → 1896.2ms total, 41.6ms worst pass
|
|
415
|
+
* budget 256 → 648.1ms total, 35.2ms worst pass
|
|
416
|
+
* Infinity → 319.4ms total, 307.3ms worst pass
|
|
417
|
+
* ```
|
|
418
|
+
*
|
|
419
|
+
* 256 is where the two goals stop trading against each other. Below it there is no
|
|
420
|
+
* frame-bound improvement at 10000 blocks — every budget lands at 35-43ms, because
|
|
421
|
+
* the worst pass is the LAST one laying out the complete subtree — while total time
|
|
422
|
+
* rises 6x. At 1000 blocks it still holds 7.7ms, inside a 60Hz frame, for less than
|
|
423
|
+
* half the total time of 64.
|
|
424
|
+
*
|
|
425
|
+
* This replaces an earlier default of 64, which was sized against a per-block cost
|
|
426
|
+
* of ~0.4ms. That figure was inflated by a forced layout per materialized block
|
|
427
|
+
* (see `contentSelectionPresentThisSync`); with that removed, 64 spends 6x the
|
|
428
|
+
* total time for no frame-bound gain.
|
|
429
|
+
*/
|
|
430
|
+
export declare const DEFAULT_CONTENT_SEMANTIC_BUDGET = 256;
|
|
334
431
|
/**
|
|
335
432
|
* Top-level orchestrator that owns the entity tree, drive the render loop,
|
|
336
433
|
* and maintains the accessibility/automation shadow layer.
|
|
@@ -488,6 +585,15 @@ export declare class Scene {
|
|
|
488
585
|
private a11yElements;
|
|
489
586
|
/** DOM nodes mirroring static text content, keyed by entity id. */
|
|
490
587
|
private contentElements;
|
|
588
|
+
/**
|
|
589
|
+
* What the last completed content-projection sync was built from, per entity.
|
|
590
|
+
*
|
|
591
|
+
* Compared at the top of {@link syncContentProjection} to skip a block whose
|
|
592
|
+
* content AND geometry are both unchanged, before the O(glyphs) projection
|
|
593
|
+
* build. Only populated for entities that opt in via
|
|
594
|
+
* {@link Entity.getContentEpoch}. (carryctx CTX-0199)
|
|
595
|
+
*/
|
|
596
|
+
private contentSyncState;
|
|
491
597
|
/** Pending cold font-calibration frame per projected grid entity. */
|
|
492
598
|
private contentGridCalibrationFrames;
|
|
493
599
|
/** Detached, untransformed font probes used by the cold calibration pass. */
|
|
@@ -522,6 +628,33 @@ export declare class Scene {
|
|
|
522
628
|
private contentMetricScaleX;
|
|
523
629
|
private contentProjectionEnabled;
|
|
524
630
|
private contentProjectionMargin;
|
|
631
|
+
private contentSemanticMargin;
|
|
632
|
+
private contentSemanticBudget;
|
|
633
|
+
private contentSemanticBudgetLeft;
|
|
634
|
+
private contentSemanticDeferred;
|
|
635
|
+
/**
|
|
636
|
+
* Per-sync memo of "does the document hold a selection at all".
|
|
637
|
+
*
|
|
638
|
+
* Reading ANY property of a `Selection` (`anchorNode`, `rangeCount`, `type`,
|
|
639
|
+
* `isCollapsed`) forces a synchronous layout, because Blink validates the
|
|
640
|
+
* selection against current box geometry before answering. Measured in real
|
|
641
|
+
* Chrome against a 1000-carrier subtree with layout dirtied between reads:
|
|
642
|
+
* `anchorNode` 0.5ms, `rangeCount` 0.4ms, `type` 0.5ms, `isCollapsed` 0.5ms —
|
|
643
|
+
* all indistinguishable from `offsetHeight` (0.5ms), against a 0ms floor for
|
|
644
|
+
* mutating without reading. So there is no cheap property to probe with; the
|
|
645
|
+
* only way to avoid the layout is to not touch the object at all.
|
|
646
|
+
*
|
|
647
|
+
* Materializing a block rebuilds its carriers, which asks whether the rebuild
|
|
648
|
+
* would destroy a selection. Once per block, that read cost a forced layout
|
|
649
|
+
* over the whole (and growing) projection subtree, which is what made
|
|
650
|
+
* per-block cost rise with resident count: profiled at 1973 forced layouts
|
|
651
|
+
* totalling 633ms of an 847ms 1000-block drain (75%).
|
|
652
|
+
*
|
|
653
|
+
* A selection is a single document-wide object and a sync walk cannot yield to
|
|
654
|
+
* the user, so its presence cannot change mid-walk. Resolving it once per walk
|
|
655
|
+
* turns O(blocks) forced layouts into O(1). `null` = not yet resolved.
|
|
656
|
+
*/
|
|
657
|
+
private contentSelectionPresentThisSync;
|
|
525
658
|
/**
|
|
526
659
|
* True while a text-selection drag that started on a projection's blank
|
|
527
660
|
* region (no text node under the press) is being driven manually — the
|
|
@@ -1063,6 +1196,15 @@ export declare class Scene {
|
|
|
1063
1196
|
* to the live DOM selection.
|
|
1064
1197
|
*/
|
|
1065
1198
|
private contentGridSelectionLine;
|
|
1199
|
+
/**
|
|
1200
|
+
* Does the document hold a selection right now, memoized for this sync walk?
|
|
1201
|
+
*
|
|
1202
|
+
* Pays one forced layout per walk instead of one per rebuilt element — see
|
|
1203
|
+
* {@link Scene.contentSelectionPresentThisSync} for the measurements. When the
|
|
1204
|
+
* answer is `false` no element can own a selection, so every per-element
|
|
1205
|
+
* ownership test can be skipped without touching the object.
|
|
1206
|
+
*/
|
|
1207
|
+
private contentSelectionPresent;
|
|
1066
1208
|
private releaseContentSelectionForRebuild;
|
|
1067
1209
|
/**
|
|
1068
1210
|
* Rebuild a content-projection element's DOM (`rebuild`) while preserving a
|
|
@@ -1390,7 +1532,9 @@ export declare class Scene {
|
|
|
1390
1532
|
* transparent DOM node positioned over the drawn glyphs. Runs on the a11y
|
|
1391
1533
|
* sync cadence; all writes are dirty-checked. Off-viewport projections are
|
|
1392
1534
|
* hidden (`display: none`) so text-heavy scenes only materialize what is
|
|
1393
|
-
* visible to the browser's text machinery anyway
|
|
1535
|
+
* visible to the browser's text machinery anyway — except in the coarse
|
|
1536
|
+
* (resident) tier, which stays displayed because hiding it would make its text
|
|
1537
|
+
* unfindable and remove it from the accessibility tree, defeating the tier.
|
|
1394
1538
|
*/
|
|
1395
1539
|
/**
|
|
1396
1540
|
* Whether `node`'s world-space box, expanded by `margin` px on every side,
|
|
@@ -1399,6 +1543,13 @@ export declare class Scene {
|
|
|
1399
1543
|
* at `margin = contentProjectionMargin`) and for the exact `display:none`
|
|
1400
1544
|
* visibility test (`margin = 0`). Boundless nodes (width/height 0) opt out of
|
|
1401
1545
|
* culling and always count as visible, matching the legacy behavior.
|
|
1546
|
+
*
|
|
1547
|
+
* `viewportOnly` skips the `clipChildren` ancestor walk, answering the narrower
|
|
1548
|
+
* question "does this box overlap the viewport at all". The coarse content tier
|
|
1549
|
+
* needs the two apart: text that is merely off-viewport is clipped by
|
|
1550
|
+
* `a11yRoot`'s own `overflow: hidden` and can safely stay displayed, while text
|
|
1551
|
+
* rejected by an ancestor clip box that itself overlaps the viewport would sit
|
|
1552
|
+
* transparently on top of whatever is really drawn there.
|
|
1402
1553
|
*/
|
|
1403
1554
|
private projectionBoxVisible;
|
|
1404
1555
|
/**
|