@vectojs/core 1.32.6 → 1.32.7

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
@@ -18,7 +18,7 @@ import {
18
18
  SVGEntity,
19
19
  VectoJSEvent,
20
20
  contentLineInHint
21
- } from "./chunk-RWN3ITBD.mjs";
21
+ } from "./chunk-YTGGL4I4.mjs";
22
22
 
23
23
  // src/tree/Scene.ts
24
24
  import { SpringDriver, TweenDriver } from "@vectojs/animation";
@@ -1434,6 +1434,7 @@ function parseInlinePx(value) {
1434
1434
  return Number.isFinite(n) && n > 0 ? n : null;
1435
1435
  }
1436
1436
  var DEFAULT_CONTENT_SEMANTIC_BUDGET = 256;
1437
+ var WASM_UPLOAD_REJECT_LIMIT = 3;
1437
1438
  function projectionGridLineWindow(grid, projectionLines, band) {
1438
1439
  const count = grid.lines.length;
1439
1440
  const all = { start: 0, end: count, gated: false };
@@ -2204,6 +2205,14 @@ var Scene = class _Scene {
2204
2205
  _wasmWorld = null;
2205
2206
  _structureVersion = 0;
2206
2207
  _storeStructureVersion = -1;
2208
+ /**
2209
+ * Consecutive `uploadRuns` rejections. Reset by any success and by
2210
+ * {@link setTransformBackend}; at {@link WASM_UPLOAD_REJECT_LIMIT} the backend
2211
+ * mode flips to `'js'` permanently.
2212
+ */
2213
+ _wasmUploadRejections = 0;
2214
+ /** Latch for the permanent-fallback warning, which fires from a per-frame path. */
2215
+ hasWarnedWasmUploadFallback = false;
2207
2216
  // Cached list of ComputeParticleEntity instances in the tree, keyed by the
2208
2217
  // structure version it was gathered at. Rebuilt only on a topology change.
2209
2218
  _computeEntities = [];
@@ -2241,6 +2250,7 @@ var Scene = class _Scene {
2241
2250
  setTransformBackend(backend) {
2242
2251
  this._wasm = backend;
2243
2252
  this._transformBackend = backend ? "wasm" : "js";
2253
+ this._wasmUploadRejections = 0;
2244
2254
  }
2245
2255
  /**
2246
2256
  * Asynchronously instantiate the WASM transform core and, on success, hot-swap
@@ -2872,8 +2882,19 @@ var Scene = class _Scene {
2872
2882
  }
2873
2883
  if (!backend.uploadRuns(built.store)) {
2874
2884
  this._transformReason = "rejected";
2885
+ this._wasmUploadRejections++;
2886
+ if (this._wasmUploadRejections >= WASM_UPLOAD_REJECT_LIMIT) {
2887
+ this._transformBackend = "js";
2888
+ if (!this.hasWarnedWasmUploadFallback) {
2889
+ console.warn(
2890
+ `[VectoJS] WASM transform backend disabled after ${this._wasmUploadRejections} consecutive run-table rejections (${built.store.runCount} runs for ${built.store.count} entities); the crate refused this topology. Falling back to JS transforms for this scene. Call scene.setTransformBackend(backend) to re-enable after reducing the tree.`
2891
+ );
2892
+ this.hasWarnedWasmUploadFallback = true;
2893
+ }
2894
+ }
2875
2895
  return null;
2876
2896
  }
2897
+ this._wasmUploadRejections = 0;
2877
2898
  this._treeStore = built.store;
2878
2899
  this._slotEntity = slotEntity2;
2879
2900
  this._wasmInputs = backend.inputView();
@@ -3003,6 +3024,16 @@ var Scene = class _Scene {
3003
3024
  * else the canvas). Stored so `destroy()` detaches from the same element. */
3004
3025
  pointerEventTarget = null;
3005
3026
  hasWarnedZeroSize = false;
3027
+ /**
3028
+ * Latch for {@link Scene.resize}'s invalid-dimension warning.
3029
+ *
3030
+ * Separate from `hasWarnedZeroSize`: that one fires for a tolerated zero-size
3031
+ * scene at `start()`, this one for a rejected resize, and sharing the flag
3032
+ * would let either suppress the other's first (and only) report. Latched
3033
+ * because `resize()` is commonly driven from a `ResizeObserver`, which would
3034
+ * otherwise warn on every frame of a drag.
3035
+ */
3036
+ hasWarnedInvalidResize = false;
3006
3037
  fontLoadHandler = null;
3007
3038
  // ── Dev-mode warning infrastructure ──────────────────────────────
3008
3039
  //
@@ -3804,9 +3835,11 @@ var Scene = class _Scene {
3804
3835
  /**
3805
3836
  * Begin the `requestAnimationFrame` render loop.
3806
3837
  *
3807
- * Idempotent — calling `start()` on an already-running scene is a no-op.
3838
+ * Idempotent — calling `start()` on an already-running scene is a no-op, and so
3839
+ * is calling it on a destroyed one.
3808
3840
  */
3809
3841
  start() {
3842
+ if (this.destroyed) return;
3810
3843
  if (this.isRunning) return;
3811
3844
  if ((this.width === 0 || this.height === 0) && !this.hasWarnedZeroSize) {
3812
3845
  console.warn(
@@ -4975,9 +5008,27 @@ var Scene = class _Scene {
4975
5008
  if (el.dataset.vectoContentGrid !== signature) {
4976
5009
  const materializeStart = typeof performance !== "undefined" ? performance.now() : 0;
4977
5010
  this.clearContentGridState(node.id, el, false);
5011
+ if (el.firstChild !== null) {
5012
+ let scan = el.firstChild;
5013
+ let strippedSelection = false;
5014
+ while (scan !== null) {
5015
+ const next = scan.nextSibling;
5016
+ if (scan.nodeType === 3) {
5017
+ if (!strippedSelection) {
5018
+ strippedSelection = true;
5019
+ this.releaseContentSelectionForRebuild(el);
5020
+ }
5021
+ scan.remove();
5022
+ }
5023
+ scan = next;
5024
+ }
5025
+ }
4978
5026
  const projectionLines = projection.lines ?? [];
4979
5027
  const selectionLine = this.contentGridSelectionLine(el);
4980
5028
  let rebuiltSelectionLine = false;
5029
+ if (selectionLine !== null && (selectionLine < gridWindow.start || selectionLine >= gridWindow.end)) {
5030
+ rebuiltSelectionLine = true;
5031
+ }
4981
5032
  const existingLines = el.children;
4982
5033
  for (let lineIndex = gridWindow.start; lineIndex < gridWindow.end; lineIndex++) {
4983
5034
  const domIndex = lineIndex - gridWindow.start;
@@ -5074,9 +5125,6 @@ var Scene = class _Scene {
5074
5125
  }
5075
5126
  const windowLength = gridWindow.end - gridWindow.start;
5076
5127
  while (el.children.length > windowLength) {
5077
- if (selectionLine !== null && selectionLine >= gridWindow.end) {
5078
- rebuiltSelectionLine = true;
5079
- }
5080
5128
  el.lastElementChild?.remove();
5081
5129
  }
5082
5130
  if (rebuiltSelectionLine) this.releaseContentSelectionForRebuild(el);
@@ -5745,6 +5793,11 @@ var Scene = class _Scene {
5745
5793
  if (isMainRenderPath && !this.device && !this.webgpuDisabled && !this.initializingWebGPU && !this.deviceLost) {
5746
5794
  this.initializingWebGPU = true;
5747
5795
  this.initWebGPUContext(computeEntities).then((newDevice) => {
5796
+ if (this.destroyed) {
5797
+ newDevice.destroy();
5798
+ this.initializingWebGPU = false;
5799
+ return;
5800
+ }
5748
5801
  this.device = newDevice;
5749
5802
  this.initializingWebGPU = false;
5750
5803
  const format = navigator.gpu ? navigator.gpu.getPreferredCanvasFormat() : "rgba8unorm";
@@ -6097,8 +6150,21 @@ var Scene = class _Scene {
6097
6150
  }
6098
6151
  /**
6099
6152
  * Manually resize the Scene's viewport.
6153
+ *
6154
+ * Rejects a negative or non-finite dimension, keeping the last known-good
6155
+ * size. `0` is accepted: a zero-size scene is tolerated and warned about at
6156
+ * {@link start} instead, and rejecting it here would contradict that.
6100
6157
  */
6101
6158
  resize(width, height) {
6159
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) {
6160
+ if (!this.hasWarnedInvalidResize) {
6161
+ console.warn(
6162
+ `[VectoJS] Scene.resize() ignored invalid dimensions (width: ${width}, height: ${height}). Width and height must be finite and non-negative; the previous viewport size was kept.`
6163
+ );
6164
+ this.hasWarnedInvalidResize = true;
6165
+ }
6166
+ return;
6167
+ }
6102
6168
  this.width = width;
6103
6169
  this.height = height;
6104
6170
  this.contentFontEpoch++;
@@ -6631,8 +6697,8 @@ function distSqToSegment(px, py, x1, y1, x2, y2) {
6631
6697
  return ex * ex + ey * ey;
6632
6698
  }
6633
6699
  var SplineEntity = class extends Entity {
6634
- doc;
6635
- lineWidth;
6700
+ _doc;
6701
+ _lineWidth;
6636
6702
  defaultColor;
6637
6703
  hitTolerance;
6638
6704
  cache;
@@ -6643,10 +6709,42 @@ var SplineEntity = class extends Entity {
6643
6709
  /** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
6644
6710
  bakedWidth = 0;
6645
6711
  bakedHeight = 0;
6646
- /** Gradient strokes can't be baked to a solid-color bitmap; they render per-frame. */
6712
+ /**
6713
+ * Gradient strokes can't be baked to a solid-color bitmap; they render
6714
+ * per-frame. Derived from `_doc`, so it is NOT readonly — assigning a new
6715
+ * document has to recompute it (see the `doc` setter).
6716
+ */
6647
6717
  containsGradient;
6648
6718
  /** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
6649
6719
  polylines = null;
6720
+ /**
6721
+ * The spline document to render. Assigning a new document invalidates all
6722
+ * caches (baked canvas, flattened polylines, bounds).
6723
+ */
6724
+ get doc() {
6725
+ return this._doc;
6726
+ }
6727
+ set doc(value) {
6728
+ if (this._doc === value) return;
6729
+ this._doc = value;
6730
+ this.invalidateCache();
6731
+ this.bounds = this.computeBounds();
6732
+ this.width = this.bounds.width;
6733
+ this.height = this.bounds.height;
6734
+ this.containsGradient = this.computeContainsGradient();
6735
+ }
6736
+ /**
6737
+ * Stroke width in local units. Assigning a new value invalidates the baked
6738
+ * canvas and hit-test polylines so visual and hit geometry stay consistent.
6739
+ */
6740
+ get lineWidth() {
6741
+ return this._lineWidth;
6742
+ }
6743
+ set lineWidth(value) {
6744
+ if (this._lineWidth === value) return;
6745
+ this._lineWidth = value;
6746
+ this.invalidateCache();
6747
+ }
6650
6748
  /**
6651
6749
  * When `true`, the renderer draws a rounded-rect outline of the entity's
6652
6750
  * local bounds after painting the curves. Useful for drag feedback and
@@ -6655,8 +6753,8 @@ var SplineEntity = class extends Entity {
6655
6753
  showBounds = false;
6656
6754
  constructor(doc, opts = {}) {
6657
6755
  super();
6658
- this.doc = doc;
6659
- this.lineWidth = opts.lineWidth ?? 2;
6756
+ this._doc = doc;
6757
+ this._lineWidth = opts.lineWidth ?? 2;
6660
6758
  this.cache = opts.cache ?? true;
6661
6759
  this.defaultColor = opts.defaultColor ?? "#e2e8f0";
6662
6760
  this.hitMode = opts.hitTest ?? "curve";
@@ -6664,21 +6762,35 @@ var SplineEntity = class extends Entity {
6664
6762
  this.bounds = this.computeBounds();
6665
6763
  this.width = this.bounds.width;
6666
6764
  this.height = this.bounds.height;
6667
- const isGradient = (c) => c !== null && !Array.isArray(c);
6668
- this.containsGradient = (this.doc.equations?.some((eq) => isGradient(eq.color_rgb)) ?? false) || (this.doc.paths?.some((p) => isGradient(p.color_rgb)) ?? false);
6765
+ this.containsGradient = this.computeContainsGradient();
6669
6766
  this.interactive = true;
6670
6767
  }
6768
+ /**
6769
+ * Whether any stroke in the current document is a gradient. Shared by the
6770
+ * constructor and the `doc` setter so both stay in agreement — the flag picks
6771
+ * the render path, so a stale value renders a gradient as a flat color.
6772
+ */
6773
+ computeContainsGradient() {
6774
+ const isGradient = (c) => c !== null && !Array.isArray(c);
6775
+ return (this._doc.equations?.some((eq) => isGradient(eq.color_rgb)) ?? false) || (this._doc.paths?.some((p) => isGradient(p.color_rgb)) ?? false);
6776
+ }
6777
+ /** Clear cached baked canvas and hit-test polylines. */
6778
+ invalidateCache() {
6779
+ this.baked = false;
6780
+ this.offscreen = null;
6781
+ this.polylines = null;
6782
+ }
6671
6783
  computeBounds() {
6672
- if (this.doc.bounding_box) {
6673
- const [minX2, minY2, maxX2, maxY2] = this.doc.bounding_box;
6784
+ if (this._doc.bounding_box) {
6785
+ const [minX2, minY2, maxX2, maxY2] = this._doc.bounding_box;
6674
6786
  return { x: minX2, y: minY2, width: maxX2 - minX2, height: maxY2 - minY2 };
6675
6787
  }
6676
6788
  let minX = Infinity;
6677
6789
  let minY = Infinity;
6678
6790
  let maxX = -Infinity;
6679
6791
  let maxY = -Infinity;
6680
- if (this.doc.equations) {
6681
- for (const eq of this.doc.equations) {
6792
+ if (this._doc.equations) {
6793
+ for (const eq of this._doc.equations) {
6682
6794
  for (const seg of eq.data) {
6683
6795
  const b = polySegmentToBezier(seg);
6684
6796
  for (const [x, y] of [
@@ -6695,8 +6807,8 @@ var SplineEntity = class extends Entity {
6695
6807
  }
6696
6808
  }
6697
6809
  }
6698
- if (this.doc.paths) {
6699
- for (const path of this.doc.paths) {
6810
+ if (this._doc.paths) {
6811
+ for (const path of this._doc.paths) {
6700
6812
  for (const pt of path.data) {
6701
6813
  if (pt.x < minX) minX = pt.x;
6702
6814
  if (pt.x > maxX) maxX = pt.x;
@@ -6756,15 +6868,15 @@ var SplineEntity = class extends Entity {
6756
6868
  getPolylines() {
6757
6869
  if (this.polylines) return this.polylines;
6758
6870
  const out = [];
6759
- if (this.doc.equations) {
6760
- for (const eq of this.doc.equations) {
6871
+ if (this._doc.equations) {
6872
+ for (const eq of this._doc.equations) {
6761
6873
  for (const seg of eq.data) {
6762
6874
  out.push(flattenBezier(polySegmentToBezier(seg), HIT_SAMPLES));
6763
6875
  }
6764
6876
  }
6765
6877
  }
6766
- if (this.doc.paths) {
6767
- for (const path of this.doc.paths) {
6878
+ if (this._doc.paths) {
6879
+ for (const path of this._doc.paths) {
6768
6880
  const pts = new Float32Array(path.data.length * 2);
6769
6881
  for (let i = 0; i < path.data.length; i++) {
6770
6882
  pts[i * 2] = path.data[i].x;
@@ -6793,8 +6905,8 @@ var SplineEntity = class extends Entity {
6793
6905
  );
6794
6906
  }
6795
6907
  strokeEquations(r) {
6796
- if (this.doc.equations) {
6797
- for (const eq of this.doc.equations) {
6908
+ if (this._doc.equations) {
6909
+ for (const eq of this._doc.equations) {
6798
6910
  const stroke = this.resolveColor(eq.color_rgb, r);
6799
6911
  r.beginPath();
6800
6912
  for (const seg of eq.data) {
@@ -6805,8 +6917,8 @@ var SplineEntity = class extends Entity {
6805
6917
  r.stroke(stroke, this.lineWidth);
6806
6918
  }
6807
6919
  }
6808
- if (this.doc.paths) {
6809
- for (const path of this.doc.paths) {
6920
+ if (this._doc.paths) {
6921
+ for (const path of this._doc.paths) {
6810
6922
  const stroke = this.resolveColor(path.color_rgb, r);
6811
6923
  r.beginPath();
6812
6924
  if (path.data.length > 0) {
@@ -6845,8 +6957,8 @@ var SplineEntity = class extends Entity {
6845
6957
  ctx.lineWidth = this.lineWidth;
6846
6958
  ctx.lineCap = "round";
6847
6959
  ctx.lineJoin = "round";
6848
- if (this.doc.equations) {
6849
- for (const eq of this.doc.equations) {
6960
+ if (this._doc.equations) {
6961
+ for (const eq of this._doc.equations) {
6850
6962
  ctx.strokeStyle = eq.color_rgb === null ? this.defaultColor : Array.isArray(eq.color_rgb) ? rgbToCss(eq.color_rgb) : this.defaultColor;
6851
6963
  ctx.beginPath();
6852
6964
  for (const seg of eq.data) {
@@ -6857,8 +6969,8 @@ var SplineEntity = class extends Entity {
6857
6969
  ctx.stroke();
6858
6970
  }
6859
6971
  }
6860
- if (this.doc.paths) {
6861
- for (const path of this.doc.paths) {
6972
+ if (this._doc.paths) {
6973
+ for (const path of this._doc.paths) {
6862
6974
  ctx.strokeStyle = path.color_rgb === null ? this.defaultColor : Array.isArray(path.color_rgb) ? rgbToCss(path.color_rgb) : this.defaultColor;
6863
6975
  ctx.beginPath();
6864
6976
  if (path.data.length > 0) {
@@ -6920,7 +7032,14 @@ var Rect = class extends Entity {
6920
7032
  this.radius = opts.radius ?? 0;
6921
7033
  }
6922
7034
  getBounds() {
6923
- return { x: 0, y: 0, width: this.width, height: this.height };
7035
+ const inflation = this.stroke !== null ? this.strokeWidth / 2 : 0;
7036
+ const origin = inflation > 0 ? -inflation : 0;
7037
+ return {
7038
+ x: origin,
7039
+ y: origin,
7040
+ width: this.width + inflation * 2,
7041
+ height: this.height + inflation * 2
7042
+ };
6924
7043
  }
6925
7044
  /**
6926
7045
  * Solid-fill, square-cornered, unstroked rectangles opt into the GPU
@@ -6982,11 +7101,15 @@ var Circle = class extends Entity {
6982
7101
  this.a11yOffsetY = -this._radius;
6983
7102
  }
6984
7103
  getBounds() {
7104
+ const r = this._radius;
7105
+ const inflation = this.stroke !== null ? this.strokeWidth / 2 : 0;
7106
+ const fullRadius = r + inflation;
7107
+ const origin = fullRadius > 0 ? -fullRadius : 0;
6985
7108
  return {
6986
- x: -this._radius,
6987
- y: -this._radius,
6988
- width: this._radius * 2,
6989
- height: this._radius * 2
7109
+ x: origin,
7110
+ y: origin,
7111
+ width: fullRadius * 2,
7112
+ height: fullRadius * 2
6990
7113
  };
6991
7114
  }
6992
7115
  /**
package/dist/text.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkTRT2TRCWjs = require('./chunk-TRT2TRCW.js');
5
+ var _chunkFQ2565ITjs = require('./chunk-FQ2565IT.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 = _chunkTRT2TRCWjs.MSDFTextEntity; exports.SVGEntity = _chunkTRT2TRCWjs.SVGEntity;
12
+ exports.MSDFTextEntity = _chunkFQ2565ITjs.MSDFTextEntity; exports.SVGEntity = _chunkFQ2565ITjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MSDFTextEntity,
3
3
  SVGEntity
4
- } from "./chunk-RWN3ITBD.mjs";
4
+ } from "./chunk-YTGGL4I4.mjs";
5
5
 
6
6
  // src/text/index.ts
7
7
  export * from "@vectojs/text";
@@ -481,6 +481,8 @@ export declare class VectoJSEvent<N = unknown> {
481
481
  get deltaX(): number | undefined;
482
482
  /** Native vertical wheel delta, if this wraps a `WheelEvent`. */
483
483
  get deltaY(): number | undefined;
484
+ /** Native wheel delta mode (0=pixels, 1=lines, 2=pages), if this wraps a `WheelEvent`. */
485
+ get deltaMode(): number | undefined;
484
486
  /** Native pointer X, if this wraps a pointer/mouse event. */
485
487
  get clientX(): number | undefined;
486
488
  /** Native pointer Y, if this wraps a pointer/mouse event. */
@@ -799,6 +799,14 @@ export declare class Scene {
799
799
  private _wasmWorld;
800
800
  private _structureVersion;
801
801
  private _storeStructureVersion;
802
+ /**
803
+ * Consecutive `uploadRuns` rejections. Reset by any success and by
804
+ * {@link setTransformBackend}; at {@link WASM_UPLOAD_REJECT_LIMIT} the backend
805
+ * mode flips to `'js'` permanently.
806
+ */
807
+ private _wasmUploadRejections;
808
+ /** Latch for the permanent-fallback warning, which fires from a per-frame path. */
809
+ private hasWarnedWasmUploadFallback;
802
810
  private _computeEntities;
803
811
  private _computeEntitiesVersion;
804
812
  /** Invalidate the resident WASM store layout; the next wasm-mode frame rebuilds
@@ -1188,6 +1196,16 @@ export declare class Scene {
1188
1196
  * else the canvas). Stored so `destroy()` detaches from the same element. */
1189
1197
  private pointerEventTarget;
1190
1198
  private hasWarnedZeroSize;
1199
+ /**
1200
+ * Latch for {@link Scene.resize}'s invalid-dimension warning.
1201
+ *
1202
+ * Separate from `hasWarnedZeroSize`: that one fires for a tolerated zero-size
1203
+ * scene at `start()`, this one for a rejected resize, and sharing the flag
1204
+ * would let either suppress the other's first (and only) report. Latched
1205
+ * because `resize()` is commonly driven from a `ResizeObserver`, which would
1206
+ * otherwise warn on every frame of a drag.
1207
+ */
1208
+ private hasWarnedInvalidResize;
1191
1209
  private fontLoadHandler;
1192
1210
  private static _devMode;
1193
1211
  /**
@@ -1376,7 +1394,8 @@ export declare class Scene {
1376
1394
  /**
1377
1395
  * Begin the `requestAnimationFrame` render loop.
1378
1396
  *
1379
- * Idempotent — calling `start()` on an already-running scene is a no-op.
1397
+ * Idempotent — calling `start()` on an already-running scene is a no-op, and so
1398
+ * is calling it on a destroyed one.
1380
1399
  */
1381
1400
  start(): void;
1382
1401
  /** Schedule the next frame, or no-op where `requestAnimationFrame` is absent (SSR). */
@@ -1737,6 +1756,10 @@ export declare class Scene {
1737
1756
  toSVG(): string;
1738
1757
  /**
1739
1758
  * Manually resize the Scene's viewport.
1759
+ *
1760
+ * Rejects a negative or non-finite dimension, keeping the last known-good
1761
+ * size. `0` is accepted: a zero-size scene is tolerated and warned about at
1762
+ * {@link start} instead, and rejecting it here would contradict that.
1740
1763
  */
1741
1764
  resize(width: number, height: number): void;
1742
1765
  /** Effective device pixel ratio, matching CanvasRenderer: real DPR clamped to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.32.6",
3
+ "version": "1.32.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },