@vectojs/core 1.35.0 → 1.35.2

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
@@ -11,14 +11,14 @@ import {
11
11
  parseColorToRGBA,
12
12
  sanitizeUrl,
13
13
  setRendererDevMode
14
- } from "./chunk-IFSFHYF7.mjs";
14
+ } from "./chunk-25PS5K7R.mjs";
15
15
  import {
16
16
  Entity,
17
17
  MSDFTextEntity,
18
18
  SVGEntity,
19
19
  VectoJSEvent,
20
20
  contentLineInHint
21
- } from "./chunk-TV4ARAC5.mjs";
21
+ } from "./chunk-CHDJEHML.mjs";
22
22
 
23
23
  // src/tree/ComputeParticleEntity.ts
24
24
  var PARTICLE_STRIDE_FLOATS = 8;
@@ -316,6 +316,7 @@ var ComputeParticleEntity = class extends Entity {
316
316
  stepWithBackend(backend, dt, mouseX, mouseY, width, height) {
317
317
  const count = this.maxParticles;
318
318
  backend.ensure(count);
319
+ backend.revalidateViews();
319
320
  backend.gather(this.particleData, count, true);
320
321
  const pending = backend.step(count, {
321
322
  dt,
@@ -1325,6 +1326,7 @@ var DriverTicker = class {
1325
1326
  tP.length = tweenCount;
1326
1327
  tD.length = tweenCount;
1327
1328
  backend.ensure(springCount, tweenCount);
1329
+ backend.revalidateViews();
1328
1330
  if (springCount > 0) {
1329
1331
  const sv = backend.springView();
1330
1332
  for (let i = 0; i < springCount; i++) {
@@ -1593,6 +1595,7 @@ var HitTester = class {
1593
1595
  gathered = gatherHitAABBs(this.root, frame);
1594
1596
  }
1595
1597
  backend.ensure(gathered.count, width, height, 64);
1598
+ backend.revalidateViews();
1596
1599
  const view = backend.inputView();
1597
1600
  view.minx.set(gathered.minx.subarray(0, gathered.count));
1598
1601
  view.miny.set(gathered.miny.subarray(0, gathered.count));
@@ -2123,10 +2126,15 @@ var ContentProjectionManager = class {
2123
2126
  }
2124
2127
  if (this.phases.enabled) this.phases.record("calibScan", performance.now() - scanStart);
2125
2128
  if (measurements.length === 0) {
2126
- el.dataset.vectoGridCalibration = calibrationKey;
2127
- el.dataset.vectoGridReady = "true";
2128
2129
  el.dataset.vectoGridCalibrationSamples = "0";
2129
2130
  delete el.dataset.vectoGridCalibrationPending;
2131
+ const readyFrame = requestAnimationFrame(() => {
2132
+ this.calibrationFrameHandles.delete(entityId);
2133
+ if (!el.isConnected) return;
2134
+ el.dataset.vectoGridCalibration = calibrationKey;
2135
+ el.dataset.vectoGridReady = "true";
2136
+ });
2137
+ this.calibrationFrameHandles.set(entityId, readyFrame);
2130
2138
  return;
2131
2139
  }
2132
2140
  const appendStart = this.phases.enabled ? performance.now() : 0;
@@ -2369,8 +2377,15 @@ var WASM_STATUS = {
2369
2377
  /** A kernel ran before `init`. */
2370
2378
  UNINITIALIZED: 2,
2371
2379
  /** A sibling run addressed a slot or parent outside the store. */
2372
- BAD_RUN: 3
2380
+ BAD_RUN: 3,
2381
+ /** A requested capacity's size arithmetic overflowed; the previous store
2382
+ * was left untouched. */
2383
+ OVERFLOW: 4
2373
2384
  };
2385
+ function viewsStale(cap, probe, memory) {
2386
+ if (cap === 0) return false;
2387
+ return probe.byteLength === 0 || probe.buffer !== memory.buffer;
2388
+ }
2374
2389
  var PAD2 = 8;
2375
2390
  var WasmTransformBackend = class {
2376
2391
  available = true;
@@ -2560,9 +2575,15 @@ var WasmTransformBackend = class {
2560
2575
  }
2561
2576
  ensure(count, runCount) {
2562
2577
  if (count + PAD2 <= this.cap && runCount <= this.runCap) return;
2563
- this.cap = count + PAD2;
2564
- this.runCap = Math.max(runCount, count, 1);
2565
- this.ex.init(count, this.runCap);
2578
+ const cap = count + PAD2;
2579
+ const runCap = Math.max(runCount, count, 1);
2580
+ const status = this.ex.init(count, runCap);
2581
+ if (status !== WASM_STATUS.OK) {
2582
+ this.lastStatus = status;
2583
+ return;
2584
+ }
2585
+ this.cap = cap;
2586
+ this.runCap = runCap;
2566
2587
  this.refreshViews();
2567
2588
  }
2568
2589
  /** Rebuild typed-array views after an init() (which may have grown, and thus
@@ -2630,11 +2651,26 @@ var AnimBackend = class {
2630
2651
  */
2631
2652
  ensure(springCount, tweenCount) {
2632
2653
  if (springCount + PAD3 <= this.springCap && tweenCount + PAD3 <= this.tweenCap) return;
2633
- this.springCap = springCount + PAD3;
2634
- this.tweenCap = tweenCount + PAD3;
2635
- this.ex.anim_init(this.springCap, this.tweenCap);
2654
+ const springCap = springCount + PAD3;
2655
+ const tweenCap = tweenCount + PAD3;
2656
+ const status = this.ex.anim_init(springCap, tweenCap);
2657
+ if (status !== WASM_STATUS.OK) {
2658
+ this.lastStatus = status;
2659
+ return;
2660
+ }
2661
+ this.springCap = springCap;
2662
+ this.tweenCap = tweenCap;
2636
2663
  this.refreshViews();
2637
2664
  }
2665
+ /**
2666
+ * Re-create the typed-array views if another backend's allocation grew the
2667
+ * shared linear memory and detached them (see {@link viewsStale}). Call after
2668
+ * {@link ensure} and before writing into {@link springView}/{@link tweenView}.
2669
+ */
2670
+ revalidateViews() {
2671
+ if (this.springCap === 0) return;
2672
+ if (viewsStale(this.springCap, this.sv.val, this.ex.memory)) this.refreshViews();
2673
+ }
2638
2674
  /**
2639
2675
  * Advance `count` springs (from index 0) by `dtMs` milliseconds, in place.
2640
2676
  * Returns `true` when the kernel ran. `false` means it rejected the call
@@ -2728,15 +2764,26 @@ var HitTestBackend = class {
2728
2764
  this.gridH = gh;
2729
2765
  this.cellSize = cellSize;
2730
2766
  }
2767
+ /**
2768
+ * Re-create the typed-array views if another backend's allocation grew the
2769
+ * shared linear memory and detached them (see {@link viewsStale}). Call after
2770
+ * {@link ensure} and before writing into {@link inputView}.
2771
+ */
2772
+ revalidateViews() {
2773
+ if (this.entityCap === 0) return;
2774
+ if (viewsStale(this.entityCap, this.vminx, this.ex.memory)) this.refreshViews();
2775
+ }
2731
2776
  /**
2732
2777
  * Run the kernel's bucketing over whatever is currently resident in
2733
2778
  * {@link inputView} (write the AABBs there, and call {@link ensure} first).
2734
- * Returns `false` if the build overflowed its item budget the caller must
2735
- * not trust {@link candidatesAt} results for this build and should fall back
2736
- * to the JS walk instead (never return a wrong hit).
2779
+ * Returns `false` if the build was rejected (no `hit_init` yet) or overflowed
2780
+ * its item budget — the caller must not trust {@link candidatesAt} results
2781
+ * for this build and should fall back to the JS walk instead (never return a
2782
+ * wrong hit).
2737
2783
  */
2738
2784
  runBuild(count, vw, vh, cellSize) {
2739
- this.ex.hit_build(count, vw, vh, cellSize);
2785
+ const status = this.ex.hit_build(count, vw, vh, cellSize);
2786
+ if (status !== WASM_STATUS.OK) return false;
2740
2787
  return this.ex.hit_overflow() === 0;
2741
2788
  }
2742
2789
  /**
@@ -2762,10 +2809,16 @@ var HitTestBackend = class {
2762
2809
  if (count + PAD4 <= this.entityCap && cellCount <= this.cellCap && itemCount <= this.itemCap) {
2763
2810
  return;
2764
2811
  }
2765
- this.entityCap = count + PAD4;
2766
- this.cellCap = Math.max(cellCount, 1);
2767
- this.itemCap = Math.max(itemCount, count, 1);
2768
- this.ex.hit_init(count, this.cellCap, this.itemCap);
2812
+ const entityCap = count + PAD4;
2813
+ const cellCap = Math.max(cellCount, 1);
2814
+ const itemCap = Math.max(itemCount, count, 1);
2815
+ const status = this.ex.hit_init(count, cellCap, itemCap);
2816
+ if (status !== WASM_STATUS.OK) {
2817
+ return;
2818
+ }
2819
+ this.entityCap = entityCap;
2820
+ this.cellCap = cellCap;
2821
+ this.itemCap = itemCap;
2769
2822
  this.refreshViews();
2770
2823
  }
2771
2824
  /** Rebuild typed-array views after a growing `hit_init` (which detaches the
@@ -2804,10 +2857,24 @@ var ParticleBackend = class {
2804
2857
  */
2805
2858
  ensure(count) {
2806
2859
  if (count + PAD5 <= this.cap) return;
2807
- this.cap = count + PAD5;
2808
- this.ex.particle_init(this.cap);
2860
+ const cap = count + PAD5;
2861
+ const status = this.ex.particle_init(cap);
2862
+ if (status !== WASM_STATUS.OK) {
2863
+ this.lastStatus = status;
2864
+ return;
2865
+ }
2866
+ this.cap = cap;
2809
2867
  this.refreshViews();
2810
2868
  }
2869
+ /**
2870
+ * Re-create the typed-array views if another backend's allocation grew the
2871
+ * shared linear memory and detached them (see {@link viewsStale}). Call after
2872
+ * {@link ensure} and before {@link gather}/{@link scatter}.
2873
+ */
2874
+ revalidateViews() {
2875
+ if (this.cap === 0) return;
2876
+ if (viewsStale(this.cap, this.view.px, this.ex.memory)) this.refreshViews();
2877
+ }
2811
2878
  /**
2812
2879
  * Advance `count` particles one step in place. Returns `true` when at least
2813
2880
  * one live particle is still moving or off-origin beyond epsilon (the fused
@@ -3074,9 +3141,17 @@ var WasmBackendFacade = class {
3074
3141
  * main render walk onto it; passing `null` reverts to the JS path.
3075
3142
  */
3076
3143
  setTransform(backend) {
3144
+ const backendChanged = backend !== this._transform && this._transform !== null;
3077
3145
  this._transform = backend;
3078
3146
  this._mode = backend ? "wasm" : "js";
3079
3147
  this._uploadRejections = 0;
3148
+ if (backendChanged) {
3149
+ this._treeStore = null;
3150
+ this._storeStructureVersion = -1;
3151
+ this._inputs = null;
3152
+ this._world = null;
3153
+ this._aabbsFresh = false;
3154
+ }
3080
3155
  }
3081
3156
  // ── The shared runtime ──────────────────────────────────────────────────────
3082
3157
  /**
@@ -3244,6 +3319,7 @@ var WasmBackendFacade = class {
3244
3319
  }
3245
3320
  if (!backend.uploadRuns(built.store)) {
3246
3321
  this.transformReason = "rejected";
3322
+ this._aabbsFresh = false;
3247
3323
  this._uploadRejections++;
3248
3324
  if (this._uploadRejections >= WASM_UPLOAD_REJECT_LIMIT) {
3249
3325
  this._mode = "js";
@@ -3283,6 +3359,7 @@ var WasmBackendFacade = class {
3283
3359
  }
3284
3360
  if (backend.runKernel("simd") !== WASM_STATUS.OK) {
3285
3361
  this.transformReason = "rejected";
3362
+ this._aabbsFresh = false;
3286
3363
  return null;
3287
3364
  }
3288
3365
  this._aabbsFresh = false;
@@ -4468,23 +4545,23 @@ var Scene = class _Scene {
4468
4545
  }
4469
4546
  /**
4470
4547
  * Drop `entity` and its whole subtree from the batched-driver candidate set.
4471
- * Called by {@link remove}/{@link hideOverlay} on detach: without this a
4472
- * removed-but-still-animating entity stays pinned in the Set (a leak) and its
4473
- * drivers keep ticking every frame even though it is off-tree. If it is later
4474
- * re-added, {@link registerActiveDriverSubtree} re-registers any node that
4475
- * still has live drivers, so the motion resumes.
4548
+ * Called by `Entity.remove` on detach (which `remove`/`hideOverlay` route
4549
+ * through): without this a removed-but-still-animating entity stays pinned in
4550
+ * the Set (a leak) and its drivers keep ticking every frame even though it is
4551
+ * off-tree. If it is later re-added, {@link _registerActiveDriverSubtree}
4552
+ * re-registers any node that still has live drivers, so the motion resumes.
4476
4553
  */
4477
- unregisterActiveDriverSubtree(entity) {
4554
+ _unregisterActiveDriverSubtree(entity) {
4478
4555
  this._driverTicker.unregisterSubtree(entity);
4479
4556
  }
4480
4557
  /**
4481
4558
  * Re-register every node in `entity`'s subtree that still has live property
4482
- * drivers. Called by {@link add}/{@link showOverlay} so re-attaching a subtree
4483
- * that was removed mid-animation resumes its batched drivers (they were
4484
- * dropped from the candidate set on removal, but the driver state still lives
4485
- * on each entity).
4559
+ * drivers. Called by `Entity._addOne` so re-attaching a subtree that was
4560
+ * removed mid-animation resumes its batched drivers (they were dropped from
4561
+ * the candidate set on removal, but the driver state still lives on each
4562
+ * entity).
4486
4563
  */
4487
- registerActiveDriverSubtree(entity) {
4564
+ _registerActiveDriverSubtree(entity) {
4488
4565
  this._driverTicker.registerSubtree(entity);
4489
4566
  }
4490
4567
  // --- domain: render-scheduler — batched driver tick (extraction 6), reads the anim backend ---
@@ -5021,7 +5098,6 @@ var Scene = class _Scene {
5021
5098
  */
5022
5099
  add(entity) {
5023
5100
  this.root.add(entity);
5024
- this.registerActiveDriverSubtree(entity);
5025
5101
  return this;
5026
5102
  }
5027
5103
  // --- domain: a11y-projection — subtree pruning, focus preservation on removal ---
@@ -5100,7 +5176,6 @@ var Scene = class _Scene {
5100
5176
  remove(entity) {
5101
5177
  this.root.remove(entity);
5102
5178
  this.removeA11yRecursively(entity);
5103
- this.unregisterActiveDriverSubtree(entity);
5104
5179
  return this;
5105
5180
  }
5106
5181
  /**
@@ -5132,7 +5207,6 @@ var Scene = class _Scene {
5132
5207
  */
5133
5208
  showOverlay(overlay) {
5134
5209
  this.overlayRoot.add(overlay);
5135
- this.registerActiveDriverSubtree(overlay);
5136
5210
  this.markDirty();
5137
5211
  }
5138
5212
  /**
@@ -5141,7 +5215,6 @@ var Scene = class _Scene {
5141
5215
  hideOverlay(overlay) {
5142
5216
  this.overlayRoot.remove(overlay);
5143
5217
  this.removeA11yRecursively(overlay);
5144
- this.unregisterActiveDriverSubtree(overlay);
5145
5218
  this.markDirty();
5146
5219
  }
5147
5220
  destroyEntitySubtree(entity) {
@@ -5385,8 +5458,9 @@ var Scene = class _Scene {
5385
5458
  * workload rendered ~1.0 frames per content change. To measure anything about
5386
5459
  * scheduling, use {@link start} and let `requestAnimationFrame` drive.
5387
5460
  *
5388
- * @param dt Seconds to advance. Not clamped by `MAX_FRAME_DT` the caller
5389
- * chooses the step, since determinism is the point.
5461
+ * @param dt Milliseconds to advance (same unit as the rAF timestamps `loop`
5462
+ * feeds `render`). Not clamped by `MAX_FRAME_DT` the caller chooses the
5463
+ * step, since determinism is the point.
5390
5464
  */
5391
5465
  step(dt) {
5392
5466
  const time = this.lastTime + dt;
@@ -6329,26 +6403,43 @@ var Scene = class _Scene {
6329
6403
  if (mctx) {
6330
6404
  mctx.font = lineFont;
6331
6405
  const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
6332
- const segments = [...segmenter.segment(line.text)];
6333
- let runningX = 0;
6334
- for (let segIdx = 0; segIdx < segments.length; segIdx++) {
6335
- const { segment, index: index2 } = segments[segIdx];
6336
- const nextIndex = segIdx + 1 < segments.length ? segments[segIdx + 1].index : line.text.length;
6337
- const canvasX = mctx.measureText(line.text.slice(0, index2)).width;
6338
- const canvasWidth = mctx.measureText(line.text.slice(0, nextIndex)).width - canvasX;
6339
- const carrierEl = document.createElement("span");
6340
- carrierEl.textContent = segment;
6341
- carrierEl.style.position = "relative";
6342
- carrierEl.style.display = "inline-block";
6343
- carrierEl.style.left = `${canvasX - runningX}px`;
6344
- carrierEl.style.width = `${canvasWidth}px`;
6345
- carrierEl.style.boxSizing = "border-box";
6346
- carrierEl.style.lineHeight = `${lineHeight2}px`;
6347
- carrierEl.style.whiteSpace = "pre";
6348
- carrierEl.style.verticalAlign = "top";
6349
- carrierEl.style.unicodeBidi = "isolate";
6350
- lineElement.appendChild(carrierEl);
6351
- runningX += canvasWidth;
6406
+ if (line.shapedPaint) {
6407
+ const segments = [...segmenter.segment(line.text)];
6408
+ let runningX = 0;
6409
+ for (let segIdx = 0; segIdx < segments.length; segIdx++) {
6410
+ const { segment, index: index2 } = segments[segIdx];
6411
+ const nextIndex = segIdx + 1 < segments.length ? segments[segIdx + 1].index : line.text.length;
6412
+ const canvasX = mctx.measureText(line.text.slice(0, index2)).width;
6413
+ const canvasWidth = mctx.measureText(line.text.slice(0, nextIndex)).width - canvasX;
6414
+ const carrierEl = document.createElement("span");
6415
+ carrierEl.textContent = segment;
6416
+ carrierEl.style.position = "relative";
6417
+ carrierEl.style.display = "inline-block";
6418
+ carrierEl.style.left = `${canvasX - runningX}px`;
6419
+ carrierEl.style.width = `${canvasWidth}px`;
6420
+ carrierEl.style.boxSizing = "border-box";
6421
+ carrierEl.style.lineHeight = `${lineHeight2}px`;
6422
+ carrierEl.style.whiteSpace = "pre";
6423
+ carrierEl.style.verticalAlign = "top";
6424
+ carrierEl.style.unicodeBidi = "isolate";
6425
+ lineElement.appendChild(carrierEl);
6426
+ runningX += canvasWidth;
6427
+ }
6428
+ } else {
6429
+ for (const { segment } of segmenter.segment(line.text)) {
6430
+ const canvasWidth = mctx.measureText(segment).width;
6431
+ const carrierEl = document.createElement("span");
6432
+ carrierEl.textContent = segment;
6433
+ carrierEl.style.position = "relative";
6434
+ carrierEl.style.display = "inline-block";
6435
+ carrierEl.style.width = `${canvasWidth}px`;
6436
+ carrierEl.style.boxSizing = "border-box";
6437
+ carrierEl.style.lineHeight = `${lineHeight2}px`;
6438
+ carrierEl.style.whiteSpace = "pre";
6439
+ carrierEl.style.verticalAlign = "top";
6440
+ carrierEl.style.unicodeBidi = "isolate";
6441
+ lineElement.appendChild(carrierEl);
6442
+ }
6352
6443
  }
6353
6444
  } else {
6354
6445
  lineElement.textContent = line.text;
@@ -6684,7 +6775,7 @@ var Scene = class _Scene {
6684
6775
  const hasInteractive = this.frameHadInteractive;
6685
6776
  const wantsContentSync = this.contentProjectionEnabled;
6686
6777
  const shouldSyncInterval = this.a11ySyncInterval <= 0 || time - this.lastA11ySync >= this.a11ySyncInterval;
6687
- if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
6778
+ if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || !hasActiveAnimation && this.a11yPendingSyncAfterAnimation)) {
6688
6779
  this.lastA11ySync = time;
6689
6780
  if (hasInteractive || wantsContentSync) {
6690
6781
  const userTiming = this.phases.userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.a11ySync) : null;
@@ -7544,7 +7635,7 @@ var GridTextEntity = class extends Entity {
7544
7635
  updateGrid(ascii) {
7545
7636
  this.grid = ascii;
7546
7637
  this.rows = ascii.length;
7547
- this.cols = ascii[0]?.length || 0;
7638
+ this.cols = ascii.reduce((widest, row) => Math.max(widest, row?.length ?? 0), 0);
7548
7639
  }
7549
7640
  isPointInside(_globalX, _globalY) {
7550
7641
  return false;
@@ -7632,6 +7723,12 @@ var SplineEntity = class extends Entity {
7632
7723
  /** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
7633
7724
  bakedWidth = 0;
7634
7725
  bakedHeight = 0;
7726
+ /**
7727
+ * Device-pixel ratio the bitmap was rasterized at. Tracked so a DPR change
7728
+ * (browser zoom, a monitor move, a renderer whose clamp changed) re-bakes
7729
+ * instead of blitting a bitmap at the wrong device density forever.
7730
+ */
7731
+ bakedDPR = 0;
7635
7732
  /**
7636
7733
  * Gradient strokes can't be baked to a solid-color bitmap; they render
7637
7734
  * per-frame. Derived from `_doc`, so it is NOT readonly — assigning a new
@@ -7855,14 +7952,14 @@ var SplineEntity = class extends Entity {
7855
7952
  }
7856
7953
  }
7857
7954
  /** Bake all equations into an OffscreenCanvas once (when available). */
7858
- bake() {
7955
+ bake(dpr) {
7859
7956
  this.baked = true;
7860
7957
  const pad = this.lineWidth + 2;
7861
7958
  const w = Math.max(1, Math.ceil(this.bounds.width) + pad * 2);
7862
7959
  const h = Math.max(1, Math.ceil(this.bounds.height) + pad * 2);
7863
7960
  this.bakedWidth = w;
7864
7961
  this.bakedHeight = h;
7865
- const dpr = typeof window !== "undefined" && typeof window.devicePixelRatio === "number" ? window.devicePixelRatio || 1 : 1;
7962
+ this.bakedDPR = dpr;
7866
7963
  let canvas;
7867
7964
  if (typeof OffscreenCanvas !== "undefined") {
7868
7965
  canvas = new OffscreenCanvas(w * dpr, h * dpr);
@@ -7910,7 +8007,8 @@ var SplineEntity = class extends Entity {
7910
8007
  render(r) {
7911
8008
  let rendered = false;
7912
8009
  if (this.cache && !this.containsGradient) {
7913
- if (!this.baked) this.bake();
8010
+ const dpr = typeof r.pixelRatio === "number" && r.pixelRatio > 0 ? r.pixelRatio : typeof window !== "undefined" && typeof window.devicePixelRatio === "number" ? window.devicePixelRatio || 1 : 1;
8011
+ if (!this.baked || this.bakedDPR !== dpr) this.bake(dpr);
7914
8012
  if (this.offscreen) {
7915
8013
  const pad = this.lineWidth + 2;
7916
8014
  r.drawImage(
@@ -49,9 +49,9 @@ export interface PointRenderer {
49
49
  *
50
50
  * Batching here is by primitive type — one draw per active type — so draw calls
51
51
  * and batches are the same number, and both are bounded at five plus one per
52
- * mid-frame MSDF atlas switch. That switch is the only variable term and the
53
- * only thing worth watching: it forces a commit of glyphs batched against the
54
- * previous font.
52
+ * mid-frame atlas switch (MSDF or sprite). Those switches are the only
53
+ * variable term and the only thing worth watching: each forces a commit of
54
+ * glyphs/sprites batched against the previous atlas.
55
55
  */
56
56
  stats?(): WebGLDrawStats;
57
57
  /** Add one circle in world (CSS-pixel) coordinates; `alpha` multiplies the color's. */
@@ -8,8 +8,8 @@ export type RGBA = [number, number, number, number];
8
8
  * Fast paths handle `#rgb`/`#rgba`/`#rrggbb`/`#rrggbbaa` and `rgb()`/`rgba()`;
9
9
  * other forms (named colors, `hsl()`, …) resolve via a cached 1×1 canvas when a
10
10
  * DOM is available. Results are cached per input string and shared by identity,
11
- * so callers must treat the returned array as read-only. Unparseable input with
12
- * no DOM yields opaque black `[0, 0, 0, 1]`.
11
+ * so callers must treat the returned array as read-only. Unparseable input
12
+ * yields opaque black `[0, 0, 0, 1]` regardless of DOM availability.
13
13
  *
14
14
  * @param css - A CSS color string, e.g. `'#38bdf8'` or `'rgba(0,0,0,.5)'`.
15
15
  * @returns The cached `[r, g, b, a]` tuple in `[0, 1]`.
@@ -16,8 +16,10 @@
16
16
  * 2. Trims leading whitespace (browsers do this before scheme resolution).
17
17
  * 3. If the URL is relative (no scheme, or starts with `#`, `?`, `/`, `./`),
18
18
  * returns it verbatim — relative navigation is never script-injectable.
19
- * 4. If the URL parses with a scheme NOT in {@link SAFE_SCHEMES}, returns `'#'`
20
- * to keep the link non-empty but inert.
19
+ * 4. If the URL parses with a scheme NOT in {@link SAFE_SCHEMES} after HTML
20
+ * character references are decoded, so an entity-encoded payload cannot
21
+ * smuggle a scheme past the check — returns `'#'` to keep the link non-empty
22
+ * but inert.
21
23
  * 5. Otherwise returns the trimmed input unchanged (no canonicalization).
22
24
  *
23
25
  * The function never throws; malformed input falls back to `'#'`.
package/dist/renderer.js CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
 
12
12
 
13
- var _chunkM73XB4ZKjs = require('./chunk-M73XB4ZK.js');
13
+ var _chunkMO24PVGTjs = require('./chunk-MO24PVGT.js');
14
14
 
15
15
 
16
16
 
@@ -22,4 +22,4 @@ var _chunkM73XB4ZKjs = require('./chunk-M73XB4ZK.js');
22
22
 
23
23
 
24
24
 
25
- exports.CanvasRenderer = _chunkM73XB4ZKjs.CanvasRenderer; exports.GlyphRasterAtlas = _chunkM73XB4ZKjs.GlyphRasterAtlas; exports.SVGRenderer = _chunkM73XB4ZKjs.SVGRenderer; exports.TextRasterCache = _chunkM73XB4ZKjs.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkM73XB4ZKjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkM73XB4ZKjs.createWebGLPointRenderer; exports.installRendererDevTraps = _chunkM73XB4ZKjs.installRendererDevTraps; exports.isRendererDevMode = _chunkM73XB4ZKjs.isRendererDevMode; exports.parseColorToRGBA = _chunkM73XB4ZKjs.parseColorToRGBA; exports.setRendererDevMode = _chunkM73XB4ZKjs.setRendererDevMode;
25
+ exports.CanvasRenderer = _chunkMO24PVGTjs.CanvasRenderer; exports.GlyphRasterAtlas = _chunkMO24PVGTjs.GlyphRasterAtlas; exports.SVGRenderer = _chunkMO24PVGTjs.SVGRenderer; exports.TextRasterCache = _chunkMO24PVGTjs.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkMO24PVGTjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkMO24PVGTjs.createWebGLPointRenderer; exports.installRendererDevTraps = _chunkMO24PVGTjs.installRendererDevTraps; exports.isRendererDevMode = _chunkMO24PVGTjs.isRendererDevMode; exports.parseColorToRGBA = _chunkMO24PVGTjs.parseColorToRGBA; exports.setRendererDevMode = _chunkMO24PVGTjs.setRendererDevMode;
package/dist/renderer.mjs CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  isRendererDevMode,
10
10
  parseColorToRGBA,
11
11
  setRendererDevMode
12
- } from "./chunk-IFSFHYF7.mjs";
12
+ } from "./chunk-25PS5K7R.mjs";
13
13
  export {
14
14
  CanvasRenderer,
15
15
  GlyphRasterAtlas,
package/dist/text.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkXXU56ZNAjs = require('./chunk-XXU56ZNA.js');
5
+ var _chunkXHPUCU5Pjs = require('./chunk-XHPUCU5P.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 = _chunkXXU56ZNAjs.MSDFTextEntity; exports.SVGEntity = _chunkXXU56ZNAjs.SVGEntity;
12
+ exports.MSDFTextEntity = _chunkXHPUCU5Pjs.MSDFTextEntity; exports.SVGEntity = _chunkXHPUCU5Pjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MSDFTextEntity,
3
3
  SVGEntity
4
- } from "./chunk-TV4ARAC5.mjs";
4
+ } from "./chunk-CHDJEHML.mjs";
5
5
 
6
6
  // src/text/index.ts
7
7
  export * from "@vectojs/text";
@@ -116,6 +116,21 @@ export interface ContentProjectionLine {
116
116
  * hit-mapping when DOM order != visual order (PR #146 revert).
117
117
  */
118
118
  perGraphemeCarriers?: boolean;
119
+ /**
120
+ * The canvas painted this line's ink as ONE shaped `fillText(line.text)`, so
121
+ * the ink includes the browser's kerning and ligatures. Per-grapheme
122
+ * carriers must then be measured as shaped PREFIX DIFFERENCES of the whole
123
+ * line — isolated grapheme advances would omit exactly the kerning/ligature
124
+ * contraction the paint applied, widening the DOM line past the ink.
125
+ *
126
+ * Leave unset for per-glyph painters (glyphs placed at summed isolated
127
+ * advances, e.g. RichText and TextEntity): there the ink is unkerned, and
128
+ * shaped prefixes would drift the carriers ahead of it by the accumulated
129
+ * kerning delta instead.
130
+ *
131
+ * Only meaningful alongside {@link perGraphemeCarriers}.
132
+ */
133
+ shapedPaint?: boolean;
119
134
  }
120
135
  /**
121
136
  * Advice from the {@link Scene} about which part of an entity is worth
@@ -936,21 +936,21 @@ export declare class Scene {
936
936
  _registerActiveDriverEntity(entity: Entity): void;
937
937
  /**
938
938
  * Drop `entity` and its whole subtree from the batched-driver candidate set.
939
- * Called by {@link remove}/{@link hideOverlay} on detach: without this a
940
- * removed-but-still-animating entity stays pinned in the Set (a leak) and its
941
- * drivers keep ticking every frame even though it is off-tree. If it is later
942
- * re-added, {@link registerActiveDriverSubtree} re-registers any node that
943
- * still has live drivers, so the motion resumes.
939
+ * Called by `Entity.remove` on detach (which `remove`/`hideOverlay` route
940
+ * through): without this a removed-but-still-animating entity stays pinned in
941
+ * the Set (a leak) and its drivers keep ticking every frame even though it is
942
+ * off-tree. If it is later re-added, {@link _registerActiveDriverSubtree}
943
+ * re-registers any node that still has live drivers, so the motion resumes.
944
944
  */
945
- private unregisterActiveDriverSubtree;
945
+ _unregisterActiveDriverSubtree(entity: Entity): void;
946
946
  /**
947
947
  * Re-register every node in `entity`'s subtree that still has live property
948
- * drivers. Called by {@link add}/{@link showOverlay} so re-attaching a subtree
949
- * that was removed mid-animation resumes its batched drivers (they were
950
- * dropped from the candidate set on removal, but the driver state still lives
951
- * on each entity).
948
+ * drivers. Called by `Entity._addOne` so re-attaching a subtree that was
949
+ * removed mid-animation resumes its batched drivers (they were dropped from
950
+ * the candidate set on removal, but the driver state still lives on each
951
+ * entity).
952
952
  */
953
- private registerActiveDriverSubtree;
953
+ _registerActiveDriverSubtree(entity: Entity): void;
954
954
  /**
955
955
  * Advance every registered entity's active drivers for this frame, batching
956
956
  * whichever are batchable (`SpringDriver`; `TweenDriver` with a named
@@ -1247,8 +1247,9 @@ export declare class Scene {
1247
1247
  * workload rendered ~1.0 frames per content change. To measure anything about
1248
1248
  * scheduling, use {@link start} and let `requestAnimationFrame` drive.
1249
1249
  *
1250
- * @param dt Seconds to advance. Not clamped by `MAX_FRAME_DT` the caller
1251
- * chooses the step, since determinism is the point.
1250
+ * @param dt Milliseconds to advance (same unit as the rAF timestamps `loop`
1251
+ * feeds `render`). Not clamped by `MAX_FRAME_DT` the caller chooses the
1252
+ * step, since determinism is the point.
1252
1253
  */
1253
1254
  step(dt: number): void;
1254
1255
  /**
@@ -7,9 +7,13 @@
7
7
  * (see `Scene._tickBatchedDrivers`), simply keeps using it.
8
8
  *
9
9
  * The kernel (`crates/vectojs-core-rs/src/anim.rs`) is bit-identical to
10
- * `SpringPhysics.update` for springs; tweens match to ~1e-9 (not bit-exact a
11
- * `Math.pow`-vs-`powi` ULP difference recorded as a spike finding), which is why
12
- * a WASM-batched tween needs a `retarget`-style resync, not raw byte reuse.
10
+ * `SpringPhysics.update` for springs, and to the JS tweens too since the
11
+ * explicit-multiplication rewrite: `easing.ts` and the kernel's `ease()` both
12
+ * express integer powers as explicit multiplication (no `Math.pow`/`powi`,
13
+ * neither of which is correctly rounded), so the earlier ~1e-12 ULP gap is
14
+ * closed. Batched state still needs re-seeding per frame — `elapsed` is
15
+ * kernel-side state, so every gather re-writes it from the driver rather than
16
+ * letting the kernel's copy drift.
13
17
  *
14
18
  * Unlike the transform/hit-test stores, this backend holds no cross-frame
15
19
  * residency: every qualifying frame re-gathers ALL currently-active batchable
@@ -56,6 +60,12 @@ export declare class AnimBackend {
56
60
  * writing first and sizing after would write into a stale buffer.
57
61
  */
58
62
  ensure(springCount: number, tweenCount: number): void;
63
+ /**
64
+ * Re-create the typed-array views if another backend's allocation grew the
65
+ * shared linear memory and detached them (see {@link viewsStale}). Call after
66
+ * {@link ensure} and before writing into {@link springView}/{@link tweenView}.
67
+ */
68
+ revalidateViews(): void;
59
69
  /**
60
70
  * Advance `count` springs (from index 0) by `dtMs` milliseconds, in place.
61
71
  * Returns `true` when the kernel ran. `false` means it rejected the call
@@ -27,7 +27,20 @@ export declare const WASM_STATUS: {
27
27
  readonly UNINITIALIZED: 2;
28
28
  /** A sibling run addressed a slot or parent outside the store. */
29
29
  readonly BAD_RUN: 3;
30
+ /** A requested capacity's size arithmetic overflowed; the previous store
31
+ * was left untouched. */
32
+ readonly OVERFLOW: 4;
30
33
  };
34
+ /**
35
+ * True when a backend's resident typed-array views must be rebuilt: the shared
36
+ * `WebAssembly.Memory` buffer they were constructed over has been detached
37
+ * (reads as `byteLength === 0`) or replaced. Every backend of a Scene shares one
38
+ * instance and therefore one linear memory, so ANY backend's `*_init` can grow
39
+ * it and detach the views of every other backend. The transform backend keeps
40
+ * this check inline in {@link WasmTransformBackend.revalidateViews}; the anim/
41
+ * hit/particle backends reuse this helper.
42
+ */
43
+ export declare function viewsStale(cap: number, probe: ArrayBufferView, memory: WebAssembly.Memory): boolean;
31
44
  /** Which kernel to run. `simd` is the default; `scalar` exists for A/B and for
32
45
  * the (theoretical) case of a build without simd128. Both are f64 and
33
46
  * bit-identical to {@link composeJS}. */