@vectojs/core 0.2.2 → 0.2.3

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7;
2
2
 
3
- var _chunkRW6NC4RBjs = require('./chunk-RW6NC4RB.js');
3
+ var _chunk6I53LI3Zjs = require('./chunk-6I53LI3Z.js');
4
4
 
5
5
  // src/math/SpringPhysics.ts
6
6
  var SpringPhysics = (_class = class {
@@ -128,14 +128,16 @@ var VectoJSEvent = (_class3 = class {
128
128
 
129
129
  /** Whether the event bubbles past its target (capture always runs). */
130
130
 
131
+
131
132
  __init8() {this.stopped = false}
132
133
  __init9() {this.stoppedImmediate = false}
133
- constructor(type, target, nativeEvent, bubbles = true) {;_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);
134
+ constructor(type, target, nativeEvent, bubbles = true, scenePoint) {;_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);
134
135
  this.type = type;
135
136
  this.target = target;
136
137
  this.currentTarget = target;
137
138
  this.nativeEvent = nativeEvent;
138
139
  this.bubbles = bubbles;
140
+ this.explicitScenePoint = scenePoint;
139
141
  }
140
142
  /** Stop the event from reaching the next node in the propagation path. */
141
143
  stopPropagation() {
@@ -178,9 +180,54 @@ var VectoJSEvent = (_class3 = class {
178
180
  get clientY() {
179
181
  return _optionalChain([this, 'access', _12 => _12.nativeEvent, 'optionalAccess', _13 => _13.clientY]);
180
182
  }
183
+ get resolvedScenePoint() {
184
+ if (this.explicitScenePoint) return this.explicitScenePoint;
185
+ const native = this.nativeEvent;
186
+ if (_optionalChain([native, 'optionalAccess', _14 => _14.vectoSceneX]) !== void 0 && native.vectoSceneY !== void 0) {
187
+ return { x: native.vectoSceneX, y: native.vectoSceneY };
188
+ }
189
+ if (_optionalChain([native, 'optionalAccess', _15 => _15.clientX]) === void 0 || native.clientY === void 0) return void 0;
190
+ const scene = this.target.scene;
191
+ return _nullishCoalesce(_optionalChain([scene, 'optionalAccess', _16 => _16.clientToScene, 'optionalCall', _17 => _17(native.clientX, native.clientY)]), () => ( {
192
+ x: native.clientX,
193
+ y: native.clientY
194
+ }));
195
+ }
196
+ /** Pointer X in the Scene's logical coordinate space. */
197
+ get sceneX() {
198
+ return _optionalChain([this, 'access', _18 => _18.resolvedScenePoint, 'optionalAccess', _19 => _19.x]);
199
+ }
200
+ /** Pointer Y in the Scene's logical coordinate space. */
201
+ get sceneY() {
202
+ return _optionalChain([this, 'access', _20 => _20.resolvedScenePoint, 'optionalAccess', _21 => _21.y]);
203
+ }
204
+ /** Pointer X local to the entity whose listener is currently running. */
205
+ get localX() {
206
+ const point = this.resolvedScenePoint;
207
+ if (!point) return void 0;
208
+ return _optionalChain([this, 'access', _22 => _22.currentTarget, 'access', _23 => _23.worldToLocal, 'call', _24 => _24(point.x, point.y), 'optionalAccess', _25 => _25.x]);
209
+ }
210
+ /** Pointer Y local to the entity whose listener is currently running. */
211
+ get localY() {
212
+ const point = this.resolvedScenePoint;
213
+ if (!point) return void 0;
214
+ return _optionalChain([this, 'access', _26 => _26.currentTarget, 'access', _27 => _27.worldToLocal, 'call', _28 => _28(point.x, point.y), 'optionalAccess', _29 => _29.y]);
215
+ }
216
+ get shiftKey() {
217
+ return !!_optionalChain([this, 'access', _30 => _30.nativeEvent, 'optionalAccess', _31 => _31.shiftKey]);
218
+ }
219
+ get ctrlKey() {
220
+ return !!_optionalChain([this, 'access', _32 => _32.nativeEvent, 'optionalAccess', _33 => _33.ctrlKey]);
221
+ }
222
+ get altKey() {
223
+ return !!_optionalChain([this, 'access', _34 => _34.nativeEvent, 'optionalAccess', _35 => _35.altKey]);
224
+ }
225
+ get metaKey() {
226
+ return !!_optionalChain([this, 'access', _36 => _36.nativeEvent, 'optionalAccess', _37 => _37.metaKey]);
227
+ }
181
228
  /** Native key, if this wraps a keyboard event. */
182
229
  get key() {
183
- return _optionalChain([this, 'access', _14 => _14.nativeEvent, 'optionalAccess', _15 => _15.key]);
230
+ return _optionalChain([this, 'access', _38 => _38.nativeEvent, 'optionalAccess', _39 => _39.key]);
184
231
  }
185
232
  }, _class3);
186
233
  var Entity = (_class4 = class {
@@ -412,28 +459,39 @@ var Entity = (_class4 = class {
412
459
  * that need to seed a starting state (e.g. the presence helper's enter `from`).
413
460
  */
414
461
  setImmediate(prop, v) {
462
+ const existing = this._drivers.get(prop);
463
+ if (existing) this._settleDriver(existing);
415
464
  this._drivers.delete(prop);
416
465
  this._applyAnimated(prop, v);
417
466
  }
467
+ _settleDriver(driver) {
468
+ const active = driver;
469
+ const onDone = active.onDone;
470
+ active.onDone = void 0;
471
+ _optionalChain([onDone, 'optionalCall', _40 => _40()]);
472
+ }
418
473
  _spawnDriver(prop, to, cfg) {
419
- if (prop !== "opacity" && _optionalChain([this, 'access', _16 => _16.scene, 'optionalAccess', _17 => _17.prefersReducedMotion])) {
474
+ if (prop !== "opacity" && _optionalChain([this, 'access', _41 => _41.scene, 'optionalAccess', _42 => _42.prefersReducedMotion])) {
475
+ const existing2 = this._drivers.get(prop);
476
+ if (existing2) this._settleDriver(existing2);
420
477
  this._drivers.delete(prop);
421
478
  this._applyAnimated(prop, to);
422
479
  return;
423
480
  }
424
481
  const existing = this._drivers.get(prop);
425
482
  if (existing) {
483
+ this._settleDriver(existing);
426
484
  existing.retarget(to);
427
485
  return;
428
486
  }
429
487
  const from = this._currentOf(prop);
430
488
  const driver = isTweenConfig(cfg) ? new TweenDriver(from, to, cfg) : new SpringDriver(from, to, cfg === "spring" ? {} : cfg);
431
489
  this._drivers.set(prop, driver);
432
- _optionalChain([this, 'access', _18 => _18.scene, 'optionalAccess', _19 => _19.markDirty, 'call', _20 => _20()]);
490
+ _optionalChain([this, 'access', _43 => _43.scene, 'optionalAccess', _44 => _44.markDirty, 'call', _45 => _45()]);
433
491
  }
434
492
  /** Assignment path when a declarative transition is configured for `prop`. */
435
493
  _animateProp(prop, to) {
436
- const cfg = _optionalChain([this, 'access', _21 => _21._transitions, 'optionalAccess', _22 => _22.get, 'call', _23 => _23(prop)]);
494
+ const cfg = _optionalChain([this, 'access', _46 => _46._transitions, 'optionalAccess', _47 => _47.get, 'call', _48 => _48(prop)]);
437
495
  if (!cfg) {
438
496
  this._applyAnimated(prop, to);
439
497
  return;
@@ -477,13 +535,13 @@ var Entity = (_class4 = class {
477
535
  driver.tick(dt);
478
536
  if (driver.isDone()) {
479
537
  this._applyAnimated(prop, driver.target);
480
- _optionalChain([driver, 'access', _24 => _24.onDone, 'optionalCall', _25 => _25()]);
538
+ this._settleDriver(driver);
481
539
  this._drivers.delete(prop);
482
540
  } else {
483
541
  this._applyAnimated(prop, driver.value);
484
542
  }
485
543
  }
486
- _optionalChain([this, 'access', _26 => _26.scene, 'optionalAccess', _27 => _27.markDirty, 'call', _28 => _28()]);
544
+ _optionalChain([this, 'access', _49 => _49.scene, 'optionalAccess', _50 => _50.markDirty, 'call', _51 => _51()]);
487
545
  }
488
546
  /**
489
547
  * Advance the entity's internal state for one frame.
@@ -532,7 +590,7 @@ var Entity = (_class4 = class {
532
590
  * @example entity.on('click', (e) => console.log('clicked', e));
533
591
  */
534
592
  on(event, callback, options) {
535
- const map = _optionalChain([options, 'optionalAccess', _29 => _29.capture]) ? this.captureListeners : this.listeners;
593
+ const map = _optionalChain([options, 'optionalAccess', _52 => _52.capture]) ? this.captureListeners : this.listeners;
536
594
  if (!map.has(event)) {
537
595
  map.set(event, []);
538
596
  }
@@ -548,7 +606,7 @@ var Entity = (_class4 = class {
548
606
  * @returns `this` for method chaining.
549
607
  */
550
608
  off(event, callback, options) {
551
- const handlers = (_optionalChain([options, 'optionalAccess', _30 => _30.capture]) ? this.captureListeners : this.listeners).get(event);
609
+ const handlers = (_optionalChain([options, 'optionalAccess', _53 => _53.capture]) ? this.captureListeners : this.listeners).get(event);
552
610
  if (handlers) {
553
611
  const idx = handlers.indexOf(callback);
554
612
  if (idx !== -1) handlers.splice(idx, 1);
@@ -621,19 +679,95 @@ var Entity = (_class4 = class {
621
679
  * @returns World-space {@link Point} for this entity.
622
680
  */
623
681
  getGlobalPosition() {
624
- let px = this.x;
625
- let py = this.y;
626
- let curr = this.parent;
627
- while (curr && curr.id !== "root") {
628
- const cos = Math.cos(curr.rotation);
629
- const sin = Math.sin(curr.rotation);
630
- const rotatedX = px * cos - py * sin;
631
- const rotatedY = px * sin + py * cos;
632
- px = curr.x + curr.scaleX * rotatedX;
633
- py = curr.y + curr.scaleY * rotatedY;
634
- curr = curr.parent;
682
+ return this.localToWorld(0, 0);
683
+ }
684
+ /**
685
+ * Return the exact accumulated Canvas `T * S * R` transform for this entity.
686
+ */
687
+ getWorldTransform() {
688
+ const path = this.id === "root" ? [] : [this];
689
+ let ancestor = this.parent;
690
+ while (ancestor && ancestor.id !== "root") {
691
+ path.push(ancestor);
692
+ ancestor = ancestor.parent;
693
+ }
694
+ let a = 1;
695
+ let b = 0;
696
+ let c = 0;
697
+ let d = 1;
698
+ let e = 0;
699
+ let f = 0;
700
+ for (let i = path.length - 1; i >= 0; i--) {
701
+ const node = path[i];
702
+ const cos = Math.cos(node.rotation);
703
+ const sin = Math.sin(node.rotation);
704
+ const la = node.scaleX * cos;
705
+ const lb = node.scaleY * sin;
706
+ const lc = -node.scaleX * sin;
707
+ const ld = node.scaleY * cos;
708
+ const le = node.x;
709
+ const lf = node.y;
710
+ const nextA = a * la + c * lb;
711
+ const nextB = b * la + d * lb;
712
+ const nextC = a * lc + c * ld;
713
+ const nextD = b * lc + d * ld;
714
+ const nextE = a * le + c * lf + e;
715
+ const nextF = b * le + d * lf + f;
716
+ a = nextA;
717
+ b = nextB;
718
+ c = nextC;
719
+ d = nextD;
720
+ e = nextE;
721
+ f = nextF;
635
722
  }
636
- return { x: px, y: py };
723
+ return { a, b, c, d, e, f };
724
+ }
725
+ /** Convert a point from this entity's local space to Scene/world space. */
726
+ localToWorld(localX, localY) {
727
+ const { a, b, c, d, e, f } = this.getWorldTransform();
728
+ return {
729
+ x: a * localX + c * localY + e,
730
+ y: b * localX + d * localY + f
731
+ };
732
+ }
733
+ /**
734
+ * Convert a Scene/world point into this entity's local space.
735
+ * Returns `null` when the accumulated transform is singular.
736
+ */
737
+ worldToLocal(worldX, worldY) {
738
+ const { a, b, c, d, e, f } = this.getWorldTransform();
739
+ const determinant = a * d - b * c;
740
+ if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-12) return null;
741
+ const x = worldX - e;
742
+ const y = worldY - f;
743
+ return {
744
+ x: (d * x - c * y) / determinant,
745
+ y: (-b * x + a * y) / determinant
746
+ };
747
+ }
748
+ /**
749
+ * Return the entity's local bounds transformed into a world-space AABB.
750
+ * Falls back to the entity's `[0, 0, width, height]` box when `getBounds()`
751
+ * does not provide a render-specific box.
752
+ */
753
+ getWorldBounds() {
754
+ const bounds = _nullishCoalesce(this.getBounds(), () => ( { x: 0, y: 0, width: this.width, height: this.height }));
755
+ const { a, b, c, d, e, f } = this.getWorldTransform();
756
+ let minX = Infinity;
757
+ let minY = Infinity;
758
+ let maxX = -Infinity;
759
+ let maxY = -Infinity;
760
+ for (let i = 0; i < 4; i++) {
761
+ const localX = i & 1 ? bounds.x + bounds.width : bounds.x;
762
+ const localY = i & 2 ? bounds.y + bounds.height : bounds.y;
763
+ const worldX = a * localX + c * localY + e;
764
+ const worldY = b * localX + d * localY + f;
765
+ minX = Math.min(minX, worldX);
766
+ minY = Math.min(minY, worldY);
767
+ maxX = Math.max(maxX, worldX);
768
+ maxY = Math.max(maxY, worldY);
769
+ }
770
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
637
771
  }
638
772
  /**
639
773
  * Accumulated world scale factors: this entity's own `scaleX`/`scaleY` times
@@ -705,12 +839,12 @@ var Entity = (_class4 = class {
705
839
  * Opt into the renderer's draw-call batching fast-path for point-cloud /
706
840
  * particle entities that draw as a single filled circle at their local origin.
707
841
  *
708
- * When a leaf entity returns a {@link BatchCircle} and has uniform scale, the
709
- * {@link Scene} skips its per-entity `save`/`translate`/`scale`/`rotate`/
710
- * `restore` and {@link render}, emitting the circle through
711
- * {@link IRenderer.fillCircle} so runs of same-color siblings coalesce into a
712
- * single `fill()`. Returns `null` by default (normal render path). Read each
713
- * frame, so an animated color/radius is honored.
842
+ * When a leaf entity returns a {@link BatchCircle} and its accumulated
843
+ * transform is representable by the selected batch backend, the {@link Scene}
844
+ * skips its per-entity `save`/`translate`/`scale`/`rotate`/`restore` and
845
+ * {@link render}. Canvas mode or an unsupported affine transform uses the
846
+ * normal render path, so implementations must keep {@link render} correct.
847
+ * Returns `null` by default. Read each frame, so animated color/radius works.
714
848
  *
715
849
  * @returns The circle to batch, or `null` to use the normal {@link render} path.
716
850
  */
@@ -874,7 +1008,7 @@ var MSDFTextEntity = (_class6 = class extends Entity {
874
1008
  setText(text) {
875
1009
  if (this.text === text && this.layoutResult) return;
876
1010
  this.text = text;
877
- _chunkRW6NC4RBjs.LayoutWorkerManager.getInstance().queueLayout(this.id, this.text, {
1011
+ _chunk6I53LI3Zjs.LayoutWorkerManager.getInstance().queueLayout(this.id, this.text, {
878
1012
  fontId: this.font.id,
879
1013
  fontSize: this.fontSize,
880
1014
  maxWidth: 1e3,
@@ -887,28 +1021,27 @@ var MSDFTextEntity = (_class6 = class extends Entity {
887
1021
  if (res.seqId < this.lastRenderedSeqId) return;
888
1022
  this.lastRenderedSeqId = res.seqId;
889
1023
  this.layoutResult = res;
890
- _optionalChain([this, 'access', _31 => _31.scene, 'optionalAccess', _32 => _32.markDirty, 'call', _33 => _33()]);
1024
+ _optionalChain([this, 'access', _54 => _54.scene, 'optionalAccess', _55 => _55.markDirty, 'call', _56 => _56()]);
891
1025
  }
892
1026
  });
893
1027
  }
894
1028
  isPointInside(globalX, globalY) {
895
1029
  if (!this.layoutResult) return false;
896
- const pos = this.getGlobalPosition();
897
- const scale = this.getWorldScale();
898
- const lx = (globalX - pos.x) / scale.x;
899
- const ly = (globalY - pos.y) / scale.y;
900
- return lx >= 0 && lx <= this.layoutResult.width && ly >= 0 && ly <= this.layoutResult.height;
1030
+ const local = this.worldToLocal(globalX, globalY);
1031
+ if (!local) return false;
1032
+ return local.x >= 0 && local.x <= this.layoutResult.width && local.y >= 0 && local.y <= this.layoutResult.height;
901
1033
  }
902
1034
  render(renderer) {
903
1035
  if (!this.layoutResult) return;
904
1036
  const scene = this.scene;
905
- if (scene && scene.pointRenderer && scene.glCanvas) {
1037
+ const world = this.getWorldTransform();
1038
+ const worldScaleX = Math.hypot(world.a, world.b);
1039
+ const worldScaleY = Math.hypot(world.c, world.d);
1040
+ const orthogonalTolerance = Math.max(1, worldScaleX * worldScaleY) * 1e-6;
1041
+ const canUsePointGlyphs = Number.isFinite(worldScaleX) && Number.isFinite(worldScaleY) && world.a * world.d - world.b * world.c >= 0 && Math.abs(world.a * world.c + world.b * world.d) <= orthogonalTolerance;
1042
+ if (scene && scene.pointRenderer && scene.glCanvas && canUsePointGlyphs) {
906
1043
  scene.pointRenderer.setMSDFTexture(this.texture, this.font.distanceRange);
907
- const globalPos = this.getGlobalPosition();
908
- const scale = this.getWorldScale();
909
- const worldRot = this.getWorldRotation();
910
- const rCos = Math.cos(worldRot);
911
- const rSin = Math.sin(worldRot);
1044
+ const worldRot = Math.atan2(world.b, world.a);
912
1045
  const len2 = this.layoutResult.codePoints.length;
913
1046
  for (let i = 0; i < len2; i++) {
914
1047
  const code = this.layoutResult.codePoints[i];
@@ -920,12 +1053,12 @@ var MSDFTextEntity = (_class6 = class extends Entity {
920
1053
  const { atlasBounds: ab, planeBounds: pb } = def;
921
1054
  const aw = this.font.atlasWidth;
922
1055
  const ah = this.font.atlasHeight;
923
- const lx = (nodeX + pb.left * this.fontSize) * scale.x;
924
- const ly = (nodeY - pb.top * this.fontSize) * scale.y;
925
- const glyphX = globalPos.x + lx * rCos - ly * rSin;
926
- const glyphY = globalPos.y + lx * rSin + ly * rCos;
927
- const glyphW = (pb.right - pb.left) * this.fontSize * scale.x;
928
- const glyphH = (pb.top - pb.bottom) * this.fontSize * scale.y;
1056
+ const lx = nodeX + pb.left * this.fontSize;
1057
+ const ly = nodeY - pb.top * this.fontSize;
1058
+ const glyphX = world.a * lx + world.c * ly + world.e;
1059
+ const glyphY = world.b * lx + world.d * ly + world.f;
1060
+ const glyphW = (pb.right - pb.left) * this.fontSize * worldScaleX;
1061
+ const glyphH = (pb.top - pb.bottom) * this.fontSize * worldScaleY;
929
1062
  const v0 = this.font.data.atlas.yOrigin === "bottom" ? 1 - ab.top / ah : ab.top / ah;
930
1063
  const v1 = this.font.data.atlas.yOrigin === "bottom" ? 1 - ab.bottom / ah : ab.bottom / ah;
931
1064
  const colorVal = packedStyle >>> 8;
@@ -979,7 +1112,7 @@ var MSDFTextEntity = (_class6 = class extends Entity {
979
1112
  }
980
1113
  }
981
1114
  destroy() {
982
- _chunkRW6NC4RBjs.LayoutWorkerManager.getInstance().cancelLayout(this.id);
1115
+ _chunk6I53LI3Zjs.LayoutWorkerManager.getInstance().cancelLayout(this.id);
983
1116
  super.destroy();
984
1117
  }
985
1118
  }, _class6);
@@ -1138,22 +1271,14 @@ var SVGEntity = (_class7 = class extends Entity {
1138
1271
  img.src = this.blobURL;
1139
1272
  }
1140
1273
  isPointInside(globalX, globalY) {
1141
- const pos = this.getGlobalPosition();
1142
- const scale = this.getWorldScale();
1143
- const rot = this.getWorldRotation();
1144
- const dx = globalX - pos.x;
1145
- const dy = globalY - pos.y;
1146
- const cos = Math.cos(-rot);
1147
- const sin = Math.sin(-rot);
1148
- const lx = (dx * cos - dy * sin) / scale.x;
1149
- const ly = (dx * sin + dy * cos) / scale.y;
1150
- return lx >= 0 && lx <= this.width && ly >= 0 && ly <= this.height;
1274
+ const local = this.worldToLocal(globalX, globalY);
1275
+ if (!local) return false;
1276
+ return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
1151
1277
  }
1152
1278
  render(r) {
1153
- const isSVGExporter = typeof r.toXMLString === "function";
1154
- if (isSVGExporter) {
1155
- const dataUri = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(this.svgSource);
1156
- r.drawImage({ src: dataUri }, 0, 0, this.width, this.height);
1279
+ const svgRenderer = r;
1280
+ if (typeof svgRenderer.drawSVG === "function") {
1281
+ svgRenderer.drawSVG(this.svgSource, 0, 0, this.width, this.height);
1157
1282
  return;
1158
1283
  }
1159
1284
  const scale = this.getWorldScale();
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ArabicShaper,
3
3
  BidiResolver
4
- } from "./chunk-YA2J5ZH7.mjs";
4
+ } from "./chunk-YSS44ADQ.mjs";
5
5
 
6
6
  // src/layout/LayoutEngine.ts
7
7
  function computeLineSegments(top, bottom, maxWidth, exclusions) {
@@ -185,8 +185,50 @@ var CanvasRenderer = class _CanvasRenderer {
185
185
  }
186
186
  return grad;
187
187
  }
188
+ /**
189
+ * Canvas2D drawing contexts are automatically released when their
190
+ * `<canvas>` element is GC'd, so there's no explicit GPU handle to free.
191
+ * This method clears our internal batch state and is idempotent.
192
+ */
193
+ dispose() {
194
+ this.batchCount = 0;
195
+ this.batchColor = "";
196
+ this.batchAlpha = 1;
197
+ this.batchActive = false;
198
+ }
188
199
  };
189
200
 
201
+ // src/renderer/url.ts
202
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "ftp:"]);
203
+ var SCHEME_PATTERN = /^[a-z][a-z\d+.-]*$/i;
204
+ function normalizeSchemeCandidate(value) {
205
+ let result = "";
206
+ for (const character of value) {
207
+ const code = character.charCodeAt(0);
208
+ if (code > 32 && (code < 127 || code > 159)) result += character;
209
+ }
210
+ return result;
211
+ }
212
+ function hasSafeSchemeOrIsRelative(url) {
213
+ const colonIndex = url.indexOf(":");
214
+ if (colonIndex < 0) return true;
215
+ const candidate = normalizeSchemeCandidate(url.slice(0, colonIndex));
216
+ if (!SCHEME_PATTERN.test(candidate)) return true;
217
+ return SAFE_SCHEMES.has(`${candidate.toLowerCase()}:`);
218
+ }
219
+ function sanitizeUrl(href) {
220
+ if (typeof href !== "string") return "";
221
+ const trimmed = href.trim();
222
+ if (trimmed === "") return "";
223
+ return hasSafeSchemeOrIsRelative(trimmed) ? trimmed : "#";
224
+ }
225
+ function isSafeUrl(urlStr) {
226
+ if (typeof urlStr !== "string") return false;
227
+ const trimmed = urlStr.trim();
228
+ if (trimmed === "") return false;
229
+ return hasSafeSchemeOrIsRelative(trimmed);
230
+ }
231
+
190
232
  // src/renderer/SVGRenderer.ts
191
233
  var SVGRenderer = class {
192
234
  width;
@@ -395,7 +437,7 @@ var SVGRenderer = class {
395
437
  }
396
438
  fill(colorOrGradient) {
397
439
  this.flush();
398
- const fillVal = this.resolveGradient(colorOrGradient);
440
+ const fillVal = this.escapeXML(this.resolveGradient(colorOrGradient));
399
441
  const dStr = this.currentPath.join(" ");
400
442
  const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
401
443
  this.buffer.push(
@@ -404,7 +446,7 @@ var SVGRenderer = class {
404
446
  }
405
447
  stroke(colorOrGradient, lineWidth = 1) {
406
448
  this.flush();
407
- const strokeVal = this.resolveGradient(colorOrGradient);
449
+ const strokeVal = this.escapeXML(this.resolveGradient(colorOrGradient));
408
450
  const dStr = this.currentPath.join(" ");
409
451
  const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
410
452
  this.buffer.push(
@@ -424,7 +466,7 @@ var SVGRenderer = class {
424
466
  const fontWeight = fontWeightMatch ? fontWeightMatch[1].toLowerCase() : "normal";
425
467
  const cleanFont = font.replace(/\d+(?:\.\d+)?(px|em|rem)(?:\/\d+(?:\.\d+)?(?:px|em|rem|%)?)?/, "").trim();
426
468
  const fontFamily = cleanFont.replace(/(bold|italic|normal|600|500|400|300|100)\s+/gi, "").trim() || "sans-serif";
427
- const fillVal = this.resolveGradient(color);
469
+ const fillVal = this.escapeXML(this.resolveGradient(color));
428
470
  const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
429
471
  this.buffer.push(
430
472
  `<g transform="${transformStr}"><text x="${x}" y="${y}" font-size="${fontSize}" font-weight="${fontWeight}" font-style="${fontStyle}" font-family="${this.escapeXML(fontFamily)}" fill="${fillVal}" opacity="${this.globalAlpha}">${this.escapeXML(text)}</text></g>`
@@ -446,7 +488,9 @@ var SVGRenderer = class {
446
488
  }
447
489
  drawImage(source, dx, dy, dw, dh) {
448
490
  this.flush();
449
- const href = typeof source?.toDataURL === "function" ? source.toDataURL() : source?.src || "";
491
+ const fromCanvas2 = typeof source?.toDataURL === "function";
492
+ const rawHref = fromCanvas2 ? source.toDataURL() : source?.src || "";
493
+ const href = fromCanvas2 && this.isSafeRasterDataUrl(rawHref) ? rawHref : sanitizeUrl(String(rawHref));
450
494
  const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
451
495
  if (href) {
452
496
  this.buffer.push(
@@ -459,6 +503,21 @@ var SVGRenderer = class {
459
503
  console.warn("drawImage source fallback triggered");
460
504
  }
461
505
  }
506
+ /**
507
+ * Embed SVG markup as an isolated nested image.
508
+ *
509
+ * This explicit path is used by `SVGEntity` during vector export. General
510
+ * `drawImage()` input remains subject to the URL policy and therefore still
511
+ * rejects caller-provided SVG data URLs.
512
+ */
513
+ drawSVG(source, dx, dy, dw, dh) {
514
+ this.flush();
515
+ const href = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
516
+ const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
517
+ this.buffer.push(
518
+ `<image href="${this.escapeXML(href)}" x="${dx}" y="${dy}" width="${dw}" height="${dh}" transform="${transformStr}" />`
519
+ );
520
+ }
462
521
  flush() {
463
522
  if (!this.batchActive || this.batchCircles.length === 0) return;
464
523
  let d = "";
@@ -566,6 +625,17 @@ var SVGRenderer = class {
566
625
  }
567
626
  });
568
627
  }
628
+ isSafeRasterDataUrl(value) {
629
+ return typeof value === "string" && /^data:image\/(?:png|jpeg|gif|webp);base64,[a-z\d+/=\s]*$/i.test(value);
630
+ }
631
+ /**
632
+ * SVGRenderer accumulates strings in memory; nothing external is allocated.
633
+ * Drop the buffers for GC and become idempotent.
634
+ */
635
+ dispose() {
636
+ this.clear();
637
+ this.gradientCache.clear();
638
+ }
569
639
  };
570
640
 
571
641
  // src/renderer/colorParse.ts
@@ -831,6 +901,7 @@ function createWebGLPointRenderer(canvas) {
831
901
  let logicalW = 0;
832
902
  let logicalH = 0;
833
903
  let dpr = 1;
904
+ let destroyed = false;
834
905
  return {
835
906
  resize(width, height) {
836
907
  logicalW = width;
@@ -1037,6 +1108,8 @@ function createWebGLPointRenderer(canvas) {
1037
1108
  gl.bindVertexArray(null);
1038
1109
  },
1039
1110
  destroy() {
1111
+ if (destroyed) return;
1112
+ destroyed = true;
1040
1113
  gl.deleteBuffer(pointBuffer);
1041
1114
  gl.deleteBuffer(rectBuffer);
1042
1115
  gl.deleteBuffer(spriteBuffer);
@@ -1326,10 +1399,14 @@ var WebGPUParticleSystemManager = class {
1326
1399
  if (!this.computePipeline || !entity.computeBindGroup) return;
1327
1400
  const uniformArray = new Float32Array(20);
1328
1401
  const color = parseColorToRGBA(entity.baseColor);
1402
+ let opacity = 1;
1403
+ for (let current = entity; current; current = current.parent) {
1404
+ opacity *= current.opacity;
1405
+ }
1329
1406
  uniformArray[0] = color[0];
1330
1407
  uniformArray[1] = color[1];
1331
1408
  uniformArray[2] = color[2];
1332
- uniformArray[3] = color[3];
1409
+ uniformArray[3] = color[3] * opacity;
1333
1410
  const mActive = !isNaN(mouseX) && !isNaN(mouseY) && mouseX > -9e3 && mouseY > -9e3;
1334
1411
  uniformArray[4] = mActive ? mouseX : -9999;
1335
1412
  uniformArray[5] = mActive ? mouseY : -9999;
@@ -1373,6 +1450,8 @@ var WebGPUParticleSystemManager = class {
1373
1450
 
1374
1451
  export {
1375
1452
  CanvasRenderer,
1453
+ sanitizeUrl,
1454
+ isSafeUrl,
1376
1455
  SVGRenderer,
1377
1456
  parseColorToRGBA,
1378
1457
  createWebGLPointRenderer,