@vectojs/core 1.23.0 → 1.24.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-LHONR3GO.js → chunk-KEBYJVD6.js} +163 -94
- package/dist/{chunk-UPULSLKA.mjs → chunk-ME4LB2HB.mjs} +163 -94
- package/dist/index.d.ts +1 -0
- package/dist/index.js +449 -236
- package/dist/index.mjs +230 -17
- package/dist/performance/UserTiming.d.ts +44 -0
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/tree/Scene.d.ts +49 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
isSafeUrl,
|
|
9
9
|
parseColorToRGBA,
|
|
10
10
|
sanitizeUrl
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ME4LB2HB.mjs";
|
|
12
12
|
import {
|
|
13
13
|
Entity,
|
|
14
14
|
MSDFTextEntity,
|
|
@@ -1242,6 +1242,79 @@ async function loadCoreWasmRuntime(source) {
|
|
|
1242
1242
|
return createCoreWasmRuntime(module);
|
|
1243
1243
|
}
|
|
1244
1244
|
|
|
1245
|
+
// src/performance/UserTiming.ts
|
|
1246
|
+
var VECTO_USER_TIMING = {
|
|
1247
|
+
scene: {
|
|
1248
|
+
transform: "vecto:scene:transform",
|
|
1249
|
+
drawWalk: "vecto:scene:draw-walk",
|
|
1250
|
+
entityPaint: "vecto:scene:entity-paint",
|
|
1251
|
+
flush: "vecto:scene:flush",
|
|
1252
|
+
a11ySync: "vecto:scene:a11y-sync"
|
|
1253
|
+
},
|
|
1254
|
+
markdown: {
|
|
1255
|
+
parse: "vecto:markdown:parse"
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
var nextSpanId = 0;
|
|
1259
|
+
function beginVectoUserTiming(name) {
|
|
1260
|
+
const candidate = globalThis.performance;
|
|
1261
|
+
if (typeof candidate?.mark !== "function" || typeof candidate.measure !== "function") {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
const id = nextSpanId++;
|
|
1265
|
+
const startMark = `${name}:start:${id}`;
|
|
1266
|
+
const endMark = `${name}:end:${id}`;
|
|
1267
|
+
try {
|
|
1268
|
+
candidate.mark(startMark);
|
|
1269
|
+
return {
|
|
1270
|
+
name,
|
|
1271
|
+
startMark,
|
|
1272
|
+
endMark,
|
|
1273
|
+
performance: candidate
|
|
1274
|
+
};
|
|
1275
|
+
} catch {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function endVectoUserTiming(span) {
|
|
1280
|
+
if (!span) return;
|
|
1281
|
+
const timing = span.performance;
|
|
1282
|
+
try {
|
|
1283
|
+
timing.mark(span.endMark);
|
|
1284
|
+
timing.measure(span.name, span.startMark, span.endMark);
|
|
1285
|
+
} catch {
|
|
1286
|
+
} finally {
|
|
1287
|
+
try {
|
|
1288
|
+
timing.clearMarks?.(span.startMark);
|
|
1289
|
+
timing.clearMarks?.(span.endMark);
|
|
1290
|
+
} catch {
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function measureVectoUserTiming(name, durationMs) {
|
|
1295
|
+
const candidate = globalThis.performance;
|
|
1296
|
+
if (typeof candidate?.now !== "function" || typeof candidate.mark !== "function" || typeof candidate.measure !== "function") {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const id = nextSpanId++;
|
|
1300
|
+
const startMark = `${name}:start:${id}`;
|
|
1301
|
+
const endMark = `${name}:end:${id}`;
|
|
1302
|
+
const endTime = candidate.now();
|
|
1303
|
+
const startTime = Math.max(0, endTime - Math.max(0, durationMs));
|
|
1304
|
+
try {
|
|
1305
|
+
candidate.mark(startMark, { startTime });
|
|
1306
|
+
candidate.mark(endMark, { startTime: endTime });
|
|
1307
|
+
candidate.measure(name, startMark, endMark);
|
|
1308
|
+
} catch {
|
|
1309
|
+
} finally {
|
|
1310
|
+
try {
|
|
1311
|
+
candidate.clearMarks?.(startMark);
|
|
1312
|
+
candidate.clearMarks?.(endMark);
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1245
1318
|
// src/tree/Scene.ts
|
|
1246
1319
|
import { clearCssLineBoxMetrics, cssLineBoxBaseline } from "@vectojs/text";
|
|
1247
1320
|
var RANGE_VALUE_ROLES = /* @__PURE__ */ new Set(["slider", "spinbutton", "progressbar", "scrollbar", "meter"]);
|
|
@@ -1256,9 +1329,47 @@ var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
|
|
|
1256
1329
|
"slider",
|
|
1257
1330
|
"combobox"
|
|
1258
1331
|
]);
|
|
1332
|
+
var A11Y_REQUIRED_OWNED = /* @__PURE__ */ new Map([
|
|
1333
|
+
["grid", /* @__PURE__ */ new Set(["row", "rowgroup"])],
|
|
1334
|
+
["table", /* @__PURE__ */ new Set(["row", "rowgroup"])],
|
|
1335
|
+
["treegrid", /* @__PURE__ */ new Set(["row", "rowgroup"])],
|
|
1336
|
+
["rowgroup", /* @__PURE__ */ new Set(["row"])],
|
|
1337
|
+
["row", /* @__PURE__ */ new Set(["cell", "columnheader", "gridcell", "rowheader"])],
|
|
1338
|
+
["tablist", /* @__PURE__ */ new Set(["tab"])],
|
|
1339
|
+
["tree", /* @__PURE__ */ new Set(["treeitem", "group"])],
|
|
1340
|
+
["group", /* @__PURE__ */ new Set(["treeitem", "menuitem", "menuitemcheckbox", "menuitemradio", "option"])],
|
|
1341
|
+
["menu", /* @__PURE__ */ new Set(["menuitem", "menuitemcheckbox", "menuitemradio", "group", "separator"])],
|
|
1342
|
+
["menubar", /* @__PURE__ */ new Set(["menuitem", "menuitemcheckbox", "menuitemradio", "group", "separator"])],
|
|
1343
|
+
["listbox", /* @__PURE__ */ new Set(["option", "group"])],
|
|
1344
|
+
["list", /* @__PURE__ */ new Set(["listitem"])]
|
|
1345
|
+
]);
|
|
1259
1346
|
function isNativelyFocusable(element) {
|
|
1260
1347
|
return element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement || element instanceof HTMLAnchorElement && element.hasAttribute("href");
|
|
1261
1348
|
}
|
|
1349
|
+
var REBASED_BOX = { left: 0, top: 0, matrix: "" };
|
|
1350
|
+
function rebaseChildBox(parent, parentOriginX, parentOriginY, child, childOriginX, childOriginY) {
|
|
1351
|
+
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1352
|
+
if (!Number.isFinite(det) || Math.abs(det) < 1e-12) {
|
|
1353
|
+
REBASED_BOX.left = 0;
|
|
1354
|
+
REBASED_BOX.top = 0;
|
|
1355
|
+
REBASED_BOX.matrix = "matrix(1, 0, 0, 1, 0, 0)";
|
|
1356
|
+
return REBASED_BOX;
|
|
1357
|
+
}
|
|
1358
|
+
const ia = parent.d / det;
|
|
1359
|
+
const ib = -parent.b / det;
|
|
1360
|
+
const ic = -parent.c / det;
|
|
1361
|
+
const id = parent.a / det;
|
|
1362
|
+
const dx = childOriginX - parentOriginX;
|
|
1363
|
+
const dy = childOriginY - parentOriginY;
|
|
1364
|
+
REBASED_BOX.left = ia * dx + ic * dy;
|
|
1365
|
+
REBASED_BOX.top = ib * dx + id * dy;
|
|
1366
|
+
const a = ia * child.a + ic * child.b;
|
|
1367
|
+
const b = ib * child.a + id * child.b;
|
|
1368
|
+
const c = ia * child.c + ic * child.d;
|
|
1369
|
+
const d = ib * child.c + id * child.d;
|
|
1370
|
+
REBASED_BOX.matrix = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
|
|
1371
|
+
return REBASED_BOX;
|
|
1372
|
+
}
|
|
1262
1373
|
var REDUCED_MOTION_FPS = 30;
|
|
1263
1374
|
function parseInlinePx(value) {
|
|
1264
1375
|
if (!value || !value.endsWith("px")) return null;
|
|
@@ -1593,6 +1704,7 @@ var Scene = class _Scene {
|
|
|
1593
1704
|
/** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
|
|
1594
1705
|
static MAX_DIRTY_REASONS = 200;
|
|
1595
1706
|
_phaseTiming = false;
|
|
1707
|
+
_userTiming = false;
|
|
1596
1708
|
_phaseTotals = /* @__PURE__ */ new Map();
|
|
1597
1709
|
/**
|
|
1598
1710
|
* Start or stop per-phase render timing.
|
|
@@ -1615,6 +1727,19 @@ var Scene = class _Scene {
|
|
|
1615
1727
|
get phaseTiming() {
|
|
1616
1728
|
return this._phaseTiming;
|
|
1617
1729
|
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Enable or disable browser User Timing phase instrumentation.
|
|
1732
|
+
*
|
|
1733
|
+
* Off by default. The disabled frame path performs only boolean checks and
|
|
1734
|
+
* emits no Performance Timeline entries.
|
|
1735
|
+
*/
|
|
1736
|
+
setUserTiming(enabled) {
|
|
1737
|
+
this._userTiming = enabled;
|
|
1738
|
+
}
|
|
1739
|
+
/** Whether browser User Timing phase instrumentation is enabled. */
|
|
1740
|
+
get userTiming() {
|
|
1741
|
+
return this._userTiming;
|
|
1742
|
+
}
|
|
1618
1743
|
/**
|
|
1619
1744
|
* Accumulate one phase sample.
|
|
1620
1745
|
*
|
|
@@ -1820,6 +1945,8 @@ var Scene = class _Scene {
|
|
|
1820
1945
|
fullViewportElements = [];
|
|
1821
1946
|
normalElements = [];
|
|
1822
1947
|
activeIds = /* @__PURE__ */ new Set();
|
|
1948
|
+
/** Per-parent insertion cursor, reused by `enforceA11yDomOrder`. */
|
|
1949
|
+
a11yOrderCursors = /* @__PURE__ */ new Map();
|
|
1823
1950
|
activePortalsThisFrame = /* @__PURE__ */ new Set();
|
|
1824
1951
|
activePortalsPrevFrame = /* @__PURE__ */ new Set();
|
|
1825
1952
|
portalEntities = /* @__PURE__ */ new Map();
|
|
@@ -2734,6 +2861,7 @@ var Scene = class _Scene {
|
|
|
2734
2861
|
this.maxFPS = options.maxFPS ?? (isTest ? 0 : 60);
|
|
2735
2862
|
this.respectReducedMotion = options.respectReducedMotion ?? true;
|
|
2736
2863
|
this.autoThrottle = options.autoThrottle ?? true;
|
|
2864
|
+
this._userTiming = options.userTiming ?? false;
|
|
2737
2865
|
this.particleBackend = options.particleBackend ?? "auto";
|
|
2738
2866
|
this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
|
|
2739
2867
|
this.contentProjectionEnabled = options.contentProjection ?? true;
|
|
@@ -2778,6 +2906,7 @@ var Scene = class _Scene {
|
|
|
2778
2906
|
});
|
|
2779
2907
|
if (typeof document !== "undefined") {
|
|
2780
2908
|
this.a11yRoot = document.createElement("div");
|
|
2909
|
+
this.a11yRoot.setAttribute("data-vecto-a11y-root", "");
|
|
2781
2910
|
this.a11yRoot.style.position = "absolute";
|
|
2782
2911
|
this.a11yRoot.style.top = "0";
|
|
2783
2912
|
this.a11yRoot.style.left = "0";
|
|
@@ -3630,7 +3759,7 @@ var Scene = class _Scene {
|
|
|
3630
3759
|
shouldProjectA11y(node) {
|
|
3631
3760
|
return node.interactive && (node.width > 0 || node.a11yFullViewport);
|
|
3632
3761
|
}
|
|
3633
|
-
syncA11y(node) {
|
|
3762
|
+
syncA11y(node, container = null) {
|
|
3634
3763
|
if (!this.a11yRoot) return;
|
|
3635
3764
|
if (node.isDOMPortal) {
|
|
3636
3765
|
return;
|
|
@@ -3640,6 +3769,7 @@ var Scene = class _Scene {
|
|
|
3640
3769
|
return;
|
|
3641
3770
|
}
|
|
3642
3771
|
const nodeStart = this._phaseTiming ? performance.now() : 0;
|
|
3772
|
+
let childContainer = container;
|
|
3643
3773
|
if (this.shouldProjectA11y(node)) {
|
|
3644
3774
|
let el = this.a11yElements.get(node.id);
|
|
3645
3775
|
const attrs = node.getA11yAttributes();
|
|
@@ -3652,9 +3782,9 @@ var Scene = class _Scene {
|
|
|
3652
3782
|
this.caretBlinkTimer = null;
|
|
3653
3783
|
}
|
|
3654
3784
|
}
|
|
3655
|
-
if (el.parentNode
|
|
3785
|
+
if (el.parentNode) {
|
|
3656
3786
|
this.preserveFocusOnRemoval(el);
|
|
3657
|
-
|
|
3787
|
+
el.remove();
|
|
3658
3788
|
}
|
|
3659
3789
|
this.a11yElements.delete(node.id);
|
|
3660
3790
|
el = void 0;
|
|
@@ -3962,6 +4092,7 @@ var Scene = class _Scene {
|
|
|
3962
4092
|
if (el.style.boxSizing !== "border-box") el.style.boxSizing = "border-box";
|
|
3963
4093
|
if (el instanceof HTMLTextAreaElement) el.style.resize = "none";
|
|
3964
4094
|
}
|
|
4095
|
+
const nestedIn = container && attrs.role && container.owned.has(attrs.role) ? container : null;
|
|
3965
4096
|
if (node.a11yFullViewport) {
|
|
3966
4097
|
el.style.left = "0px";
|
|
3967
4098
|
el.style.top = "0px";
|
|
@@ -3972,11 +4103,36 @@ var Scene = class _Scene {
|
|
|
3972
4103
|
} else {
|
|
3973
4104
|
const worldTf = node.getWorldTransform();
|
|
3974
4105
|
const { a, b, c, d, e, f } = worldTf;
|
|
3975
|
-
|
|
3976
|
-
|
|
4106
|
+
const originX = e + node.a11yOffsetX;
|
|
4107
|
+
const originY = f + node.a11yOffsetY;
|
|
4108
|
+
const parentEl = nestedIn && nestedIn.el !== el && nestedIn.el.isConnected ? nestedIn.el : this.a11yRoot;
|
|
4109
|
+
if (el.parentNode !== parentEl) {
|
|
4110
|
+
parentEl.appendChild(el);
|
|
4111
|
+
this.a11yNeedsReorder = true;
|
|
4112
|
+
}
|
|
4113
|
+
if (parentEl === this.a11yRoot) {
|
|
4114
|
+
el.style.left = `${originX}px`;
|
|
4115
|
+
el.style.top = `${originY}px`;
|
|
4116
|
+
el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
|
|
4117
|
+
} else {
|
|
4118
|
+
const box = rebaseChildBox(
|
|
4119
|
+
nestedIn.transform,
|
|
4120
|
+
nestedIn.originX,
|
|
4121
|
+
nestedIn.originY,
|
|
4122
|
+
worldTf,
|
|
4123
|
+
originX,
|
|
4124
|
+
originY
|
|
4125
|
+
);
|
|
4126
|
+
el.style.left = `${box.left}px`;
|
|
4127
|
+
el.style.top = `${box.top}px`;
|
|
4128
|
+
el.style.transform = box.matrix;
|
|
4129
|
+
}
|
|
3977
4130
|
el.style.width = `${node.width}px`;
|
|
3978
4131
|
el.style.height = `${node.height}px`;
|
|
3979
|
-
|
|
4132
|
+
const owned = attrs.role ? A11Y_REQUIRED_OWNED.get(attrs.role) : void 0;
|
|
4133
|
+
if (owned) {
|
|
4134
|
+
childContainer = { el, owned, transform: worldTf, originX, originY };
|
|
4135
|
+
}
|
|
3980
4136
|
const visible = this.projectionBoxVisible(node, worldTf, 0);
|
|
3981
4137
|
const display = visible ? "" : "none";
|
|
3982
4138
|
if (el.style.display !== display) el.style.display = display;
|
|
@@ -3990,9 +4146,9 @@ var Scene = class _Scene {
|
|
|
3990
4146
|
} else {
|
|
3991
4147
|
this.syncContentProjection(node);
|
|
3992
4148
|
}
|
|
3993
|
-
for (const child of node.children) this.syncA11y(child);
|
|
4149
|
+
for (const child of node.children) this.syncA11y(child, childContainer);
|
|
3994
4150
|
if (node === this.root) {
|
|
3995
|
-
for (const overlay of this.overlayRoot.children) this.syncA11y(overlay);
|
|
4151
|
+
for (const overlay of this.overlayRoot.children) this.syncA11y(overlay, null);
|
|
3996
4152
|
}
|
|
3997
4153
|
}
|
|
3998
4154
|
/**
|
|
@@ -4550,9 +4706,9 @@ var Scene = class _Scene {
|
|
|
4550
4706
|
this.caretBlinkTimer = null;
|
|
4551
4707
|
}
|
|
4552
4708
|
}
|
|
4553
|
-
if (el.parentNode
|
|
4709
|
+
if (el.parentNode) {
|
|
4554
4710
|
this.preserveFocusOnRemoval(el);
|
|
4555
|
-
|
|
4711
|
+
el.remove();
|
|
4556
4712
|
}
|
|
4557
4713
|
this.a11yElements.delete(id);
|
|
4558
4714
|
}
|
|
@@ -4565,23 +4721,39 @@ var Scene = class _Scene {
|
|
|
4565
4721
|
const fullLen = this.fullViewportElements.length;
|
|
4566
4722
|
const normalLen = this.normalElements.length;
|
|
4567
4723
|
const totalLen = fullLen + normalLen;
|
|
4724
|
+
this.a11yOrderCursors.clear();
|
|
4568
4725
|
for (let i = 0; i < totalLen; i++) {
|
|
4569
4726
|
const expected = i < fullLen ? this.fullViewportElements[i] : this.normalElements[i - fullLen];
|
|
4570
|
-
const
|
|
4727
|
+
const parent = expected.parentNode;
|
|
4728
|
+
if (!parent) continue;
|
|
4729
|
+
const at = this.a11yOrderCursors.get(parent) ?? 0;
|
|
4730
|
+
this.a11yOrderCursors.set(parent, at + 1);
|
|
4731
|
+
const current = parent.childNodes[at];
|
|
4571
4732
|
if (current !== expected) {
|
|
4572
|
-
|
|
4733
|
+
parent.insertBefore(expected, current || null);
|
|
4573
4734
|
}
|
|
4574
4735
|
}
|
|
4575
4736
|
this.a11yNeedsReorder = false;
|
|
4576
4737
|
}
|
|
4577
4738
|
/**
|
|
4578
4739
|
* Reorder `normalElements` (in place) into visual reading order using the
|
|
4579
|
-
*
|
|
4740
|
+
* positions `syncA11y` already wrote to each element's inline style
|
|
4580
4741
|
* (`top`/`left`/`height`). Elements are grouped into rows top-to-bottom (an
|
|
4581
4742
|
* element belongs to the current row while its top is above the row's
|
|
4582
4743
|
* running bottom edge), then sorted within a row by `left` — ascending for
|
|
4583
4744
|
* `'ltr'`, descending for `'rtl'`. The sort is stable, so entities at the
|
|
4584
4745
|
* same position keep their scene-graph (collection) order as a tiebreak.
|
|
4746
|
+
*
|
|
4747
|
+
* Those inline values are world coordinates for a top-level mirror but
|
|
4748
|
+
* PARENT-RELATIVE for a nested one, so this list mixes coordinate spaces.
|
|
4749
|
+
* That is sound because the result is only ever applied per DOM parent
|
|
4750
|
+
* ({@link enforceA11yDomOrder} advances a cursor per parent), and all of one
|
|
4751
|
+
* parent's children share one space: a `grid`'s rows are all grid-relative, a
|
|
4752
|
+
* `row`'s cells all row-relative. Comparisons ACROSS spaces do happen while
|
|
4753
|
+
* banding, but they only affect the relative order of elements in different
|
|
4754
|
+
* parents, which no `insertBefore` ever acts on. Normalizing everything back
|
|
4755
|
+
* to world coordinates here would cost a transform per element per frame to
|
|
4756
|
+
* change nothing observable.
|
|
4585
4757
|
*/
|
|
4586
4758
|
sortNormalElementsVisually() {
|
|
4587
4759
|
const els = this.normalElements;
|
|
@@ -4817,9 +4989,11 @@ var Scene = class _Scene {
|
|
|
4817
4989
|
if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
|
|
4818
4990
|
this.lastA11ySync = time;
|
|
4819
4991
|
if (hasInteractive || wantsContentSync) {
|
|
4992
|
+
const userTiming = this._userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.a11ySync) : null;
|
|
4820
4993
|
const t0 = this._phaseTiming ? performance.now() : 0;
|
|
4821
4994
|
this.syncA11y(this.root);
|
|
4822
4995
|
if (this._phaseTiming) this._recordPhase("a11ySync", performance.now() - t0);
|
|
4996
|
+
if (userTiming) endVectoUserTiming(userTiming);
|
|
4823
4997
|
}
|
|
4824
4998
|
const t1 = this._phaseTiming ? performance.now() : 0;
|
|
4825
4999
|
this.enforceA11yDomOrder();
|
|
@@ -4833,6 +5007,26 @@ var Scene = class _Scene {
|
|
|
4833
5007
|
/**
|
|
4834
5008
|
* Render the entire scene graph onto the specified renderer.
|
|
4835
5009
|
*
|
|
5010
|
+
* Main-frame causal order is a correctness contract:
|
|
5011
|
+
*
|
|
5012
|
+
* 1. Browser/input callbacks finish before the scheduled frame begins.
|
|
5013
|
+
* 2. Batched property drivers and particle simulation advance.
|
|
5014
|
+
* 3. Entity `update()` hooks run.
|
|
5015
|
+
* 4. Transform inputs are gathered and world matrices are composed.
|
|
5016
|
+
* 5. Updated world bounds are tested for culling.
|
|
5017
|
+
* 6. Visible entities paint in scene-graph order.
|
|
5018
|
+
* 7. Canvas/GPU batches flush and retained renderers present.
|
|
5019
|
+
* 8. The rAF loop synchronizes content and accessibility projections after
|
|
5020
|
+
* this method returns.
|
|
5021
|
+
*
|
|
5022
|
+
* The causal order is fixed; physical walks may stay fused. The JavaScript
|
|
5023
|
+
* transform path interleaves update → compose → cull → paint per node in
|
|
5024
|
+
* pre-order. The WASM path updates the whole tree first, then gathers and
|
|
5025
|
+
* composes it in one store pass before the same cull/paint walk. Both must
|
|
5026
|
+
* expose an update's transform mutation in that same rendered frame.
|
|
5027
|
+
* Secondary renderers are read-only snapshots: they skip simulation and
|
|
5028
|
+
* updates, then compose/cull/paint/flush the current state.
|
|
5029
|
+
*
|
|
4836
5030
|
* @param renderer - The renderer instance to draw to.
|
|
4837
5031
|
* @param dt - Delta time in milliseconds (default 0).
|
|
4838
5032
|
* @param time - Current absolute time in milliseconds (default 0).
|
|
@@ -5018,10 +5212,13 @@ var Scene = class _Scene {
|
|
|
5018
5212
|
updateWalk(this.root);
|
|
5019
5213
|
for (const overlay of this.overlayRoot.children) updateWalk(overlay);
|
|
5020
5214
|
}
|
|
5215
|
+
const transformTiming = this._userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.transform) : null;
|
|
5021
5216
|
const wasmT0 = this._phaseTiming ? performance.now() : 0;
|
|
5022
5217
|
const wasmWorld = wasmMain ? this._syncWasmStore() : null;
|
|
5023
5218
|
if (this._phaseTiming) this._recordPhase("transform", performance.now() - wasmT0);
|
|
5219
|
+
if (transformTiming) endVectoUserTiming(transformTiming);
|
|
5024
5220
|
const wasmSlotEntity = this._slotEntity;
|
|
5221
|
+
let userEntityPaintMs = 0;
|
|
5025
5222
|
const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
|
|
5026
5223
|
if (isMainRenderer && !wasmMain) {
|
|
5027
5224
|
runUpdate(node);
|
|
@@ -5150,10 +5347,15 @@ var Scene = class _Scene {
|
|
|
5150
5347
|
);
|
|
5151
5348
|
}
|
|
5152
5349
|
} else {
|
|
5153
|
-
if (this._phaseTiming) {
|
|
5350
|
+
if (this._userTiming || this._phaseTiming) {
|
|
5154
5351
|
const t0 = performance.now();
|
|
5155
|
-
|
|
5156
|
-
|
|
5352
|
+
try {
|
|
5353
|
+
node.render(renderer);
|
|
5354
|
+
} finally {
|
|
5355
|
+
const elapsed = performance.now() - t0;
|
|
5356
|
+
if (this._userTiming) userEntityPaintMs += elapsed;
|
|
5357
|
+
if (this._phaseTiming) this._recordPhase("entityPaint", elapsed);
|
|
5358
|
+
}
|
|
5157
5359
|
} else {
|
|
5158
5360
|
node.render(renderer);
|
|
5159
5361
|
}
|
|
@@ -5168,17 +5370,23 @@ var Scene = class _Scene {
|
|
|
5168
5370
|
renderer.flush();
|
|
5169
5371
|
renderer.restore();
|
|
5170
5372
|
};
|
|
5373
|
+
const drawTiming = this._userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.drawWalk) : null;
|
|
5171
5374
|
const drawT0 = this._phaseTiming ? performance.now() : 0;
|
|
5172
5375
|
renderNode(this.root, 1, 0, 0, 1, 0, 0, 1);
|
|
5173
5376
|
for (const overlay of this.overlayRoot.children) {
|
|
5174
5377
|
renderNode(overlay, 1, 0, 0, 1, 0, 0, 1);
|
|
5175
5378
|
}
|
|
5176
5379
|
if (this._phaseTiming) this._recordPhase("drawWalk", performance.now() - drawT0);
|
|
5380
|
+
if (drawTiming) endVectoUserTiming(drawTiming);
|
|
5381
|
+
if (this._userTiming) {
|
|
5382
|
+
measureVectoUserTiming(VECTO_USER_TIMING.scene.entityPaint, userEntityPaintMs);
|
|
5383
|
+
}
|
|
5177
5384
|
if (isMainRenderer) {
|
|
5178
5385
|
this.frameHadAnimation = walkHadAnimation;
|
|
5179
5386
|
this.frameHadInteractive = walkHadInteractive;
|
|
5180
5387
|
this.reconcilePortals();
|
|
5181
5388
|
}
|
|
5389
|
+
const flushTiming = this._userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.flush) : null;
|
|
5182
5390
|
const flushT0 = this._phaseTiming ? performance.now() : 0;
|
|
5183
5391
|
renderer.flush();
|
|
5184
5392
|
if (isMainRenderer) {
|
|
@@ -5186,6 +5394,7 @@ var Scene = class _Scene {
|
|
|
5186
5394
|
}
|
|
5187
5395
|
renderer.present?.();
|
|
5188
5396
|
if (this._phaseTiming) this._recordPhase("flush", performance.now() - flushT0);
|
|
5397
|
+
if (flushTiming) endVectoUserTiming(flushTiming);
|
|
5189
5398
|
if (this._devActive) {
|
|
5190
5399
|
this._devFrameCount++;
|
|
5191
5400
|
this._devRunChecks();
|
|
@@ -6285,11 +6494,15 @@ export {
|
|
|
6285
6494
|
SplineEntity,
|
|
6286
6495
|
TextEntity,
|
|
6287
6496
|
TextRasterCache,
|
|
6497
|
+
VECTO_USER_TIMING,
|
|
6288
6498
|
VectoJSEvent,
|
|
6289
6499
|
WebGPUParticleSystemManager,
|
|
6500
|
+
beginVectoUserTiming,
|
|
6290
6501
|
createWebGLPointRenderer,
|
|
6502
|
+
endVectoUserTiming,
|
|
6291
6503
|
isSafeUrl,
|
|
6292
6504
|
loadSpline,
|
|
6505
|
+
measureVectoUserTiming,
|
|
6293
6506
|
parseColorToRGBA,
|
|
6294
6507
|
polySegmentToBezier,
|
|
6295
6508
|
sanitizeUrl
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Stable User Timing measure names emitted by VectoJS instrumentation. */
|
|
2
|
+
export declare const VECTO_USER_TIMING: {
|
|
3
|
+
readonly scene: {
|
|
4
|
+
readonly transform: 'vecto:scene:transform';
|
|
5
|
+
readonly drawWalk: 'vecto:scene:draw-walk';
|
|
6
|
+
readonly entityPaint: 'vecto:scene:entity-paint';
|
|
7
|
+
readonly flush: 'vecto:scene:flush';
|
|
8
|
+
readonly a11ySync: 'vecto:scene:a11y-sync';
|
|
9
|
+
};
|
|
10
|
+
readonly markdown: {
|
|
11
|
+
readonly parse: 'vecto:markdown:parse';
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
interface TimingPerformance {
|
|
15
|
+
now(): number;
|
|
16
|
+
mark(name: string, options?: {
|
|
17
|
+
startTime?: number;
|
|
18
|
+
}): unknown;
|
|
19
|
+
measure(name: string, startMark: string, endMark: string): unknown;
|
|
20
|
+
clearMarks?(name?: string): void;
|
|
21
|
+
}
|
|
22
|
+
/** Opaque handle for one enabled User Timing interval. */
|
|
23
|
+
export interface VectoUserTimingSpan {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly startMark: string;
|
|
26
|
+
readonly endMark: string;
|
|
27
|
+
readonly performance: TimingPerformance;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Begin a User Timing interval when the host implements marks and measures.
|
|
31
|
+
* Returns `null` instead of making optional profiling a runtime requirement.
|
|
32
|
+
*/
|
|
33
|
+
export declare function beginVectoUserTiming(name: string): VectoUserTimingSpan | null;
|
|
34
|
+
/** Finish a span and release its uniquely named marks. */
|
|
35
|
+
export declare function endVectoUserTiming(span: VectoUserTimingSpan | null): void;
|
|
36
|
+
/**
|
|
37
|
+
* Emit one measure for a duration accumulated from disjoint calls.
|
|
38
|
+
*
|
|
39
|
+
* The marks are anchored at the current time with the measured duration ending
|
|
40
|
+
* there. This keeps one entry per frame while still reporting the sum of every
|
|
41
|
+
* entity's paint call instead of instrumenting every entity individually.
|
|
42
|
+
*/
|
|
43
|
+
export declare function measureVectoUserTiming(name: string, durationMs: number): void;
|
|
44
|
+
export {};
|
package/dist/renderer.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
var
|
|
10
|
+
var _chunkKEBYJVD6js = require('./chunk-KEBYJVD6.js');
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
@@ -16,4 +16,4 @@ var _chunkLHONR3GOjs = require('./chunk-LHONR3GO.js');
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
exports.CanvasRenderer =
|
|
19
|
+
exports.CanvasRenderer = _chunkKEBYJVD6js.CanvasRenderer; exports.GlyphRasterAtlas = _chunkKEBYJVD6js.GlyphRasterAtlas; exports.SVGRenderer = _chunkKEBYJVD6js.SVGRenderer; exports.TextRasterCache = _chunkKEBYJVD6js.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkKEBYJVD6js.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkKEBYJVD6js.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkKEBYJVD6js.parseColorToRGBA;
|
package/dist/renderer.mjs
CHANGED
package/dist/tree/Scene.d.ts
CHANGED
|
@@ -176,6 +176,11 @@ export interface SceneOptions {
|
|
|
176
176
|
* and not marked dirty) to save power/CPU. Default is `true`.
|
|
177
177
|
*/
|
|
178
178
|
autoThrottle?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Emit User Timing marks and measures for render phases. Default `false`.
|
|
181
|
+
* Intended for short profiler captures; enable only while collecting one.
|
|
182
|
+
*/
|
|
183
|
+
userTiming?: boolean;
|
|
179
184
|
/**
|
|
180
185
|
* Mirror static text from entities implementing
|
|
181
186
|
* {@link Entity.getContentProjection} as transparent, position-synced DOM
|
|
@@ -341,6 +346,7 @@ export declare class Scene {
|
|
|
341
346
|
/** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
|
|
342
347
|
private static readonly MAX_DIRTY_REASONS;
|
|
343
348
|
private _phaseTiming;
|
|
349
|
+
private _userTiming;
|
|
344
350
|
private _phaseTotals;
|
|
345
351
|
/**
|
|
346
352
|
* Start or stop per-phase render timing.
|
|
@@ -358,6 +364,15 @@ export declare class Scene {
|
|
|
358
364
|
setPhaseTiming(enabled: boolean): void;
|
|
359
365
|
/** Whether per-phase render timing is being recorded. */
|
|
360
366
|
get phaseTiming(): boolean;
|
|
367
|
+
/**
|
|
368
|
+
* Enable or disable browser User Timing phase instrumentation.
|
|
369
|
+
*
|
|
370
|
+
* Off by default. The disabled frame path performs only boolean checks and
|
|
371
|
+
* emits no Performance Timeline entries.
|
|
372
|
+
*/
|
|
373
|
+
setUserTiming(enabled: boolean): void;
|
|
374
|
+
/** Whether browser User Timing phase instrumentation is enabled. */
|
|
375
|
+
get userTiming(): boolean;
|
|
361
376
|
/**
|
|
362
377
|
* Accumulate one phase sample.
|
|
363
378
|
*
|
|
@@ -521,6 +536,8 @@ export declare class Scene {
|
|
|
521
536
|
private fullViewportElements;
|
|
522
537
|
private normalElements;
|
|
523
538
|
private activeIds;
|
|
539
|
+
/** Per-parent insertion cursor, reused by `enforceA11yDomOrder`. */
|
|
540
|
+
private a11yOrderCursors;
|
|
524
541
|
private activePortalsThisFrame;
|
|
525
542
|
private activePortalsPrevFrame;
|
|
526
543
|
private portalEntities;
|
|
@@ -1258,12 +1275,23 @@ export declare class Scene {
|
|
|
1258
1275
|
private enforceA11yDomOrder;
|
|
1259
1276
|
/**
|
|
1260
1277
|
* Reorder `normalElements` (in place) into visual reading order using the
|
|
1261
|
-
*
|
|
1278
|
+
* positions `syncA11y` already wrote to each element's inline style
|
|
1262
1279
|
* (`top`/`left`/`height`). Elements are grouped into rows top-to-bottom (an
|
|
1263
1280
|
* element belongs to the current row while its top is above the row's
|
|
1264
1281
|
* running bottom edge), then sorted within a row by `left` — ascending for
|
|
1265
1282
|
* `'ltr'`, descending for `'rtl'`. The sort is stable, so entities at the
|
|
1266
1283
|
* same position keep their scene-graph (collection) order as a tiebreak.
|
|
1284
|
+
*
|
|
1285
|
+
* Those inline values are world coordinates for a top-level mirror but
|
|
1286
|
+
* PARENT-RELATIVE for a nested one, so this list mixes coordinate spaces.
|
|
1287
|
+
* That is sound because the result is only ever applied per DOM parent
|
|
1288
|
+
* ({@link enforceA11yDomOrder} advances a cursor per parent), and all of one
|
|
1289
|
+
* parent's children share one space: a `grid`'s rows are all grid-relative, a
|
|
1290
|
+
* `row`'s cells all row-relative. Comparisons ACROSS spaces do happen while
|
|
1291
|
+
* banding, but they only affect the relative order of elements in different
|
|
1292
|
+
* parents, which no `insertBefore` ever acts on. Normalizing everything back
|
|
1293
|
+
* to world coordinates here would cost a transform per element per frame to
|
|
1294
|
+
* change nothing observable.
|
|
1267
1295
|
*/
|
|
1268
1296
|
private sortNormalElementsVisually;
|
|
1269
1297
|
/** Keep DOM/WebGL overlay layers aligned with the canvas's CSS box. */
|
|
@@ -1281,6 +1309,26 @@ export declare class Scene {
|
|
|
1281
1309
|
/**
|
|
1282
1310
|
* Render the entire scene graph onto the specified renderer.
|
|
1283
1311
|
*
|
|
1312
|
+
* Main-frame causal order is a correctness contract:
|
|
1313
|
+
*
|
|
1314
|
+
* 1. Browser/input callbacks finish before the scheduled frame begins.
|
|
1315
|
+
* 2. Batched property drivers and particle simulation advance.
|
|
1316
|
+
* 3. Entity `update()` hooks run.
|
|
1317
|
+
* 4. Transform inputs are gathered and world matrices are composed.
|
|
1318
|
+
* 5. Updated world bounds are tested for culling.
|
|
1319
|
+
* 6. Visible entities paint in scene-graph order.
|
|
1320
|
+
* 7. Canvas/GPU batches flush and retained renderers present.
|
|
1321
|
+
* 8. The rAF loop synchronizes content and accessibility projections after
|
|
1322
|
+
* this method returns.
|
|
1323
|
+
*
|
|
1324
|
+
* The causal order is fixed; physical walks may stay fused. The JavaScript
|
|
1325
|
+
* transform path interleaves update → compose → cull → paint per node in
|
|
1326
|
+
* pre-order. The WASM path updates the whole tree first, then gathers and
|
|
1327
|
+
* composes it in one store pass before the same cull/paint walk. Both must
|
|
1328
|
+
* expose an update's transform mutation in that same rendered frame.
|
|
1329
|
+
* Secondary renderers are read-only snapshots: they skip simulation and
|
|
1330
|
+
* updates, then compose/cull/paint/flush the current state.
|
|
1331
|
+
*
|
|
1284
1332
|
* @param renderer - The renderer instance to draw to.
|
|
1285
1333
|
* @param dt - Delta time in milliseconds (default 0).
|
|
1286
1334
|
* @param time - Current absolute time in milliseconds (default 0).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vectojs/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"@vectojs/text": "^0.2.0"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
+
"@guidepup/virtual-screen-reader": "0.32.1",
|
|
73
74
|
"@vitest/coverage-v8": "^4.1.10",
|
|
74
75
|
"esbuild": "^0.28.1",
|
|
75
76
|
"jsdom": "^29.1.1",
|