@vectojs/core 1.19.0 → 1.20.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
@@ -13,7 +13,7 @@ import {
13
13
  MSDFTextEntity,
14
14
  SVGEntity,
15
15
  VectoJSEvent
16
- } from "./chunk-XWBVBXFZ.mjs";
16
+ } from "./chunk-TJCXB2F6.mjs";
17
17
 
18
18
  // src/tree/Scene.ts
19
19
  import { SpringDriver, TweenDriver } from "@vectojs/animation";
@@ -1538,6 +1538,70 @@ var Scene = class _Scene {
1538
1538
  renderMode = "always";
1539
1539
  /** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
1540
1540
  static MAX_DIRTY_REASONS = 200;
1541
+ _phaseTiming = false;
1542
+ _phaseTotals = /* @__PURE__ */ new Map();
1543
+ /**
1544
+ * Start or stop per-phase render timing.
1545
+ *
1546
+ * Off by default, and the probes compile to a single boolean test when off:
1547
+ * these sit on the frame path, so the disabled cost has to be nothing. Enable,
1548
+ * run the scene, then read {@link renderPhases}.
1549
+ *
1550
+ * Exists because a frame total cannot tell you where the time went. The
1551
+ * markdown streaming benchmark put render at 85-99% of an append's cost, and
1552
+ * there was no way to decompose that number further — which is exactly the
1553
+ * position that led to two wrong optimisation guesses earlier
1554
+ * (`CodeBlock` reuse, hit-grid fusion), both of which measured as no change.
1555
+ */
1556
+ setPhaseTiming(enabled) {
1557
+ this._phaseTiming = enabled;
1558
+ if (!enabled) this.clearRenderPhases();
1559
+ }
1560
+ /** Whether per-phase render timing is being recorded. */
1561
+ get phaseTiming() {
1562
+ return this._phaseTiming;
1563
+ }
1564
+ /**
1565
+ * Accumulate one phase sample.
1566
+ *
1567
+ * Totals rather than a per-frame log: the question is always "which phase owns
1568
+ * the frame", and a log of thousands of samples answers it less directly while
1569
+ * costing far more memory. `maxMs` is kept because a phase that is cheap on
1570
+ * average but spikes is a different problem from one that is uniformly slow.
1571
+ */
1572
+ _recordPhase(phase, ms) {
1573
+ const existing = this._phaseTotals.get(phase);
1574
+ if (existing) {
1575
+ existing.totalMs += ms;
1576
+ existing.calls++;
1577
+ if (ms > existing.maxMs) existing.maxMs = ms;
1578
+ return;
1579
+ }
1580
+ this._phaseTotals.set(phase, { totalMs: ms, calls: 1, maxMs: ms });
1581
+ }
1582
+ /**
1583
+ * Recorded phase timings, most expensive first, with each phase's share of the
1584
+ * measured total.
1585
+ *
1586
+ * `share` is the number that matters: a phase at 4% cannot be worth optimising
1587
+ * however inefficient it looks in isolation.
1588
+ */
1589
+ get renderPhases() {
1590
+ const entries = [...this._phaseTotals.entries()];
1591
+ const denominator = entries.filter(([phase]) => phase !== "render").reduce((sum, [, v]) => sum + v.totalMs, 0);
1592
+ return entries.map(([phase, v]) => ({
1593
+ phase,
1594
+ totalMs: +v.totalMs.toFixed(3),
1595
+ calls: v.calls,
1596
+ avgMs: +(v.totalMs / Math.max(1, v.calls)).toFixed(4),
1597
+ maxMs: +v.maxMs.toFixed(3),
1598
+ share: phase === "render" ? null : +(100 * v.totalMs / Math.max(1e-9, denominator)).toFixed(1)
1599
+ })).sort((a, b) => b.totalMs - a.totalMs);
1600
+ }
1601
+ /** Drop recorded phase timings, keeping timing enabled. */
1602
+ clearRenderPhases() {
1603
+ this._phaseTotals.clear();
1604
+ }
1541
1605
  _dirtyTracking = false;
1542
1606
  _dirtyReasons = /* @__PURE__ */ new Map();
1543
1607
  dirty = true;
@@ -2834,6 +2898,20 @@ var Scene = class _Scene {
2834
2898
  delete el.dataset.vectoGridCalibrationMs;
2835
2899
  this.releaseContentSelectionForRebuild(el);
2836
2900
  }
2901
+ /**
2902
+ * Drop any projected elements under `node` without touching the entity tree.
2903
+ *
2904
+ * Used when the walk reaches an invisible subtree: the entities stay put (a
2905
+ * later `show()` re-projects them), but nothing under here may remain
2906
+ * focusable or announced while hidden.
2907
+ */
2908
+ pruneA11ySubtree(node) {
2909
+ if (this.a11yElements.has(node.id) || this.contentElements.has(node.id)) {
2910
+ this.removeA11yRecursively(node);
2911
+ return;
2912
+ }
2913
+ for (const child of node.children) this.pruneA11ySubtree(child);
2914
+ }
2837
2915
  removeA11yRecursively(node) {
2838
2916
  if (node.isDOMPortal) {
2839
2917
  node.releaseDOMBindings();
@@ -3151,7 +3229,9 @@ var Scene = class _Scene {
3151
3229
  step(dt) {
3152
3230
  const time = this.lastTime + dt;
3153
3231
  this.lastTime = time;
3232
+ const t0 = this._phaseTiming ? performance.now() : 0;
3154
3233
  this.render(this.renderer, dt, time);
3234
+ if (this._phaseTiming) this._recordPhase("render", performance.now() - t0);
3155
3235
  this.dirty = false;
3156
3236
  }
3157
3237
  /**
@@ -3286,6 +3366,10 @@ var Scene = class _Scene {
3286
3366
  if (node.isDOMPortal) {
3287
3367
  return;
3288
3368
  }
3369
+ if (node.a11yHidden) {
3370
+ this.pruneA11ySubtree(node);
3371
+ return;
3372
+ }
3289
3373
  if (this.shouldProjectA11y(node)) {
3290
3374
  let el = this.a11yElements.get(node.id);
3291
3375
  const attrs = node.getA11yAttributes();
@@ -4372,7 +4456,9 @@ var Scene = class _Scene {
4372
4456
  }
4373
4457
  this._lastRenderTick = time;
4374
4458
  this._lastDt = dt;
4459
+ const phaseClock = this._phaseTiming ? performance.now() : 0;
4375
4460
  this.render(this.renderer, dt, time);
4461
+ if (this._phaseTiming) this._recordPhase("render", performance.now() - phaseClock);
4376
4462
  this._lastFrameMs = (typeof performance !== "undefined" ? performance.now() : time) - now;
4377
4463
  this._renderedFrames++;
4378
4464
  const hasActiveAnimation = this.frameHadAnimation;
@@ -4382,9 +4468,13 @@ var Scene = class _Scene {
4382
4468
  if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
4383
4469
  this.lastA11ySync = time;
4384
4470
  if (hasInteractive || wantsContentSync) {
4471
+ const t0 = this._phaseTiming ? performance.now() : 0;
4385
4472
  this.syncA11y(this.root);
4473
+ if (this._phaseTiming) this._recordPhase("a11ySync", performance.now() - t0);
4386
4474
  }
4475
+ const t1 = this._phaseTiming ? performance.now() : 0;
4387
4476
  this.enforceA11yDomOrder();
4477
+ if (this._phaseTiming) this._recordPhase("a11yOrder", performance.now() - t1);
4388
4478
  this.a11yPendingSyncAfterAnimation = hasActiveAnimation;
4389
4479
  } else if (hasActiveAnimation) {
4390
4480
  this.a11yPendingSyncAfterAnimation = true;
@@ -4558,7 +4648,9 @@ var Scene = class _Scene {
4558
4648
  updateWalk(this.root);
4559
4649
  for (const overlay of this.overlayRoot.children) updateWalk(overlay);
4560
4650
  }
4651
+ const wasmT0 = this._phaseTiming ? performance.now() : 0;
4561
4652
  const wasmWorld = wasmMain ? this._syncWasmStore() : null;
4653
+ if (this._phaseTiming) this._recordPhase("transform", performance.now() - wasmT0);
4562
4654
  const wasmSlotEntity = this._slotEntity;
4563
4655
  const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
4564
4656
  if (isMainRenderer && !wasmMain) {
@@ -4688,7 +4780,13 @@ var Scene = class _Scene {
4688
4780
  );
4689
4781
  }
4690
4782
  } else {
4691
- node.render(renderer);
4783
+ if (this._phaseTiming) {
4784
+ const t0 = performance.now();
4785
+ node.render(renderer);
4786
+ this._recordPhase("entityPaint", performance.now() - t0);
4787
+ } else {
4788
+ node.render(renderer);
4789
+ }
4692
4790
  }
4693
4791
  }
4694
4792
  if (node.clipChildren) {
@@ -4700,20 +4798,24 @@ var Scene = class _Scene {
4700
4798
  renderer.flush();
4701
4799
  renderer.restore();
4702
4800
  };
4801
+ const drawT0 = this._phaseTiming ? performance.now() : 0;
4703
4802
  renderNode(this.root, 1, 0, 0, 1, 0, 0, 1);
4704
4803
  for (const overlay of this.overlayRoot.children) {
4705
4804
  renderNode(overlay, 1, 0, 0, 1, 0, 0, 1);
4706
4805
  }
4806
+ if (this._phaseTiming) this._recordPhase("drawWalk", performance.now() - drawT0);
4707
4807
  if (isMainRenderer) {
4708
4808
  this.frameHadAnimation = walkHadAnimation;
4709
4809
  this.frameHadInteractive = walkHadInteractive;
4710
4810
  this.reconcilePortals();
4711
4811
  }
4812
+ const flushT0 = this._phaseTiming ? performance.now() : 0;
4712
4813
  renderer.flush();
4713
4814
  if (isMainRenderer) {
4714
4815
  this.pointRenderer?.flush();
4715
4816
  }
4716
4817
  renderer.present?.();
4818
+ if (this._phaseTiming) this._recordPhase("flush", performance.now() - flushT0);
4717
4819
  if (this._devActive) {
4718
4820
  this._devFrameCount++;
4719
4821
  this._devRunChecks();
package/dist/text.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkIA3KW4CGjs = require('./chunk-IA3KW4CG.js');
5
+ var _chunkYLH7F4ZVjs = require('./chunk-YLH7F4ZV.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 = _chunkIA3KW4CGjs.MSDFTextEntity; exports.SVGEntity = _chunkIA3KW4CGjs.SVGEntity;
12
+ exports.MSDFTextEntity = _chunkYLH7F4ZVjs.MSDFTextEntity; exports.SVGEntity = _chunkYLH7F4ZVjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MSDFTextEntity,
3
3
  SVGEntity
4
- } from "./chunk-XWBVBXFZ.mjs";
4
+ } from "./chunk-TJCXB2F6.mjs";
5
5
 
6
6
  // src/text/index.ts
7
7
  export * from "@vectojs/text";
@@ -416,6 +416,24 @@ export declare abstract class Entity {
416
416
  * nodes, so on-top components stay clickable.
417
417
  */
418
418
  a11yFullViewport: boolean;
419
+ /**
420
+ * Hide this entity AND its whole subtree from the accessibility/automation
421
+ * projection, regardless of each node's own `interactive` flag.
422
+ *
423
+ * For a container that is logically closed while still mounted — an `Overlay`
424
+ * after `hide()`, a collapsed panel kept in the tree for its transition. Setting
425
+ * `interactive = false` on the container alone is not enough: the projection walk
426
+ * still descends, and any still-interactive child is re-created on the next
427
+ * frame. Measured before this existed: after `Popover.hide()` the popover's own
428
+ * element was gone while its button stayed projected with `tabIndex: 0` and a
429
+ * live box, so a keyboard user could Tab into a hidden popover.
430
+ *
431
+ * Deliberately NOT inferred from `opacity`: `Overlay.hide()` springs opacity
432
+ * toward 0, so mid-transition it reads nonzero (~0.26 when measured) and an
433
+ * `=== 0` test never fires; a threshold would instead silently un-project a
434
+ * faint-but-live control.
435
+ */
436
+ a11yHidden: boolean;
419
437
  /**
420
438
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
421
439
  * rendering. Combined with translating a content child, this is how
@@ -17,6 +17,27 @@ import { type CoreWasmRuntime } from '../wasm/runtime';
17
17
  import { type HitModuleSource, type HitTestBackend } from '../wasm/hit-backend';
18
18
  import { type AnimModuleSource, type AnimBackend } from '../wasm/anim-backend';
19
19
  import { type ParticleModuleSource, type ParticleBackend } from '../wasm/particle-backend';
20
+ /**
21
+ * A timed phase of a frame.
22
+ *
23
+ * `render` is the ENCLOSING phase — it contains `transform`, `drawWalk` and
24
+ * `flush` — so it is reported without a share to avoid double-counting.
25
+ * `a11ySync` and `a11yOrder` run after `render` in the frame loop, so they are
26
+ * siblings of it, not children.
27
+ */
28
+ export type RenderPhase = 'render' | 'transform' | 'drawWalk' | 'flush' | 'a11ySync' | 'a11yOrder'
29
+ /** Sum of every entity's own render(), nested inside drawWalk. */
30
+ | 'entityPaint';
31
+ export interface RenderPhaseEntry {
32
+ phase: RenderPhase;
33
+ totalMs: number;
34
+ calls: number;
35
+ avgMs: number;
36
+ /** Worst single sample — a spiky phase is a different problem from a slow one. */
37
+ maxMs: number;
38
+ /** Percent of the measured total, or `null` for the enclosing `render` phase. */
39
+ share: number | null;
40
+ }
20
41
  /**
21
42
  * Who marked the scene dirty, and why.
22
43
  *
@@ -230,6 +251,43 @@ export declare class Scene {
230
251
  renderMode: 'always' | 'onDemand';
231
252
  /** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
232
253
  private static readonly MAX_DIRTY_REASONS;
254
+ private _phaseTiming;
255
+ private _phaseTotals;
256
+ /**
257
+ * Start or stop per-phase render timing.
258
+ *
259
+ * Off by default, and the probes compile to a single boolean test when off:
260
+ * these sit on the frame path, so the disabled cost has to be nothing. Enable,
261
+ * run the scene, then read {@link renderPhases}.
262
+ *
263
+ * Exists because a frame total cannot tell you where the time went. The
264
+ * markdown streaming benchmark put render at 85-99% of an append's cost, and
265
+ * there was no way to decompose that number further — which is exactly the
266
+ * position that led to two wrong optimisation guesses earlier
267
+ * (`CodeBlock` reuse, hit-grid fusion), both of which measured as no change.
268
+ */
269
+ setPhaseTiming(enabled: boolean): void;
270
+ /** Whether per-phase render timing is being recorded. */
271
+ get phaseTiming(): boolean;
272
+ /**
273
+ * Accumulate one phase sample.
274
+ *
275
+ * Totals rather than a per-frame log: the question is always "which phase owns
276
+ * the frame", and a log of thousands of samples answers it less directly while
277
+ * costing far more memory. `maxMs` is kept because a phase that is cheap on
278
+ * average but spikes is a different problem from one that is uniformly slow.
279
+ */
280
+ private _recordPhase;
281
+ /**
282
+ * Recorded phase timings, most expensive first, with each phase's share of the
283
+ * measured total.
284
+ *
285
+ * `share` is the number that matters: a phase at 4% cannot be worth optimising
286
+ * however inefficient it looks in isolation.
287
+ */
288
+ get renderPhases(): RenderPhaseEntry[];
289
+ /** Drop recorded phase timings, keeping timing enabled. */
290
+ clearRenderPhases(): void;
233
291
  private _dirtyTracking;
234
292
  private _dirtyReasons;
235
293
  private dirty;
@@ -779,6 +837,14 @@ export declare class Scene {
779
837
  */
780
838
  add(entity: Entity): this;
781
839
  private clearContentGridState;
840
+ /**
841
+ * Drop any projected elements under `node` without touching the entity tree.
842
+ *
843
+ * Used when the walk reaches an invisible subtree: the entities stay put (a
844
+ * later `show()` re-projects them), but nothing under here may remain
845
+ * focusable or announced while hidden.
846
+ */
847
+ private pruneA11ySubtree;
782
848
  private removeA11yRecursively;
783
849
  /**
784
850
  * If `el` is about to be removed from the DOM while it holds browser focus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },