@vectojs/core 0.2.1 → 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.
package/dist/index.mjs CHANGED
@@ -3,14 +3,16 @@ import {
3
3
  LayoutResultBuffer,
4
4
  computeLineSegments,
5
5
  createCanvasMeasurer
6
- } from "./chunk-T456DL4P.mjs";
6
+ } from "./chunk-LIOJ37MH.mjs";
7
7
  import {
8
8
  CanvasRenderer,
9
9
  SVGRenderer,
10
10
  WebGPUParticleSystemManager,
11
11
  createWebGLPointRenderer,
12
- parseColorToRGBA
13
- } from "./chunk-2Y45S4JK.mjs";
12
+ isSafeUrl,
13
+ parseColorToRGBA,
14
+ sanitizeUrl
15
+ } from "./chunk-NKOQV3RM.mjs";
14
16
  import {
15
17
  Easing,
16
18
  Entity,
@@ -22,12 +24,12 @@ import {
22
24
  TweenDriver,
23
25
  VectoJSEvent,
24
26
  isTweenConfig
25
- } from "./chunk-H3QIE77O.mjs";
27
+ } from "./chunk-ISGOYXPF.mjs";
26
28
  import {
27
29
  ArabicShaper,
28
30
  BidiResolver,
29
31
  LayoutWorkerManager
30
- } from "./chunk-YA2J5ZH7.mjs";
32
+ } from "./chunk-YSS44ADQ.mjs";
31
33
 
32
34
  // src/tree/ComputeParticleEntity.ts
33
35
  var PARTICLE_STRIDE_FLOATS = 8;
