@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/index.mjs CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  isSafeUrl,
9
9
  parseColorToRGBA,
10
10
  sanitizeUrl
11
- } from "./chunk-UPULSLKA.mjs";
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 === this.a11yRoot) {
3785
+ if (el.parentNode) {
3656
3786
  this.preserveFocusOnRemoval(el);
3657
- this.a11yRoot.removeChild(el);
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
- el.style.left = `${e + node.a11yOffsetX}px`;
3976
- el.style.top = `${f + node.a11yOffsetY}px`;
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
- el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
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 === this.a11yRoot) {
4709
+ if (el.parentNode) {
4554
4710
  this.preserveFocusOnRemoval(el);
4555
- this.a11yRoot.removeChild(el);
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 current = this.a11yRoot.childNodes[i];
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
- this.a11yRoot.insertBefore(expected, current || null);
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
- * world positions `syncA11y` already wrote to each element's inline style
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
- node.render(renderer);
5156
- this._recordPhase("entityPaint", performance.now() - t0);
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 _chunkLHONR3GOjs = require('./chunk-LHONR3GO.js');
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 = _chunkLHONR3GOjs.CanvasRenderer; exports.GlyphRasterAtlas = _chunkLHONR3GOjs.GlyphRasterAtlas; exports.SVGRenderer = _chunkLHONR3GOjs.SVGRenderer; exports.TextRasterCache = _chunkLHONR3GOjs.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkLHONR3GOjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkLHONR3GOjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkLHONR3GOjs.parseColorToRGBA;
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
@@ -6,7 +6,7 @@ import {
6
6
  WebGPUParticleSystemManager,
7
7
  createWebGLPointRenderer,
8
8
  parseColorToRGBA
9
- } from "./chunk-UPULSLKA.mjs";
9
+ } from "./chunk-ME4LB2HB.mjs";
10
10
  export {
11
11
  CanvasRenderer,
12
12
  GlyphRasterAtlas,
@@ -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
- * world positions `syncA11y` already wrote to each element's inline style
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.23.0",
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",