@vectojs/core 1.22.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.
@@ -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,
@@ -143,8 +143,12 @@ export declare class ComputeParticleEntity extends Entity {
143
143
  * The backend holds one resident SoA store, so a Scene with multiple particle
144
144
  * entities reuses it sequentially — origin is therefore re-gathered each call
145
145
  * (not upload-once), a couple of extra f32 reads per particle.
146
+ *
147
+ * Returns `false` if the kernel declined the call and {@link updateCPU} ran
148
+ * instead, so the Scene can report which path actually simulated this frame
149
+ * rather than assuming an installed backend did the work.
146
150
  */
147
- stepWithBackend(backend: ParticleBackend, dt: number, mouseX: number, mouseY: number, width: number, height: number): void;
151
+ stepWithBackend(backend: ParticleBackend, dt: number, mouseX: number, mouseY: number, width: number, height: number): boolean;
148
152
  destroy(): void;
149
153
  /**
150
154
  * Frees all GPU resources allocated for WebGPU simulation.
@@ -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
@@ -208,6 +213,60 @@ export interface SceneOptions {
208
213
  }
209
214
  /** Frame-rate the loop is capped to when the OS requests reduced motion. */
210
215
  export declare const REDUCED_MOTION_FPS = 30;
216
+ /**
217
+ * Why an accelerator did or did not run on the most recent frame.
218
+ *
219
+ * `'active'` is the only value that means the accelerator ran. Everything else
220
+ * is a distinct decline, kept separate because they call for different actions:
221
+ * `'not-installed'` means enable it, `'below-gate'` means the workload is too
222
+ * small to be worth it (working as designed), and `'rejected'` means the kernel
223
+ * refused its arguments — a fault worth reporting, not a tuning outcome.
224
+ */
225
+ export type AcceleratorReason =
226
+ /** Ran on this frame. */
227
+ 'active'
228
+ /** No backend installed; the JS path is the permanent fallback. */
229
+ | 'not-installed'
230
+ /** Installed, but the per-frame gate chose JS (workload below threshold). */
231
+ | 'below-gate'
232
+ /** Installed and gated in, but the kernel rejected the call and wrote nothing. */
233
+ | 'rejected'
234
+ /** Not applicable to this pass (e.g. a non-main renderer, or nothing to do). */
235
+ | 'not-applicable';
236
+ /**
237
+ * One accelerator's per-frame status, read from {@link Scene.accelerators}.
238
+ *
239
+ * The pair exists because `available` and `activeThisFrame` genuinely differ:
240
+ * before this shape, `transformBackend`/`animBackend` reported only that a
241
+ * backend was *installed*, which invites concluding an accelerator is doing work
242
+ * when its gate never opens. Read `activeThisFrame` for what actually happened
243
+ * and `reason` for why.
244
+ */
245
+ export interface AcceleratorStatus {
246
+ /** A backend is installed and could run, gate permitting. */
247
+ available: boolean;
248
+ /** It ran on the most recent frame. */
249
+ activeThisFrame: boolean;
250
+ /** Why it did or did not run. */
251
+ reason: AcceleratorReason;
252
+ /** Which implementation actually did the work on the most recent frame. */
253
+ path: string;
254
+ }
255
+ /**
256
+ * Per-frame status of every invisible accelerator, read from
257
+ * {@link Scene.accelerators}. Each is independent: a scene can compose
258
+ * transforms in WASM while ticking drivers in JS.
259
+ */
260
+ export interface AcceleratorReport {
261
+ /** World-matrix composition (`compose_simd`). */
262
+ transform: AcceleratorStatus;
263
+ /** Batched property drivers (`spring_step`/`tween_step`). */
264
+ animation: AcceleratorStatus;
265
+ /** Hit-test broad phase (`hit_build`/`hit_query`) and its gather source. */
266
+ hitTest: AcceleratorStatus;
267
+ /** Particle simulation — WebGPU compute, the WASM CPU kernel, or JS. */
268
+ particle: AcceleratorStatus;
269
+ }
211
270
  /**
212
271
  * Live render-loop telemetry, read from {@link Scene.frameStats}. See that
213
272
  * getter for how each field is measured.
@@ -287,6 +346,7 @@ export declare class Scene {
287
346
  /** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
288
347
  private static readonly MAX_DIRTY_REASONS;
289
348
  private _phaseTiming;
349
+ private _userTiming;
290
350
  private _phaseTotals;
291
351
  /**
292
352
  * Start or stop per-phase render timing.
@@ -304,6 +364,15 @@ export declare class Scene {
304
364
  setPhaseTiming(enabled: boolean): void;
305
365
  /** Whether per-phase render timing is being recorded. */
306
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;
307
376
  /**
308
377
  * Accumulate one phase sample.
309
378
  *
@@ -467,6 +536,8 @@ export declare class Scene {
467
536
  private fullViewportElements;
468
537
  private normalElements;
469
538
  private activeIds;
539
+ /** Per-parent insertion cursor, reused by `enforceA11yDomOrder`. */
540
+ private a11yOrderCursors;
470
541
  private activePortalsThisFrame;
471
542
  private activePortalsPrevFrame;
472
543
  private portalEntities;
@@ -574,6 +645,41 @@ export declare class Scene {
574
645
  private _wasmAabbsFresh;
575
646
  /** Did the last hit-grid build use the fused (WASM-store) gather? */
576
647
  get hitGatherPath(): 'fused' | 'js';
648
+ /**
649
+ * Why the transform accelerator did or did not run on the most recent frame.
650
+ * Written by the render walk and `_syncWasmStore`.
651
+ */
652
+ private _transformReason;
653
+ /** Why the batched-driver accelerator did or did not run. */
654
+ private _animReason;
655
+ /**
656
+ * Why the hit-test accelerator did or did not serve the last pointer query.
657
+ * The grid is built lazily on demand, not every frame, so this describes the
658
+ * most recent BUILD. Starts at `'not-installed'` because that is the truth
659
+ * before a backend exists; `_ensureHitGrid` moves it to `'not-applicable'`
660
+ * once one is installed but nothing has queried yet.
661
+ */
662
+ private _hitReason;
663
+ /** Why the particle accelerator did or did not run. */
664
+ private _particleReason;
665
+ /** Which particle implementation actually simulated the most recent frame. */
666
+ private _particlePath;
667
+ /**
668
+ * Per-frame status of every invisible accelerator: whether each is installed,
669
+ * whether it actually ran on the most recent frame, and why.
670
+ *
671
+ * This exists because the older per-accelerator getters
672
+ * ({@link transformBackend}, {@link animBackend}, {@link hitTestBackend},
673
+ * {@link particleBackend}) report only that a backend is INSTALLED. Reading
674
+ * `'wasm'` from one of those and concluding the accelerator is doing work is
675
+ * wrong whenever a gate never opens, a kernel rejects its arguments, or a
676
+ * faster backend takes the pass instead. Read {@link AcceleratorStatus.reason}
677
+ * for which of those happened.
678
+ *
679
+ * Reflects the most recent main-renderer frame; a secondary renderer (SVG
680
+ * export, offscreen snapshot) does not overwrite it.
681
+ */
682
+ get accelerators(): AcceleratorReport;
577
683
  /** Which backend answers `findEntityAt` for the main tree. */
578
684
  get hitTestBackend(): 'js' | 'wasm';
579
685
  /** Install (or clear) a WASM hit-test backend directly. Prefer
@@ -1169,12 +1275,23 @@ export declare class Scene {
1169
1275
  private enforceA11yDomOrder;
1170
1276
  /**
1171
1277
  * Reorder `normalElements` (in place) into visual reading order using the
1172
- * world positions `syncA11y` already wrote to each element's inline style
1278
+ * positions `syncA11y` already wrote to each element's inline style
1173
1279
  * (`top`/`left`/`height`). Elements are grouped into rows top-to-bottom (an
1174
1280
  * element belongs to the current row while its top is above the row's
1175
1281
  * running bottom edge), then sorted within a row by `left` — ascending for
1176
1282
  * `'ltr'`, descending for `'rtl'`. The sort is stable, so entities at the
1177
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.
1178
1295
  */
1179
1296
  private sortNormalElementsVisually;
1180
1297
  /** Keep DOM/WebGL overlay layers aligned with the canvas's CSS box. */
@@ -1192,6 +1309,26 @@ export declare class Scene {
1192
1309
  /**
1193
1310
  * Render the entire scene graph onto the specified renderer.
1194
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
+ *
1195
1332
  * @param renderer - The renderer instance to draw to.
1196
1333
  * @param dt - Delta time in milliseconds (default 0).
1197
1334
  * @param time - Current absolute time in milliseconds (default 0).
@@ -56,10 +56,27 @@ export declare class AnimBackend {
56
56
  * writing first and sizing after would write into a stale buffer.
57
57
  */
58
58
  ensure(springCount: number, tweenCount: number): void;
59
- /** Advance `count` springs (from index 0) by `dtMs` milliseconds, in place. */
60
- stepSprings(dtMs: number, count: number): void;
61
- /** Advance `count` tweens (from index 0) by `dtMs` milliseconds, writing `val`. */
62
- stepTweens(dtMs: number, count: number): void;
59
+ /**
60
+ * Advance `count` springs (from index 0) by `dtMs` milliseconds, in place.
61
+ * Returns `true` when the kernel ran. `false` means it rejected the call
62
+ * (count beyond the capacity {@link ensure} allocated, or no `anim_init` yet)
63
+ * and wrote nothing, so the caller must tick those drivers in JS instead of
64
+ * scattering back a pack the kernel never touched. See {@link lastStatus}.
65
+ */
66
+ stepSprings(dtMs: number, count: number): boolean;
67
+ /**
68
+ * Advance `count` tweens (from index 0) by `dtMs` milliseconds, writing `val`.
69
+ * Returns `true` when the kernel ran; `false` means it rejected the call and
70
+ * wrote nothing (see {@link stepSprings}). `elapsed` is kernel-side state, so
71
+ * a rejected tween pack must not be read back — it is unadvanced, not
72
+ * partially advanced.
73
+ */
74
+ stepTweens(dtMs: number, count: number): boolean;
75
+ /**
76
+ * Status of the most recent kernel call — `WASM_STATUS.OK` unless the kernel
77
+ * declined it. Mirrors {@link TransformBackend.lastStatus}.
78
+ */
79
+ lastStatus: number;
63
80
  private refreshViews;
64
81
  }
65
82
  /**
@@ -84,8 +84,11 @@ export declare class WasmTransformBackend {
84
84
  */
85
85
  runKernel(kernel?: Kernel): number;
86
86
  /** Upload only the run table + count (topology), leaving per-entity inputs to
87
- * the resident views. Call when the tree structure changes, not per frame. */
88
- uploadRuns(store: TransformStore): void;
87
+ * the resident views. Call when the tree structure changes, not per frame.
88
+ * Returns `false` if the crate rejected the run count, in which case the
89
+ * PREVIOUS topology is still published and any kernel run against it would
90
+ * compose the wrong tree — the caller must not proceed. */
91
+ uploadRuns(store: TransformStore): boolean;
89
92
  /**
90
93
  * Status of the most recent kernel or run-table call. `WASM_STATUS.OK` unless
91
94
  * the crate rejected its arguments, in which case that call was a no-op.
@@ -125,8 +128,10 @@ export declare class WasmTransformBackend {
125
128
  computeAabbs(store: TransformStore): void;
126
129
  /** Run the AABB pass only, over `count` entities already resident in wasm
127
130
  * memory (bounds written via {@link boundsView}, world matrices already
128
- * composed). No upload/readback — the per-frame resident path. */
129
- runAabbs(count: number): void;
131
+ * composed). No upload/readback — the per-frame resident path. Returns
132
+ * `false` if the kernel rejected `count` (beyond capacity, or uninitialized),
133
+ * in which case {@link aabbView} still holds the previous frame's bounds. */
134
+ runAabbs(count: number): boolean;
130
135
  /** Resident wasm local-bounds input views (`bx,by,bw,bh`) for the AABB pass. */
131
136
  boundsView(): {
132
137
  bx: Float64Array;
@@ -71,8 +71,19 @@ export declare class ParticleBackend {
71
71
  * Advance `count` particles one step in place. Returns `true` when at least
72
72
  * one live particle is still moving or off-origin beyond epsilon (the fused
73
73
  * `hasPendingAnimations` flag), so the caller need not re-scan the buffer.
74
+ *
75
+ * Returns `null` when the kernel REJECTED the call — `count` beyond the
76
+ * capacity {@link ensure} allocated, or no `particle_init` yet. Nothing was
77
+ * written, so the caller must NOT {@link scatter} (that would write the
78
+ * gathered pre-step values back and freeze the simulation) and should fall
79
+ * back to the JS `updateCPU` path for this frame. See {@link lastStatus}.
74
80
  */
75
- step(count: number, p: ParticleStepParams): boolean;
81
+ step(count: number, p: ParticleStepParams): boolean | null;
82
+ /**
83
+ * Status of the most recent {@link step} — `WASM_STATUS.OK` unless the kernel
84
+ * declined it. Mirrors {@link TransformBackend.lastStatus}.
85
+ */
86
+ lastStatus: number;
76
87
  /** Transpose the AoS stride-8 buffer into the SoA views (position/velocity/
77
88
  * life every frame; origin upload-once when `withOrigin`). */
78
89
  gather(data: Float32Array, count: number, withOrigin: boolean): void;
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.22.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",