@@ -489,6 +491,17 @@ var Scene = class _Scene {
489
491
  getRenderer() {
490
492
  return this.renderer;
491
493
  }
494
+ /** Convert browser viewport coordinates into this Scene's logical coordinates. */
495
+ clientToScene(clientX, clientY) {
496
+ const rect = this.canvas.getBoundingClientRect?.();
497
+ if (!rect) return { x: clientX, y: clientY };
498
+ const cssWidth = rect.width || this.canvas.clientWidth || this.width;
499
+ const cssHeight = rect.height || this.canvas.clientHeight || this.height;
500
+ return {
501
+ x: (clientX - rect.left) * (cssWidth > 0 ? this.width / cssWidth : 1),
502
+ y: (clientY - rect.top) * (cssHeight > 0 ? this.height / cssHeight : 1)
503
+ };
504
+ }
492
505
  /**
493
506
  * Add a top-level entity to the scene graph.
494
507
  *
@@ -563,12 +576,21 @@ var Scene = class _Scene {
563
576
  this.removeA11yRecursively(overlay);
564
577
  this.markDirty();
565
578
  }
579
+ destroyEntitySubtree(entity) {
580
+ while (entity.children.length > 0) this.destroyEntitySubtree(entity.children.at(-1));
581
+ entity.destroy();
582
+ }
566
583
  /**
567
584
  * Tear down the Scene, halt the loop, and clean up event listeners and DOM elements.
568
585
  */
569
586
  destroy() {
587
+ if (this.destroyed) return;
570
588
  this.destroyed = true;
571
589
  this.stop();
590
+ while (this.root.children.length > 0) this.destroyEntitySubtree(this.root.children.at(-1));
591
+ while (this.overlayRoot.children.length > 0) {
592
+ this.destroyEntitySubtree(this.overlayRoot.children.at(-1));
593
+ }
572
594
  if (typeof window !== "undefined" && !this.disableWindowResize) {
573
595
  window.removeEventListener("resize", this.resizeHandler);
574
596
  }
@@ -584,6 +606,7 @@ var Scene = class _Scene {
584
606
  this.portalRoot?.remove();
585
607
  this.a11yElements.clear();
586
608
  this.pointRenderer?.destroy();
609
+ this.renderer.dispose?.();
587
610
  this.glCanvas?.remove();
588
611
  this.gpuCanvas?.remove();
589
612
  this.gpuCanvas = null;
@@ -603,9 +626,9 @@ var Scene = class _Scene {
603
626
  }
604
627
  if (typeof window !== "undefined" && this.canvas && typeof this.canvas.addEventListener === "function") {
605
628
  this.pointerMoveListener = (e) => {
606
- const rect = this.canvas.getBoundingClientRect();
607
- this.mouseX = e.clientX - rect.left;
608
- this.mouseY = e.clientY - rect.top;
629
+ const point = this.clientToScene(e.clientX, e.clientY);
630
+ this.mouseX = point.x;
631
+ this.mouseY = point.y;
609
632
  };
610
633
  this.pointerLeaveListener = () => {
611
634
  this.mouseX = -9999;
@@ -656,6 +679,17 @@ var Scene = class _Scene {
656
679
  this.caretBlinkTimer = null;
657
680
  }
658
681
  }
682
+ /**
683
+ * Manually advance the scene clock by `dt` milliseconds and render synchronously.
684
+ * Essential for deterministic rendering (e.g. video export).
685
+ * Note: You should call `scene.stop()` before using this to avoid conflict with the rAF loop.
686
+ */
687
+ step(dt) {
688
+ const time = this.lastTime + dt;
689
+ this.lastTime = time;
690
+ this.render(this.renderer, dt, time);
691
+ this.dirty = false;
692
+ }
659
693
  /**
660
694
  * Mark the scene as needing a redraw on the next frame.
661
695
  *
@@ -710,6 +744,7 @@ var Scene = class _Scene {
710
744
  el.id = node.id;
711
745
  el.setAttribute("data-vecto-id", node.id);
712
746
  el.style.position = "absolute";
747
+ el.style.transformOrigin = "0 0";
713
748
  el.style.pointerEvents = "auto";
714
749
  el.style.touchAction = "pinch-zoom";
715
750
  el.style.margin = "0";
@@ -857,7 +892,8 @@ var Scene = class _Scene {
857
892
  if (el.placeholder !== attrs.placeholder) el.placeholder = attrs.placeholder;
858
893
  }
859
894
  if (attrs.href !== void 0 && el instanceof HTMLAnchorElement) {
860
- if (el.getAttribute("href") !== attrs.href) el.setAttribute("href", attrs.href);
895
+ const safeHref = sanitizeUrl(attrs.href);
896
+ if (el.getAttribute("href") !== safeHref) el.setAttribute("href", safeHref);
861
897
  }
862
898
  if (el instanceof HTMLImageElement) {
863
899
  if (attrs.src !== void 0 && el.src !== attrs.src) el.src = attrs.src;
@@ -918,12 +954,12 @@ var Scene = class _Scene {
918
954
  el.style.height = `${this.height}px`;
919
955
  el.style.transform = "";
920
956
  } else {
921
- const pos = node.getGlobalPosition();
922
- el.style.left = `${pos.x + node.a11yOffsetX}px`;
923
- el.style.top = `${pos.y + node.a11yOffsetY}px`;
924
- el.style.width = `${node.width * node.scaleX}px`;
925
- el.style.height = `${node.height * node.scaleY}px`;
926
- el.style.transform = `rotate(${node.rotation}rad)`;
957
+ const { a, b, c, d, e, f } = node.getWorldTransform();
958
+ el.style.left = `${e + node.a11yOffsetX}px`;
959
+ el.style.top = `${f + node.a11yOffsetY}px`;
960
+ el.style.width = `${node.width}px`;
961
+ el.style.height = `${node.height}px`;
962
+ el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
927
963
  }
928
964
  }
929
965
  for (const child of node.children) this.syncA11y(child);
@@ -985,6 +1021,35 @@ var Scene = class _Scene {
985
1021
  }
986
1022
  this.a11yNeedsReorder = false;
987
1023
  }
1024
+ /** Keep DOM/WebGL overlay layers aligned with the canvas's CSS box. */
1025
+ syncOverlayGeometry() {
1026
+ const parent = this.canvas.parentElement;
1027
+ if (!parent) return;
1028
+ const canvasRect = this.canvas.getBoundingClientRect?.();
1029
+ const parentRect = parent.getBoundingClientRect?.();
1030
+ const cssWidth = canvasRect?.width || this.canvas.clientWidth || this.width;
1031
+ const cssHeight = canvasRect?.height || this.canvas.clientHeight || this.height;
1032
+ const left = (canvasRect?.left ?? 0) - (parentRect?.left ?? 0) - (parent.clientLeft || 0) + parent.scrollLeft;
1033
+ const top = (canvasRect?.top ?? 0) - (parentRect?.top ?? 0) - (parent.clientTop || 0) + parent.scrollTop;
1034
+ const scaleX = this.width > 0 ? cssWidth / this.width : 1;
1035
+ const scaleY = this.height > 0 ? cssHeight / this.height : 1;
1036
+ for (const root of [this.a11yRoot, this.portalRoot]) {
1037
+ if (!root) continue;
1038
+ root.style.left = `${left}px`;
1039
+ root.style.top = `${top}px`;
1040
+ root.style.width = `${this.width}px`;
1041
+ root.style.height = `${this.height}px`;
1042
+ root.style.transformOrigin = "0 0";
1043
+ root.style.transform = `scale(${scaleX}, ${scaleY})`;
1044
+ }
1045
+ for (const canvas of [this.glCanvas, this.gpuCanvas]) {
1046
+ if (!canvas) continue;
1047
+ canvas.style.left = `${left}px`;
1048
+ canvas.style.top = `${top}px`;
1049
+ canvas.style.width = `${cssWidth}px`;
1050
+ canvas.style.height = `${cssHeight}px`;
1051
+ }
1052
+ }
988
1053
  getA11yTree() {
989
1054
  const map = /* @__PURE__ */ new Map();
990
1055
  const roots = [];
@@ -1028,7 +1093,7 @@ var Scene = class _Scene {
1028
1093
  traverse(this.root, null);
1029
1094
  return roots;
1030
1095
  }
1031
- renderPortalDOM(portal, te, tf, a, b, c, d) {
1096
+ renderPortalDOM(portal, te, tf, a, b, c, d, opacity) {
1032
1097
  if (!this.portalRoot) return;
1033
1098
  this.activePortalsThisFrame.add(portal.id);
1034
1099
  this.portalEntities.set(portal.id, portal);
@@ -1062,6 +1127,11 @@ var Scene = class _Scene {
1062
1127
  portal.domElement.style.zIndex = zIndexStr;
1063
1128
  portal.lastZIndex = zIndexStr;
1064
1129
  }
1130
+ const opacityStr = String(opacity);
1131
+ if (portal.lastOpacity !== opacityStr) {
1132
+ portal.domElement.style.opacity = opacityStr;
1133
+ portal.lastOpacity = opacityStr;
1134
+ }
1065
1135
  }
1066
1136
  reconcilePortals() {
1067
1137
  if (!this.portalRoot) return;
@@ -1109,19 +1179,17 @@ var Scene = class _Scene {
1109
1179
  }
1110
1180
  this.render(this.renderer, dt, time);
1111
1181
  const hasActiveAnimation = this.hasAnyPendingAnimation(this.root) || this.hasAnyPendingAnimation(this.overlayRoot);
1112
- if (hasActiveAnimation) {
1182
+ const hasInteractive = this.hasAnyInteractive(this.root) || this.hasAnyInteractive(this.overlayRoot);
1183
+ const shouldSyncInterval = this.a11ySyncInterval <= 0 || time - this.lastA11ySync >= this.a11ySyncInterval;
1184
+ if ((hasInteractive || this.a11yElements.size > 0) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1185
+ this.lastA11ySync = time;
1186
+ if (hasInteractive) {
1187
+ this.syncA11y(this.root);
1188
+ }
1189
+ this.enforceA11yDomOrder();
1190
+ this.a11yPendingSyncAfterAnimation = hasActiveAnimation;
1191
+ } else if (hasActiveAnimation) {
1113
1192
  this.a11yPendingSyncAfterAnimation = true;
1114
- } else {
1115
- const hasInteractive = this.hasAnyInteractive(this.root) || this.hasAnyInteractive(this.overlayRoot);
1116
- const shouldSyncInterval = this.a11ySyncInterval <= 0 || time - this.lastA11ySync >= this.a11ySyncInterval;
1117
- if ((hasInteractive || this.a11yElements.size > 0) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
1118
- this.lastA11ySync = time;
1119
- if (hasInteractive) {
1120
- this.syncA11y(this.root);
1121
- }
1122
- this.enforceA11yDomOrder();
1123
- this.a11yPendingSyncAfterAnimation = false;
1124
- }
1125
1193
  }
1126
1194
  this.dirty = false;
1127
1195
  this.scheduleFrame();
@@ -1134,14 +1202,18 @@ var Scene = class _Scene {
1134
1202
  * @param time - Current absolute time in milliseconds (default 0).
1135
1203
  */
1136
1204
  render(renderer, dt = 0, time = 0) {
1137
- if (this.a11yRoot && this.canvas.parentElement) {
1205
+ const isMainRenderer = renderer === this.renderer;
1206
+ if (isMainRenderer && this.a11yRoot && this.canvas.parentElement) {
1138
1207
  const parentStyle = this.canvas.parentElement.style;
1139
1208
  if (!parentStyle.position || parentStyle.position === "static") {
1140
1209
  parentStyle.position = "relative";
1141
1210
  }
1211
+ this.syncOverlayGeometry();
1212
+ }
1213
+ if (isMainRenderer) {
1214
+ this.renderOrderCounter = 0;
1215
+ this.activePortalsThisFrame.clear();
1142
1216
  }
1143
- this.renderOrderCounter = 0;
1144
- this.activePortalsThisFrame.clear();
1145
1217
  const computeEntities = [];
1146
1218
  const collectComputeEntities = (node) => {
1147
1219
  if (node instanceof ComputeParticleEntity) {
@@ -1156,7 +1228,8 @@ var Scene = class _Scene {
1156
1228
  collectComputeEntities(overlay);
1157
1229
  }
1158
1230
  if (computeEntities.length > 0) {
1159
- if (!this.device && !this.webgpuDisabled && !this.initializingWebGPU && !this.deviceLost) {
1231
+ const isMainRenderPath = renderer === this.renderer;
1232
+ if (isMainRenderPath && !this.device && !this.webgpuDisabled && !this.initializingWebGPU && !this.deviceLost) {
1160
1233
  this.initializingWebGPU = true;
1161
1234
  this.initWebGPUContext(computeEntities).then((newDevice) => {
1162
1235
  this.device = newDevice;
@@ -1179,12 +1252,16 @@ var Scene = class _Scene {
1179
1252
  }
1180
1253
  }
1181
1254
  }).catch((err) => {
1182
- console.error("Failed to initialize WebGPU:", err);
1255
+ if (this.particleBackend === "webgpu") {
1256
+ console.error("Failed to initialize WebGPU:", err);
1257
+ } else {
1258
+ console.warn("WebGPU unavailable; using CPU particle fallback.", err);
1259
+ }
1183
1260
  this.webgpuDisabled = true;
1184
1261
  this.initializingWebGPU = false;
1185
1262
  });
1186
1263
  }
1187
- if (this.device && this.manager && !this.deviceLost && !this.webgpuDisabled) {
1264
+ if (isMainRenderPath && this.device && this.manager && !this.deviceLost && !this.webgpuDisabled) {
1188
1265
  try {
1189
1266
  const commandEncoder = this.device.createCommandEncoder();
1190
1267
  const computePass = commandEncoder.beginComputePass();
@@ -1232,21 +1309,20 @@ var Scene = class _Scene {
1232
1309
  this.device = null;
1233
1310
  this.recreateWebGPUDeviceWithRetry(computeEntities);
1234
1311
  }
1235
- } else {
1312
+ } else if (isMainRenderPath) {
1236
1313
  for (const entity of computeEntities) {
1237
1314
  entity.updateCPU(dt / 1e3, this.mouseX, this.mouseY, this.width, this.height);
1238
1315
  }
1239
1316
  }
1240
1317
  }
1241
1318
  renderer.clear();
1242
- const isMainRenderer = renderer === this.renderer;
1243
1319
  if (isMainRenderer) {
1244
1320
  this.pointRenderer?.begin();
1245
1321
  }
1246
1322
  const vw = this.width;
1247
1323
  const vh = this.height;
1248
- const renderNode = (node, pa, pb, pc, pd, pe, pf) => {
1249
- node.update(dt, time);
1324
+ const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
1325
+ if (isMainRenderer) node.update(dt, time);
1250
1326
  const cos = Math.cos(node.rotation);
1251
1327
  const sin = Math.sin(node.rotation);
1252
1328
  const te = pa * node.x + pc * node.y + pe;
@@ -1255,16 +1331,24 @@ var Scene = class _Scene {
1255
1331
  const sxSin = node.scaleX * sin;
1256
1332
  const syCos = node.scaleY * cos;
1257
1333
  const sySin = node.scaleY * sin;
1258
- const a = pa * sxCos + pc * sxSin;
1259
- const b = pb * sxCos + pd * sxSin;
1260
- const c = pa * -sySin + pc * syCos;
1261
- const d = pb * -sySin + pd * syCos;
1262
- const a11yEl = this.a11yElements.get(node.id);
1334
+ const a = pa * sxCos + pc * sySin;
1335
+ const b = pb * sxCos + pd * sySin;
1336
+ const c = pa * -sxSin + pc * syCos;
1337
+ const d = pb * -sxSin + pd * syCos;
1338
+ const worldScaleX = Math.hypot(a, b);
1339
+ const worldScaleY = Math.hypot(c, d);
1340
+ const worldOpacity = parentOpacity * node.opacity;
1341
+ const scaleTolerance = Math.max(1, worldScaleX, worldScaleY) * 1e-6;
1342
+ const orthogonalTolerance = Math.max(1, worldScaleX * worldScaleY) * 1e-6;
1343
+ const isSimilarityTransform = Number.isFinite(worldScaleX) && Number.isFinite(worldScaleY) && Math.abs(worldScaleX - worldScaleY) <= scaleTolerance && Math.abs(a * c + b * d) <= orthogonalTolerance;
1344
+ const a11yEl = isMainRenderer ? this.a11yElements.get(node.id) : void 0;
1263
1345
  if (a11yEl) {
1264
1346
  a11yEl.style.zIndex = String(this.renderOrderCounter++);
1265
1347
  }
1266
1348
  if (node.isDOMPortal) {
1267
- this.renderPortalDOM(node, te, tf, a, b, c, d);
1349
+ if (isMainRenderer) {
1350
+ this.renderPortalDOM(node, te, tf, a, b, c, d, worldOpacity);
1351
+ }
1268
1352
  return;
1269
1353
  }
1270
1354
  let visible = true;
@@ -1290,33 +1374,27 @@ var Scene = class _Scene {
1290
1374
  if (node.children.length === 0 && node.scaleX === node.scaleY) {
1291
1375
  const bc = node.getBatchCircle();
1292
1376
  if (bc) {
1293
- if (visible) {
1294
- if (isMainRenderer && this.pointRenderer) {
1295
- this.pointRenderer.addCircle(
1296
- te,
1297
- tf,
1298
- bc.radius * Math.hypot(a, b),
1299
- bc.color,
1300
- node.opacity
1301
- );
1302
- } else {
1303
- renderer.fillCircle(node.x, node.y, bc.radius * node.scaleX, bc.color, node.opacity);
1377
+ if (!visible) return;
1378
+ if (isMainRenderer && this.pointRenderer) {
1379
+ if (isSimilarityTransform) {
1380
+ this.pointRenderer.addCircle(te, tf, bc.radius * worldScaleX, bc.color, worldOpacity);
1381
+ return;
1304
1382
  }
1383
+ } else {
1384
+ renderer.fillCircle(node.x, node.y, bc.radius * node.scaleX, bc.color, worldOpacity);
1385
+ return;
1305
1386
  }
1306
- return;
1307
- }
1308
- if (isMainRenderer && this.pointRenderer) {
1387
+ } else if (isMainRenderer && this.pointRenderer) {
1309
1388
  const br = node.getBatchRect();
1310
- if (br) {
1389
+ if (br && isSimilarityTransform && a * d - b * c >= 0) {
1311
1390
  if (visible) {
1312
- const ws = Math.hypot(a, b);
1313
1391
  this.pointRenderer.addRect(
1314
1392
  te,
1315
1393
  tf,
1316
- br.width * ws,
1317
- br.height * ws,
1394
+ br.width * worldScaleX,
1395
+ br.height * worldScaleX,
1318
1396
  br.color,
1319
- node.opacity,
1397
+ worldOpacity,
1320
1398
  Math.atan2(b, a)
1321
1399
  );
1322
1400
  }
@@ -1329,11 +1407,11 @@ var Scene = class _Scene {
1329
1407
  renderer.translate(node.x, node.y);
1330
1408
  renderer.scale(node.scaleX, node.scaleY);
1331
1409
  renderer.rotate(node.rotation);
1332
- renderer.setGlobalAlpha(node.opacity);
1410
+ renderer.setGlobalAlpha(worldOpacity);
1333
1411
  if (visible) {
1334
1412
  if (node instanceof ComputeParticleEntity) {
1335
1413
  if (this.deviceLost || this.webgpuDisabled || !this.device || !this.manager) {
1336
- this.renderCPUParticles(renderer, node);
1414
+ this.renderCPUParticles(renderer, node, worldOpacity);
1337
1415
  }
1338
1416
  } else {
1339
1417
  node.render(renderer);
@@ -1343,16 +1421,16 @@ var Scene = class _Scene {
1343
1421
  renderer.clip(0, 0, node.width, node.height);
1344
1422
  }
1345
1423
  for (const child of node.children) {
1346
- renderNode(child, a, b, c, d, te, tf);
1424
+ renderNode(child, a, b, c, d, te, tf, worldOpacity);
1347
1425
  }
1348
1426
  renderer.flush();
1349
1427
  renderer.restore();
1350
1428
  };
1351
- renderNode(this.root, 1, 0, 0, 1, 0, 0);
1429
+ renderNode(this.root, 1, 0, 0, 1, 0, 0, 1);
1352
1430
  for (const overlay of this.overlayRoot.children) {
1353
- renderNode(overlay, 1, 0, 0, 1, 0, 0);
1431
+ renderNode(overlay, 1, 0, 0, 1, 0, 0, 1);
1354
1432
  }
1355
- this.reconcilePortals();
1433
+ if (isMainRenderer) this.reconcilePortals();
1356
1434
  renderer.flush();
1357
1435
  if (isMainRenderer) {
1358
1436
  this.pointRenderer?.flush();
@@ -1482,13 +1560,15 @@ var Scene = class _Scene {
1482
1560
  this.manager.initPipelines(format);
1483
1561
  for (const entity of entities) {
1484
1562
  this.manager.setupEntityResources(entity);
1485
- newDevice.queue.writeBuffer(entity.gpuStorageBuffer, 0, entity.particleData);
1563
+ if (entity.gpuStorageBuffer) {
1564
+ newDevice.queue.writeBuffer(entity.gpuStorageBuffer, 0, entity.particleData);
1565
+ }
1486
1566
  }
1487
1567
  }
1488
1568
  }).catch(() => this.recreateWebGPUDeviceWithRetry(entities, attempt + 1));
1489
1569
  }, backoff);
1490
1570
  }
1491
- renderCPUParticles(renderer, entity) {
1571
+ renderCPUParticles(renderer, entity, worldOpacity) {
1492
1572
  const data = entity.particleData;
1493
1573
  const size = entity.maxParticles;
1494
1574
  const isMain = renderer === this.renderer;
@@ -1499,7 +1579,7 @@ var Scene = class _Scene {
1499
1579
  const pSize = data[idx + 6];
1500
1580
  const life = data[idx + 7];
1501
1581
  if (life === 0) continue;
1502
- const opacity = life < 0 ? entity.opacity : entity.opacity * Math.min(1, life);
1582
+ const opacity = life < 0 ? worldOpacity : worldOpacity * Math.min(1, life);
1503
1583
  const scale = life >= 0 ? Math.min(1, life) : 1;
1504
1584
  if (isMain && this.pointRenderer) {
1505
1585
  this.pointRenderer.addCircle(x, y, pSize * scale, entity.baseColor, opacity);
@@ -1583,10 +1663,9 @@ var TextEntity = class extends Entity {
1583
1663
  this.a11yOffsetY = 0;
1584
1664
  }
1585
1665
  isPointInside(globalX, globalY) {
1586
- const pos = this.getGlobalPosition();
1587
- const lx = globalX - pos.x;
1588
- const ly = globalY - pos.y;
1589
- return lx >= 0 && lx <= this.width && ly >= 0 && ly <= this.height;
1666
+ const local = this.worldToLocal(globalX, globalY);
1667
+ if (!local) return false;
1668
+ return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
1590
1669
  }
1591
1670
  render(renderer) {
1592
1671
  const currentFill = this.isHovered ? this.hoveredFillStyle : this.fillStyle;
@@ -1801,10 +1880,9 @@ var SplineEntity = class extends Entity {
1801
1880
  * this method already calls it as a refinement when it is overridden.
1802
1881
  */
1803
1882
  isPointInside(globalX, globalY) {
1804
- const pos = this.getGlobalPosition();
1805
- const scale = this.getWorldScale();
1806
- const lx = (globalX - pos.x) / (scale.x || 1);
1807
- const ly = (globalY - pos.y) / (scale.y || 1);
1883
+ const local = this.worldToLocal(globalX, globalY);
1884
+ if (!local) return false;
1885
+ const { x: lx, y: ly } = local;
1808
1886
  const inAabb = lx >= this.bounds.x && lx <= this.bounds.x + this.bounds.width && ly >= this.bounds.y && ly <= this.bounds.y + this.bounds.height;
1809
1887
  if (!inAabb) return false;
1810
1888
  const refined = this.hitTestCurve(lx, ly);
@@ -2088,6 +2166,7 @@ var DOMPortalEntity = class extends Entity {
2088
2166
  lastHeight = "";
2089
2167
  lastTransform = "";
2090
2168
  lastZIndex = "";
2169
+ lastOpacity = "";
2091
2170
  constructor(domElement, width, height, id) {
2092
2171
  super(id);
2093
2172
  this.domElement = domElement;
@@ -2138,22 +2217,15 @@ var DOMPortalEntity = class extends Entity {
2138
2217
  }
2139
2218
  }
2140
2219
  isPointInside(globalX, globalY) {
2141
- const pos = this.getGlobalPosition();
2142
- const scale = this.getWorldScale();
2143
- const rot = this.getWorldRotation();
2144
- const dx = globalX - pos.x;
2145
- const dy = globalY - pos.y;
2146
- const cos = Math.cos(-rot);
2147
- const sin = Math.sin(-rot);
2148
- const lx = (dx * cos - dy * sin) / scale.x;
2149
- const ly = (dx * sin + dy * cos) / scale.y;
2150
2220
  const w = this.width > 0 ? this.width : this.cachedWidth;
2151
2221
  const h = this.height > 0 ? this.height : this.cachedHeight;
2152
- return lx >= 0 && lx <= w && ly >= 0 && ly <= h;
2222
+ const local = this.worldToLocal(globalX, globalY);
2223
+ if (!local) return false;
2224
+ return local.x >= 0 && local.x <= w && local.y >= 0 && local.y <= h;
2153
2225
  }
2154
- add(child) {
2226
+ add(_child) {
2155
2227
  console.warn(`DOMPortalEntity (${this.id}) is a leaf node. Child entities are not supported.`);
2156
- return super.add(child);
2228
+ return this;
2157
2229
  }
2158
2230
  render() {
2159
2231
  }
@@ -2216,8 +2288,10 @@ export {
2216
2288
  computeLineSegments,
2217
2289
  createCanvasMeasurer,
2218
2290
  createWebGLPointRenderer,
2291
+ isSafeUrl,
2219
2292
  isTweenConfig,
2220
2293
  loadSpline,
2221
2294
  parseColorToRGBA,
2222
- polySegmentToBezier
2295
+ polySegmentToBezier,
2296
+ sanitizeUrl
2223
2297
  };
@@ -7,6 +7,10 @@ export declare class LayoutWorkerManager {
7
7
  private seqIdCounter;
8
8
  private debounceTimers;
9
9
  private constructor();
10
+ private createWorker;
11
+ private ensureWorker;
12
+ private handleWorkerFailure;
13
+ destroy(): void;
10
14
  static getInstance(): LayoutWorkerManager;
11
15
  queueLayout(entityId: string, text: string, options: {
12
16
  fontId: string;
package/dist/layout.js CHANGED
@@ -3,14 +3,14 @@
3
3
 
4
4
 
5
5
 
6
- var _chunk72WVPMSJjs = require('./chunk-72WVPMSJ.js');
6
+ var _chunkVVOQNUBKjs = require('./chunk-VVOQNUBK.js');
7
7
 
8
8
 
9
- var _chunkRW6NC4RBjs = require('./chunk-RW6NC4RB.js');
9
+ var _chunk6I53LI3Zjs = require('./chunk-6I53LI3Z.js');
10
10
 
11
11
 
12
12
 
13
13
 
14
14
 
15
15
 
16
- exports.LayoutEngine = _chunk72WVPMSJjs.LayoutEngine; exports.LayoutResultBuffer = _chunk72WVPMSJjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunkRW6NC4RBjs.LayoutWorkerManager; exports.computeLineSegments = _chunk72WVPMSJjs.computeLineSegments; exports.createCanvasMeasurer = _chunk72WVPMSJjs.createCanvasMeasurer;
16
+ exports.LayoutEngine = _chunkVVOQNUBKjs.LayoutEngine; exports.LayoutResultBuffer = _chunkVVOQNUBKjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunk6I53LI3Zjs.LayoutWorkerManager; exports.computeLineSegments = _chunkVVOQNUBKjs.computeLineSegments; exports.createCanvasMeasurer = _chunkVVOQNUBKjs.createCanvasMeasurer;
package/dist/layout.mjs CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  LayoutResultBuffer,
4
4
  computeLineSegments,
5
5
  createCanvasMeasurer
6
- } from "./chunk-T456DL4P.mjs";
6
+ } from "./chunk-LIOJ37MH.mjs";
7
7
  import {
8
8
  LayoutWorkerManager
9
- } from "./chunk-YA2J5ZH7.mjs";
9
+ } from "./chunk-YSS44ADQ.mjs";
10
10
  export {
11
11
  LayoutEngine,
12
12
  LayoutResultBuffer,
@@ -78,4 +78,10 @@ export declare class CanvasRenderer implements IRenderer {
78
78
  stop: number;
79
79
  color: string;
80
80
  }[]): any;
81
+ /**
82
+ * Canvas2D drawing contexts are automatically released when their
83
+ * `<canvas>` element is GC'd, so there's no explicit GPU handle to free.
84
+ * This method clears our internal batch state and is idempotent.
85
+ */
86
+ dispose(): void;
81
87
  }
@@ -175,4 +175,13 @@ export interface IRenderer {
175
175
  stop: number;
176
176
  color: string;
177
177
  }[]): any;
178
+ /**
179
+ * Release any backend-owned GPU textures / GL contexts / caches.
180
+ *
181
+ * Called by {@link Scene.destroy()} so renderers that hold scarce resources
182
+ * (e.g. a WebGL2 context — browsers cap concurrent contexts to ~16) clean up
183
+ * before GC. Implementations MUST be idempotent: a second call after a
184
+ * successful teardown must be a silent no-op, not throw.
185
+ */
186
+ dispose?(): void;
178
187
  }
@@ -57,6 +57,14 @@ export declare class SVGRenderer implements IRenderer {
57
57
  fillText(text: string, x: number, y: number, font: string, color: string | SVGLinearGradient): void;
58
58
  fillCircle(cx: number, cy: number, radius: number, color: string, alpha?: number): void;
59
59
  drawImage(source: any, dx: number, dy: number, dw: number, dh: number): void;
60
+ /**
61
+ * Embed SVG markup as an isolated nested image.
62
+ *
63
+ * This explicit path is used by `SVGEntity` during vector export. General
64
+ * `drawImage()` input remains subject to the URL policy and therefore still
65
+ * rejects caller-provided SVG data URLs.
66
+ */
67
+ drawSVG(source: string, dx: number, dy: number, dw: number, dh: number): void;
60
68
  flush(): void;
61
69
  createLinearGradient(x0: number, y0: number, x1: number, y1: number, colorStops: {
62
70
  stop: number;
@@ -66,4 +74,10 @@ export declare class SVGRenderer implements IRenderer {
66
74
  toXMLString(): string;
67
75
  private resolveGradient;
68
76
  private escapeXML;
77
+ private isSafeRasterDataUrl;
78
+ /**
79
+ * SVGRenderer accumulates strings in memory; nothing external is allocated.
80
+ * Drop the buffers for GC and become idempotent.
81
+ */
82
+ dispose(): void;
69
83
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * URL sanitization helpers used by accessibility sinks (shadow `<a>` elements,
3
+ * `window.open`, Markdown link renders, …) to prevent `javascript:` / `data:`
4
+ * URI-script injection.
5
+ *
6
+ * The goal is conservative: allow safe browsing/navigation schemes, rewrite
7
+ * everything else to a benign `#` placeholder so click handlers resolve without
8
+ * executing payload or compromising the host DOM.
9
+ */
10
+ /**
11
+ * Sanitize a potentially untrusted `href` / URL string for projection onto
12
+ * an `<a>` element or a `window.open` call.
13
+ *
14
+ * Behaviour:
15
+ * 1. Returns `''` for `null`/`undefined`/non-string input.
16
+ * 2. Trims leading whitespace (browsers do this before scheme resolution).
17
+ * 3. If the URL is relative (no scheme, or starts with `#`, `?`, `/`, `./`),
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.
21
+ * 5. Otherwise returns the canonical `URL.toString()` form.
22
+ *
23
+ * The function never throws; malformed input falls back to `'#'`.
24
+ */
25
+ export declare function sanitizeUrl(href: string | null | undefined): string;
26
+ /**
27
+ * Narrower guard used by link renderers that already know they hold an
28
+ * absolute URL: returns `true` if `urlStr` uses a scheme in
29
+ * {@link SAFE_SCHEMES}, `false` otherwise. Relative URLs are considered safe.
30
+ */
31
+ export declare function isSafeUrl(urlStr: string): boolean;
package/dist/renderer.js CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkLIX7DJTIjs = require('./chunk-LIX7DJTI.js');
7
+ var _chunkTXA3LZDMjs = require('./chunk-TXA3LZDM.js');
8
8
 
9
9
 
10
10
 
11
11
 
12
12
 
13
13
 
14
- exports.CanvasRenderer = _chunkLIX7DJTIjs.CanvasRenderer; exports.SVGRenderer = _chunkLIX7DJTIjs.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkLIX7DJTIjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkLIX7DJTIjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkLIX7DJTIjs.parseColorToRGBA;
14
+ exports.CanvasRenderer = _chunkTXA3LZDMjs.CanvasRenderer; exports.SVGRenderer = _chunkTXA3LZDMjs.SVGRenderer; exports.WebGPUParticleSystemManager = _chunkTXA3LZDMjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkTXA3LZDMjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkTXA3LZDMjs.parseColorToRGBA;