@vectojs/core 1.29.0 → 1.31.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-MVFPN4Y5.mjs → chunk-RWN3ITBD.mjs} +81 -1
- package/dist/{chunk-Z3HFY75R.js → chunk-TRT2TRCW.js} +158 -78
- package/dist/components/TextEntity.d.ts +3 -0
- package/dist/index.js +513 -244
- package/dist/index.mjs +293 -24
- 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 +104 -1
- package/dist/tree/Scene.d.ts +127 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
Entity,
|
|
17
17
|
MSDFTextEntity,
|
|
18
18
|
SVGEntity,
|
|
19
|
-
VectoJSEvent
|
|
20
|
-
|
|
19
|
+
VectoJSEvent,
|
|
20
|
+
contentLineInHint
|
|
21
|
+
} from "./chunk-RWN3ITBD.mjs";
|
|
21
22
|
|
|
22
23
|
// src/tree/Scene.ts
|
|
23
24
|
import { SpringDriver, TweenDriver } from "@vectojs/animation";
|
|
@@ -1378,6 +1379,7 @@ var SCENE_OPTION_KEYS = [
|
|
|
1378
1379
|
"autoThrottle",
|
|
1379
1380
|
"contentProjection",
|
|
1380
1381
|
"contentProjectionMargin",
|
|
1382
|
+
"contentSemanticMargin",
|
|
1381
1383
|
"debugA11y",
|
|
1382
1384
|
"disableWindowResize",
|
|
1383
1385
|
"maxDPR",
|
|
@@ -1430,6 +1432,45 @@ function parseInlinePx(value) {
|
|
|
1430
1432
|
const n = parseFloat(value);
|
|
1431
1433
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
1432
1434
|
}
|
|
1435
|
+
function projectionGridLineWindow(grid, projectionLines, band) {
|
|
1436
|
+
const count = grid.lines.length;
|
|
1437
|
+
const all = { start: 0, end: count, gated: false };
|
|
1438
|
+
if (!band || count === 0) return all;
|
|
1439
|
+
const lines = [];
|
|
1440
|
+
for (let i = 0; i < count; i++) {
|
|
1441
|
+
const projected = projectionLines?.[i];
|
|
1442
|
+
lines.push({
|
|
1443
|
+
y: projected?.y ?? i * grid.lineHeight,
|
|
1444
|
+
lineHeight: projected?.lineHeight ?? grid.lineHeight
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
return projectionLineWindow(lines, band, grid.lineHeight);
|
|
1448
|
+
}
|
|
1449
|
+
function projectionLineWindow(lines, band, fallbackLineHeight) {
|
|
1450
|
+
const all = {
|
|
1451
|
+
start: 0,
|
|
1452
|
+
end: lines.length,
|
|
1453
|
+
gated: false
|
|
1454
|
+
};
|
|
1455
|
+
if (!band || lines.length === 0) return all;
|
|
1456
|
+
let start = -1;
|
|
1457
|
+
let end = -1;
|
|
1458
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1459
|
+
const line = lines[i];
|
|
1460
|
+
const h = line.lineHeight ?? fallbackLineHeight;
|
|
1461
|
+
if (line.y + h >= band.minY && line.y <= band.maxY) {
|
|
1462
|
+
if (start === -1) start = i;
|
|
1463
|
+
end = i + 1;
|
|
1464
|
+
} else if (start !== -1 && line.y > band.maxY) {
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (start === -1) {
|
|
1469
|
+
return { start: 0, end: Math.min(1, lines.length), gated: true };
|
|
1470
|
+
}
|
|
1471
|
+
if (start === 0 && end === lines.length) return all;
|
|
1472
|
+
return { start, end, gated: true };
|
|
1473
|
+
}
|
|
1433
1474
|
function collectTextNodes(root) {
|
|
1434
1475
|
const out = [];
|
|
1435
1476
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
@@ -1916,6 +1957,15 @@ var Scene = class _Scene {
|
|
|
1916
1957
|
a11yElements = /* @__PURE__ */ new Map();
|
|
1917
1958
|
/** DOM nodes mirroring static text content, keyed by entity id. */
|
|
1918
1959
|
contentElements = /* @__PURE__ */ new Map();
|
|
1960
|
+
/**
|
|
1961
|
+
* What the last completed content-projection sync was built from, per entity.
|
|
1962
|
+
*
|
|
1963
|
+
* Compared at the top of {@link syncContentProjection} to skip a block whose
|
|
1964
|
+
* content AND geometry are both unchanged, before the O(glyphs) projection
|
|
1965
|
+
* build. Only populated for entities that opt in via
|
|
1966
|
+
* {@link Entity.getContentEpoch}. (carryctx CTX-0199)
|
|
1967
|
+
*/
|
|
1968
|
+
contentSyncState = /* @__PURE__ */ new Map();
|
|
1919
1969
|
/** Pending cold font-calibration frame per projected grid entity. */
|
|
1920
1970
|
contentGridCalibrationFrames = /* @__PURE__ */ new Map();
|
|
1921
1971
|
/** Detached, untransformed font probes used by the cold calibration pass. */
|
|
@@ -1949,9 +1999,14 @@ var Scene = class _Scene {
|
|
|
1949
1999
|
contentMetricScaleEpoch = -1;
|
|
1950
2000
|
contentMetricScaleX = 1;
|
|
1951
2001
|
contentProjectionEnabled = true;
|
|
1952
|
-
// Virtualization margin (px) for content
|
|
1953
|
-
// viewport height, resolved at sync time. `Infinity`
|
|
2002
|
+
// Virtualization margin (px) for the content-projection CARRIER band;
|
|
2003
|
+
// `undefined` → one viewport height, resolved at sync time. `Infinity` is
|
|
2004
|
+
// unsupported here: it unwindows every carrier, which is O(total glyphs).
|
|
1954
2005
|
contentProjectionMargin = void 0;
|
|
2006
|
+
// Virtualization margin (px) for the SEMANTIC tier — whether a block has
|
|
2007
|
+
// any projected DOM. `undefined` → falls back to contentProjectionMargin, so
|
|
2008
|
+
// the default keeps one gate. `Infinity` = every block keeps resident text.
|
|
2009
|
+
contentSemanticMargin = void 0;
|
|
1955
2010
|
/**
|
|
1956
2011
|
* True while a text-selection drag that started on a projection's blank
|
|
1957
2012
|
* region (no text node under the press) is being driven manually — the
|
|
@@ -1988,6 +2043,14 @@ var Scene = class _Scene {
|
|
|
1988
2043
|
* mid-hover synthesize the `pointerleave` the browser never sends for a
|
|
1989
2044
|
* detached element, so the entity doesn't keep its hover state. */
|
|
1990
2045
|
hoveredA11yElements = /* @__PURE__ */ new WeakSet();
|
|
2046
|
+
/**
|
|
2047
|
+
* Entity ids the application has pinned via {@link requestA11yProjection}.
|
|
2048
|
+
*
|
|
2049
|
+
* Ids rather than entities so a removed entity cannot be retained by this set;
|
|
2050
|
+
* a stale id simply never matches. Cleared per-entity by
|
|
2051
|
+
* {@link releaseA11yProjection}.
|
|
2052
|
+
*/
|
|
2053
|
+
a11yProjectionRequests = /* @__PURE__ */ new Set();
|
|
1991
2054
|
/** Persistent tabindex=-1 element in a11yRoot. When the focused a11y mirror is
|
|
1992
2055
|
* pruned (virtualization/streaming/removal) while it holds focus, we move
|
|
1993
2056
|
* focus here instead of letting the browser drop it to <body> — keeping the
|
|
@@ -2941,7 +3004,9 @@ var Scene = class _Scene {
|
|
|
2941
3004
|
const el = this.contentElements?.get(node.id);
|
|
2942
3005
|
if (el) {
|
|
2943
3006
|
const projectedText = el.textContent || "";
|
|
2944
|
-
|
|
3007
|
+
const windowed = el.dataset.vectoProjectionWindow !== void 0;
|
|
3008
|
+
const mismatched = windowed ? !proj.text.includes(projectedText) : projectedText !== proj.text;
|
|
3009
|
+
if (projectedText !== "" && mismatched) {
|
|
2945
3010
|
this._devWarn(
|
|
2946
3011
|
`Content projection mismatch for entity "${node.id}": projection says "${proj.text.slice(0, 60)}" but DOM shows "${projectedText.slice(0, 60)}". Ensure getContentProjection() output matches what drawSelf renders.`
|
|
2947
3012
|
);
|
|
@@ -2977,6 +3042,7 @@ var Scene = class _Scene {
|
|
|
2977
3042
|
this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
|
|
2978
3043
|
this.contentProjectionEnabled = options.contentProjection ?? true;
|
|
2979
3044
|
this.contentProjectionMargin = options.contentProjectionMargin;
|
|
3045
|
+
this.contentSemanticMargin = options.contentSemanticMargin;
|
|
2980
3046
|
this.readingDirection = options.readingDirection ?? "ltr";
|
|
2981
3047
|
this.renderMode = options.renderMode ?? "always";
|
|
2982
3048
|
this._devActive = _Scene._devModeDetected();
|
|
@@ -3399,6 +3465,7 @@ var Scene = class _Scene {
|
|
|
3399
3465
|
this.clearContentGridState(node.id, contentEl);
|
|
3400
3466
|
contentEl.remove();
|
|
3401
3467
|
this.contentElements.delete(node.id);
|
|
3468
|
+
this.contentSyncState.delete(node.id);
|
|
3402
3469
|
this.a11yNeedsReorder = true;
|
|
3403
3470
|
}
|
|
3404
3471
|
const el = this.a11yElements.get(node.id);
|
|
@@ -3543,6 +3610,7 @@ var Scene = class _Scene {
|
|
|
3543
3610
|
this.a11yElements.clear();
|
|
3544
3611
|
for (const el of this.contentElements.values()) el.remove();
|
|
3545
3612
|
this.contentElements.clear();
|
|
3613
|
+
this.contentSyncState.clear();
|
|
3546
3614
|
if (typeof cancelAnimationFrame === "function") {
|
|
3547
3615
|
for (const frame of this.contentGridCalibrationFrames.values()) {
|
|
3548
3616
|
cancelAnimationFrame(frame);
|
|
@@ -3885,7 +3953,92 @@ var Scene = class _Scene {
|
|
|
3885
3953
|
* predicate, which is only tractable while it has one home.
|
|
3886
3954
|
*/
|
|
3887
3955
|
shouldProjectA11y(node) {
|
|
3888
|
-
|
|
3956
|
+
if (!node.interactive) return false;
|
|
3957
|
+
if (!(node.width > 0 || node.a11yFullViewport)) return false;
|
|
3958
|
+
switch (node.a11yProjection) {
|
|
3959
|
+
case "never":
|
|
3960
|
+
return false;
|
|
3961
|
+
case "onDemand":
|
|
3962
|
+
return this.a11yEngaged(node);
|
|
3963
|
+
default:
|
|
3964
|
+
return true;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
/**
|
|
3968
|
+
* Whether an `a11yProjection: 'onDemand'` entity is currently engaged enough to
|
|
3969
|
+
* deserve a shadow node.
|
|
3970
|
+
*
|
|
3971
|
+
* Deliberately **not** hover alone. A keyboard or assistive-technology user
|
|
3972
|
+
* generates no pointer events, so a hover-only trigger would withhold the
|
|
3973
|
+
* semantic node from precisely the users it exists for. Three signals, any of
|
|
3974
|
+
* which counts:
|
|
3975
|
+
*
|
|
3976
|
+
* - **Focus.** Covers keyboard traversal and AT-driven focus. Checked against
|
|
3977
|
+
* the live element so a node keeps its own focus rather than being pruned out
|
|
3978
|
+
* from under the user mid-interaction.
|
|
3979
|
+
* - **Pointer target.** The entity under the pointer, so a mouse user gets the
|
|
3980
|
+
* same node a hover-gated design would have given them.
|
|
3981
|
+
* - **Explicit request.** {@link Scene.requestA11yProjection}, for anything the
|
|
3982
|
+
* app knows is significant — the selected item, a search hit, a
|
|
3983
|
+
* just-announced element. This is the escape hatch that keeps the mode usable
|
|
3984
|
+
* when neither focus nor pointer applies.
|
|
3985
|
+
*
|
|
3986
|
+
* The entity stays hit-testable on canvas regardless, so a click always reaches
|
|
3987
|
+
* it and promotes it on the next sync.
|
|
3988
|
+
*/
|
|
3989
|
+
a11yEngaged(node) {
|
|
3990
|
+
if (this.a11yProjectionRequests.has(node.id)) return true;
|
|
3991
|
+
if (this.mouseX > -9e3 && !this.projectsSelectableText(node)) {
|
|
3992
|
+
if (node.isPointInside(this.mouseX, this.mouseY)) return true;
|
|
3993
|
+
}
|
|
3994
|
+
const existing = this.a11yElements?.get(node.id);
|
|
3995
|
+
if (existing && typeof document !== "undefined" && document.activeElement === existing) {
|
|
3996
|
+
return true;
|
|
3997
|
+
}
|
|
3998
|
+
return false;
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Whether `node` mirrors selectable text of its own.
|
|
4002
|
+
*
|
|
4003
|
+
* Such an entity must not be promoted by the pointer: its interactive a11y node
|
|
4004
|
+
* would sit above the text mirror and eat the mousedown that starts a native
|
|
4005
|
+
* selection.
|
|
4006
|
+
*/
|
|
4007
|
+
projectsSelectableText(node) {
|
|
4008
|
+
const projection = node.getContentProjection?.();
|
|
4009
|
+
return !!projection?.text && projection.selectable !== false;
|
|
4010
|
+
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Keep `entity`'s a11y shadow node projected while it has
|
|
4013
|
+
* `a11yProjection: 'onDemand'`.
|
|
4014
|
+
*
|
|
4015
|
+
* For anything the application knows matters but the engine cannot infer — the
|
|
4016
|
+
* selected danmaku, a search hit, a node just announced in a live region.
|
|
4017
|
+
* Without this, `'onDemand'` would be reachable only by focus or pointer, and
|
|
4018
|
+
* an app-driven selection change would leave the selected entity semantically
|
|
4019
|
+
* invisible.
|
|
4020
|
+
*
|
|
4021
|
+
* Idempotent. Has no effect on an `'eager'` entity, which is always projected.
|
|
4022
|
+
*/
|
|
4023
|
+
requestA11yProjection(entity) {
|
|
4024
|
+
const id = typeof entity === "string" ? entity : entity.id;
|
|
4025
|
+
if (this.a11yProjectionRequests.has(id)) return;
|
|
4026
|
+
this.a11yProjectionRequests.add(id);
|
|
4027
|
+
this.a11yNeedsReorder = true;
|
|
4028
|
+
this.markDirty({ entity: id, reason: "a11y-reorder" });
|
|
4029
|
+
}
|
|
4030
|
+
/**
|
|
4031
|
+
* Drop a projection request made by {@link requestA11yProjection}.
|
|
4032
|
+
*
|
|
4033
|
+
* The node is not removed immediately: it survives while it is focused or under
|
|
4034
|
+
* the pointer, and is pruned on the next sync that finds it unengaged. Releasing
|
|
4035
|
+
* a request the scene does not hold is a no-op.
|
|
4036
|
+
*/
|
|
4037
|
+
releaseA11yProjection(entity) {
|
|
4038
|
+
const id = typeof entity === "string" ? entity : entity.id;
|
|
4039
|
+
if (!this.a11yProjectionRequests.delete(id)) return;
|
|
4040
|
+
this.a11yNeedsReorder = true;
|
|
4041
|
+
this.markDirty({ entity: id, reason: "a11y-reorder" });
|
|
3889
4042
|
}
|
|
3890
4043
|
syncA11y(node, container = null) {
|
|
3891
4044
|
if (!this.a11yRoot) return;
|
|
@@ -4359,6 +4512,57 @@ var Scene = class _Scene {
|
|
|
4359
4512
|
}
|
|
4360
4513
|
return true;
|
|
4361
4514
|
}
|
|
4515
|
+
/**
|
|
4516
|
+
* The band of an entity's own y coordinates that is worth projecting, or
|
|
4517
|
+
* `null` to project everything.
|
|
4518
|
+
*
|
|
4519
|
+
* {@link projectionBoxVisible} answers "is this entity near the viewport",
|
|
4520
|
+
* which frees whole blocks that scroll away. It cannot help a single entity
|
|
4521
|
+
* *taller* than the viewport: that entity's box always intersects, so every
|
|
4522
|
+
* one of its visual lines was materialized — a `<span>` per line and, on the
|
|
4523
|
+
* grid path, a `<span>` per glyph cluster. That is where "14.8k elements for a
|
|
4524
|
+
* 346KB Markdown doc" comes from, and it is O(document) rather than
|
|
4525
|
+
* O(viewport) in both element count and per-frame walk cost.
|
|
4526
|
+
*
|
|
4527
|
+
* Measured on one entity scrolled to its middle, real headed browsers
|
|
4528
|
+
* (`benchmarks/projection-per-line/`): at 4000 lines, materializing every line
|
|
4529
|
+
* costs 6.28 ms/frame on Chrome and 6.51 ms on Firefox with 36,000 child
|
|
4530
|
+
* elements, against 0.28/0.16 ms and 963 elements when only the visible band
|
|
4531
|
+
* is emitted. The gated cost is *flat* across a 20x document-size range, so
|
|
4532
|
+
* this converts an asymptote rather than shaving a constant.
|
|
4533
|
+
*
|
|
4534
|
+
* Returns local-y bounds in the entity's own coordinate space, already
|
|
4535
|
+
* expanded by `margin` and intersected with every `clipChildren` ancestor, so
|
|
4536
|
+
* a line inside a scrolled container is measured against the container rather
|
|
4537
|
+
* than the window. `null` means "no useful bound" — a degenerate transform, a
|
|
4538
|
+
* rotation/skew that makes a y-band meaningless, or a boundless entity — and
|
|
4539
|
+
* the caller must then project every line, because emitting nothing would
|
|
4540
|
+
* silently drop text from selection, find-in-page and screen readers.
|
|
4541
|
+
*/
|
|
4542
|
+
projectionVisibleLocalYBand(node, tf, margin) {
|
|
4543
|
+
const { b, d, f } = tf;
|
|
4544
|
+
if (b !== 0 || d === 0 || !Number.isFinite(d) || !Number.isFinite(f)) return null;
|
|
4545
|
+
const top = (-margin - f) / d;
|
|
4546
|
+
const bottom = (this.height + margin - f) / d;
|
|
4547
|
+
let minY = Math.min(top, bottom);
|
|
4548
|
+
let maxY = Math.max(top, bottom);
|
|
4549
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
4550
|
+
if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
|
|
4551
|
+
const originWorldY = f;
|
|
4552
|
+
const unitWorldY = d + f;
|
|
4553
|
+
const originLocal = ancestor.worldToLocal(tf.e, originWorldY);
|
|
4554
|
+
const unitLocal = ancestor.worldToLocal(tf.e + tf.c, unitWorldY);
|
|
4555
|
+
if (!originLocal || !unitLocal) return null;
|
|
4556
|
+
const slope = unitLocal.y - originLocal.y;
|
|
4557
|
+
if (slope === 0 || !Number.isFinite(slope)) return null;
|
|
4558
|
+
const a1 = (-margin - originLocal.y) / slope;
|
|
4559
|
+
const a2 = (ancestor.height + margin - originLocal.y) / slope;
|
|
4560
|
+
minY = Math.max(minY, Math.min(a1, a2));
|
|
4561
|
+
maxY = Math.min(maxY, Math.max(a1, a2));
|
|
4562
|
+
}
|
|
4563
|
+
if (!Number.isFinite(minY) || !Number.isFinite(maxY) || maxY < minY) return null;
|
|
4564
|
+
return { minY, maxY };
|
|
4565
|
+
}
|
|
4362
4566
|
syncContentProjection(node) {
|
|
4363
4567
|
if (!this.contentProjectionEnabled || !this.a11yRoot) return;
|
|
4364
4568
|
let el = this.contentElements.get(node.id);
|
|
@@ -4369,14 +4573,30 @@ var Scene = class _Scene {
|
|
|
4369
4573
|
this.contentElements.delete(node.id);
|
|
4370
4574
|
this.a11yNeedsReorder = true;
|
|
4371
4575
|
}
|
|
4576
|
+
this.contentSyncState.delete(node.id);
|
|
4372
4577
|
};
|
|
4373
4578
|
const worldTf = node.getWorldTransform();
|
|
4374
|
-
const
|
|
4375
|
-
|
|
4579
|
+
const interactionMargin = this.contentProjectionMargin ?? this.height;
|
|
4580
|
+
const semanticMargin = this.contentSemanticMargin ?? interactionMargin;
|
|
4581
|
+
if (Number.isFinite(semanticMargin) && !this.projectionBoxVisible(node, worldTf, semanticMargin)) {
|
|
4376
4582
|
releaseProjectionEl();
|
|
4377
4583
|
return;
|
|
4378
4584
|
}
|
|
4379
|
-
const
|
|
4585
|
+
const inInteractionBand = !Number.isFinite(interactionMargin) || this.projectionBoxVisible(node, worldTf, interactionMargin);
|
|
4586
|
+
const tier = inInteractionBand ? "fine" : "coarse";
|
|
4587
|
+
const lineBand = tier === "coarse" ? null : Number.isFinite(interactionMargin) ? this.projectionVisibleLocalYBand(node, worldTf, interactionMargin) : null;
|
|
4588
|
+
const visible = this.projectionBoxVisible(node, worldTf, 0);
|
|
4589
|
+
const epoch = node.getContentEpoch();
|
|
4590
|
+
const prior = this.contentSyncState.get(node.id);
|
|
4591
|
+
if (epoch !== null && el && prior !== void 0) {
|
|
4592
|
+
const { a: a2, b: b2, c: c2, d: d2, e: e2, f: f2 } = worldTf;
|
|
4593
|
+
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) {
|
|
4594
|
+
return;
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
const projection = node.getContentProjection(
|
|
4598
|
+
lineBand ? { minY: lineBand.minY, maxY: lineBand.maxY } : void 0
|
|
4599
|
+
);
|
|
4380
4600
|
if (!projection || !projection.text) {
|
|
4381
4601
|
releaseProjectionEl();
|
|
4382
4602
|
return;
|
|
@@ -4406,23 +4626,30 @@ var Scene = class _Scene {
|
|
|
4406
4626
|
this.a11yNeedsReorder = true;
|
|
4407
4627
|
}
|
|
4408
4628
|
const lines = projection.lines;
|
|
4409
|
-
|
|
4629
|
+
const useCarriers = tier === "fine";
|
|
4630
|
+
if ((!projection.grid || !useCarriers) && el.dataset.vectoContentGrid !== void 0) {
|
|
4410
4631
|
this.clearContentGridState(node.id, el);
|
|
4411
4632
|
}
|
|
4412
|
-
if (projection.grid) {
|
|
4633
|
+
if (projection.grid && useCarriers) {
|
|
4413
4634
|
const gridSyncStart = this._phaseTiming ? performance.now() : 0;
|
|
4414
|
-
this.syncContentGridProjection(node, el, projection, projection.grid);
|
|
4635
|
+
this.syncContentGridProjection(node, el, projection, projection.grid, lineBand);
|
|
4415
4636
|
if (this._phaseTiming) this._recordPhase("gridSync", performance.now() - gridSyncStart);
|
|
4416
|
-
} else if (lines && lines.length > 0) {
|
|
4637
|
+
} else if (useCarriers && lines && lines.length > 0) {
|
|
4638
|
+
const lineWindow = projectionLineWindow(lines, lineBand, projection.lineHeight ?? 16);
|
|
4417
4639
|
const signature = JSON.stringify({
|
|
4418
4640
|
lines,
|
|
4419
4641
|
fallbackFont: projection.font ?? "",
|
|
4420
|
-
fallbackLineHeight: projection.lineHeight ?? 16
|
|
4642
|
+
fallbackLineHeight: projection.lineHeight ?? 16,
|
|
4643
|
+
// Part of the signature, or scrolling would not rebuild the carriers and
|
|
4644
|
+
// the window would stay frozen where it was first built. Quantized to
|
|
4645
|
+
// whole line indices by construction, so a sub-pixel scroll inside one
|
|
4646
|
+
// line does not churn the DOM.
|
|
4647
|
+
window: lineWindow.gated ? `${lineWindow.start}-${lineWindow.end}` : "all"
|
|
4421
4648
|
});
|
|
4422
4649
|
if (el.dataset.vectoProjectionLines !== signature) {
|
|
4423
4650
|
this.preserveContentSelectionAcrossRebuild(el, () => {
|
|
4424
4651
|
el.replaceChildren();
|
|
4425
|
-
for (let index =
|
|
4652
|
+
for (let index = lineWindow.start; index < lineWindow.end; index++) {
|
|
4426
4653
|
const line = lines[index];
|
|
4427
4654
|
const lineElement = document.createElement("span");
|
|
4428
4655
|
const lineFont = line.font ?? projection.font ?? "";
|
|
@@ -4463,13 +4690,20 @@ var Scene = class _Scene {
|
|
|
4463
4690
|
}
|
|
4464
4691
|
});
|
|
4465
4692
|
el.dataset.vectoProjectionLines = signature;
|
|
4693
|
+
if (lineWindow.gated) {
|
|
4694
|
+
el.dataset.vectoProjectionWindow = `${lineWindow.start}-${lineWindow.end}/${lines.length}`;
|
|
4695
|
+
} else {
|
|
4696
|
+
delete el.dataset.vectoProjectionWindow;
|
|
4697
|
+
}
|
|
4466
4698
|
}
|
|
4467
4699
|
} else {
|
|
4468
|
-
|
|
4700
|
+
const demoted = tier === "coarse" && el.children.length > 0;
|
|
4701
|
+
if (el.textContent !== projection.text || demoted) {
|
|
4469
4702
|
this.releaseContentSelectionForRebuild(el);
|
|
4470
4703
|
el.textContent = projection.text;
|
|
4471
4704
|
}
|
|
4472
4705
|
delete el.dataset.vectoProjectionLines;
|
|
4706
|
+
if (tier === "coarse") delete el.dataset.vectoProjectionWindow;
|
|
4473
4707
|
}
|
|
4474
4708
|
const font = projection.font ?? "";
|
|
4475
4709
|
if (el.style.font !== font) el.style.font = font;
|
|
@@ -4502,20 +4736,41 @@ var Scene = class _Scene {
|
|
|
4502
4736
|
if (node.width > 0) el.style.width = `${node.width}px`;
|
|
4503
4737
|
if (node.height > 0) el.style.height = `${node.height}px`;
|
|
4504
4738
|
el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
|
|
4505
|
-
const visible = this.projectionBoxVisible(node, worldTf, 0);
|
|
4506
4739
|
const display = visible ? "" : "none";
|
|
4507
4740
|
if (el.style.display !== display) el.style.display = display;
|
|
4741
|
+
if (epoch !== null) {
|
|
4742
|
+
const { a: a2, b: b2, c: c2, d: d2, e: e2, f: f2 } = worldTf;
|
|
4743
|
+
const next = prior ?? {};
|
|
4744
|
+
next.epoch = epoch;
|
|
4745
|
+
next.fontEpoch = this.contentFontEpoch;
|
|
4746
|
+
next.a = a2;
|
|
4747
|
+
next.b = b2;
|
|
4748
|
+
next.c = c2;
|
|
4749
|
+
next.d = d2;
|
|
4750
|
+
next.e = e2;
|
|
4751
|
+
next.f = f2;
|
|
4752
|
+
next.tier = tier;
|
|
4753
|
+
next.hasBand = lineBand !== null;
|
|
4754
|
+
next.bandMin = lineBand?.minY ?? 0;
|
|
4755
|
+
next.bandMax = lineBand?.maxY ?? 0;
|
|
4756
|
+
next.visible = visible;
|
|
4757
|
+
next.width = node.width;
|
|
4758
|
+
next.height = node.height;
|
|
4759
|
+
next.interactive = node.interactive;
|
|
4760
|
+
if (prior === void 0) this.contentSyncState.set(node.id, next);
|
|
4761
|
+
}
|
|
4508
4762
|
}
|
|
4509
4763
|
/**
|
|
4510
4764
|
* Materialize a prepared grid in logical source order while positioning each
|
|
4511
4765
|
* carrier from the shared canvas geometry. Browser font measurement happens
|
|
4512
4766
|
* later in one cold read/write batch, never inside projection synchronization.
|
|
4513
4767
|
*/
|
|
4514
|
-
syncContentGridProjection(node, el, projection, grid) {
|
|
4768
|
+
syncContentGridProjection(node, el, projection, grid, lineBand) {
|
|
4515
4769
|
if (grid.source !== projection.text) {
|
|
4516
4770
|
throw new Error("ContentProjection.grid.source must equal ContentProjection.text");
|
|
4517
4771
|
}
|
|
4518
|
-
const
|
|
4772
|
+
const gridWindow = projectionGridLineWindow(grid, projection.lines, lineBand);
|
|
4773
|
+
const signature = gridWindow.gated ? `${grid.revision}:${gridWindow.start}-${gridWindow.end}` : `${grid.revision}`;
|
|
4519
4774
|
if (el.dataset.vectoContentGrid !== signature) {
|
|
4520
4775
|
const materializeStart = typeof performance !== "undefined" ? performance.now() : 0;
|
|
4521
4776
|
this.clearContentGridState(node.id, el, false);
|
|
@@ -4523,7 +4778,8 @@ var Scene = class _Scene {
|
|
|
4523
4778
|
const selectionLine = this.contentGridSelectionLine(el);
|
|
4524
4779
|
let rebuiltSelectionLine = false;
|
|
4525
4780
|
const existingLines = el.children;
|
|
4526
|
-
for (let lineIndex =
|
|
4781
|
+
for (let lineIndex = gridWindow.start; lineIndex < gridWindow.end; lineIndex++) {
|
|
4782
|
+
const domIndex = lineIndex - gridWindow.start;
|
|
4527
4783
|
const gridLine = grid.lines[lineIndex];
|
|
4528
4784
|
const projectedLine = projectionLines[lineIndex];
|
|
4529
4785
|
const lineHeight = projectedLine?.lineHeight ?? grid.lineHeight;
|
|
@@ -4538,7 +4794,7 @@ var Scene = class _Scene {
|
|
|
4538
4794
|
lineFont,
|
|
4539
4795
|
lineIndex === 0
|
|
4540
4796
|
);
|
|
4541
|
-
const reusable = existingLines[
|
|
4797
|
+
const reusable = existingLines[domIndex];
|
|
4542
4798
|
if (reusable !== void 0 && reusable.dataset.vectoGridLineSig === lineSignature && reusable.dataset.vectoGridLine === `${lineIndex}`) {
|
|
4543
4799
|
continue;
|
|
4544
4800
|
}
|
|
@@ -4611,12 +4867,13 @@ var Scene = class _Scene {
|
|
|
4611
4867
|
lineElement.appendChild(marker);
|
|
4612
4868
|
}
|
|
4613
4869
|
}
|
|
4614
|
-
const occupant = el.children[
|
|
4870
|
+
const occupant = el.children[domIndex];
|
|
4615
4871
|
if (occupant) el.replaceChild(lineElement, occupant);
|
|
4616
4872
|
else el.appendChild(lineElement);
|
|
4617
4873
|
}
|
|
4618
|
-
|
|
4619
|
-
|
|
4874
|
+
const windowLength = gridWindow.end - gridWindow.start;
|
|
4875
|
+
while (el.children.length > windowLength) {
|
|
4876
|
+
if (selectionLine !== null && selectionLine >= gridWindow.end) {
|
|
4620
4877
|
rebuiltSelectionLine = true;
|
|
4621
4878
|
}
|
|
4622
4879
|
el.lastElementChild?.remove();
|
|
@@ -4624,6 +4881,11 @@ var Scene = class _Scene {
|
|
|
4624
4881
|
if (rebuiltSelectionLine) this.releaseContentSelectionForRebuild(el);
|
|
4625
4882
|
el.dataset.vectoProjectionLines = signature;
|
|
4626
4883
|
el.dataset.vectoContentGrid = signature;
|
|
4884
|
+
if (gridWindow.gated) {
|
|
4885
|
+
el.dataset.vectoProjectionWindow = `${gridWindow.start}-${gridWindow.end}/${grid.lines.length}`;
|
|
4886
|
+
} else {
|
|
4887
|
+
delete el.dataset.vectoProjectionWindow;
|
|
4888
|
+
}
|
|
4627
4889
|
el.dataset.vectoGridCarriers = `${el.querySelectorAll("[data-vecto-grid-cell]").length}`;
|
|
4628
4890
|
if (typeof performance !== "undefined") {
|
|
4629
4891
|
const materializeMs = performance.now() - materializeStart;
|
|
@@ -5899,6 +6161,8 @@ var TextEntity = class extends Entity {
|
|
|
5899
6161
|
hoveredFillStyle = "#ffffff";
|
|
5900
6162
|
lineWidth = 1;
|
|
5901
6163
|
isHovered = false;
|
|
6164
|
+
/** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
|
|
6165
|
+
contentEpoch = 0;
|
|
5902
6166
|
constructor(text, atlas, maxWidth, fontSize = 24) {
|
|
5903
6167
|
super();
|
|
5904
6168
|
this.text = text;
|
|
@@ -5919,6 +6183,9 @@ var TextEntity = class extends Entity {
|
|
|
5919
6183
|
if (!this.text) return null;
|
|
5920
6184
|
return { text: this.text, font: `${this.fontSize}px sans-serif` };
|
|
5921
6185
|
}
|
|
6186
|
+
getContentEpoch() {
|
|
6187
|
+
return this.contentEpoch;
|
|
6188
|
+
}
|
|
5922
6189
|
/**
|
|
5923
6190
|
* Replace the text content. Runs the **cold** measurement pass (re-segment +
|
|
5924
6191
|
* re-measure) since the glyphs changed, then re-lays out.
|
|
@@ -5965,6 +6232,7 @@ var TextEntity = class extends Entity {
|
|
|
5965
6232
|
}
|
|
5966
6233
|
/** Hot pass: place the cached {@link PreparedText} and refresh the a11y box. */
|
|
5967
6234
|
applyLayout() {
|
|
6235
|
+
this.contentEpoch++;
|
|
5968
6236
|
const result = this.layout.layoutPrepared(this.prepared);
|
|
5969
6237
|
this.nodes = result.nodes;
|
|
5970
6238
|
this.width = result.totalWidth;
|
|
@@ -6670,6 +6938,7 @@ export {
|
|
|
6670
6938
|
VectoJSEvent,
|
|
6671
6939
|
WebGPUParticleSystemManager,
|
|
6672
6940
|
beginVectoUserTiming,
|
|
6941
|
+
contentLineInHint,
|
|
6673
6942
|
createWebGLPointRenderer,
|
|
6674
6943
|
endVectoUserTiming,
|
|
6675
6944
|
installRendererDevTraps,
|
|
@@ -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;
|