@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.
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;
@@ -721,6 +744,7 @@ var Scene = class _Scene {
721
744
  el.id = node.id;
722
745
  el.setAttribute("data-vecto-id", node.id);
723
746
  el.style.position = "absolute";
747
+ el.style.transformOrigin = "0 0";
724
748
  el.style.pointerEvents = "auto";
725
749
  el.style.touchAction = "pinch-zoom";
726
750
  el.style.margin = "0";
@@ -868,7 +892,8 @@ var Scene = class _Scene {
868
892
  if (el.placeholder !== attrs.placeholder) el.placeholder = attrs.placeholder;
869
893
  }
870
894
  if (attrs.href !== void 0 && el instanceof HTMLAnchorElement) {
871
- 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);
872
897
  }
873
898
  if (el instanceof HTMLImageElement) {
874
899
  if (attrs.src !== void 0 && el.src !== attrs.src) el.src = attrs.src;
@@ -929,12 +954,12 @@ var Scene = class _Scene {
929
954
  el.style.height = `${this.height}px`;
930
955
  el.style.transform = "";
931
956
  } else {
932
- const pos = node.getGlobalPosition();
933
- el.style.left = `${pos.x + node.a11yOffsetX}px`;
934
- el.style.top = `${pos.y + node.a11yOffsetY}px`;
935
- el.style.width = `${node.width * node.scaleX}px`;
936
- el.style.height = `${node.height * node.scaleY}px`;
937
- 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)`;
938
963
  }
939
964
  }
940
965
  for (const child of node.children) this.syncA11y(child);
@@ -996,6 +1021,35 @@ var Scene = class _Scene {
996
1021
  }
997
1022
  this.a11yNeedsReorder = false;
998
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
+ }
999
1053
  getA11yTree() {
1000
1054
  const map = /* @__PURE__ */ new Map();
1001
1055
  const roots = [];
@@ -1039,7 +1093,7 @@ var Scene = class _Scene {
1039
1093
  traverse(this.root, null);
1040
1094
  return roots;
1041
1095
  }
1042
- renderPortalDOM(portal, te, tf, a, b, c, d) {
1096
+ renderPortalDOM(portal, te, tf, a, b, c, d, opacity) {
1043
1097
  if (!this.portalRoot) return;
1044
1098
  this.activePortalsThisFrame.add(portal.id);
1045
1099
  this.portalEntities.set(portal.id, portal);
@@ -1073,6 +1127,11 @@ var Scene = class _Scene {
1073
1127
  portal.domElement.style.zIndex = zIndexStr;
1074
1128
  portal.lastZIndex = zIndexStr;
1075
1129
  }
1130
+ const opacityStr = String(opacity);
1131
+ if (portal.lastOpacity !== opacityStr) {
1132
+ portal.domElement.style.opacity = opacityStr;
1133
+ portal.lastOpacity = opacityStr;
1134
+ }
1076
1135
  }
1077
1136
  reconcilePortals() {
1078
1137
  if (!this.portalRoot) return;
@@ -1143,14 +1202,18 @@ var Scene = class _Scene {
1143
1202
  * @param time - Current absolute time in milliseconds (default 0).
1144
1203
  */
1145
1204
  render(renderer, dt = 0, time = 0) {
1146
- if (this.a11yRoot && this.canvas.parentElement) {
1205
+ const isMainRenderer = renderer === this.renderer;
1206
+ if (isMainRenderer && this.a11yRoot && this.canvas.parentElement) {
1147
1207
  const parentStyle = this.canvas.parentElement.style;
1148
1208
  if (!parentStyle.position || parentStyle.position === "static") {
1149
1209
  parentStyle.position = "relative";
1150
1210
  }
1211
+ this.syncOverlayGeometry();
1212
+ }
1213
+ if (isMainRenderer) {
1214
+ this.renderOrderCounter = 0;
1215
+ this.activePortalsThisFrame.clear();
1151
1216
  }
1152
- this.renderOrderCounter = 0;
1153
- this.activePortalsThisFrame.clear();
1154
1217
  const computeEntities = [];
1155
1218
  const collectComputeEntities = (node) => {
1156
1219
  if (node instanceof ComputeParticleEntity) {
@@ -1165,7 +1228,8 @@ var Scene = class _Scene {
1165
1228
  collectComputeEntities(overlay);
1166
1229
  }
1167
1230
  if (computeEntities.length > 0) {
1168
- 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) {
1169
1233
  this.initializingWebGPU = true;
1170
1234
  this.initWebGPUContext(computeEntities).then((newDevice) => {
1171
1235
  this.device = newDevice;
@@ -1188,12 +1252,16 @@ var Scene = class _Scene {
1188
1252
  }
1189
1253
  }
1190
1254
  }).catch((err) => {
1191
- 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
+ }
1192
1260
  this.webgpuDisabled = true;
1193
1261
  this.initializingWebGPU = false;
1194
1262
  });
1195
1263
  }
1196
- if (this.device && this.manager && !this.deviceLost && !this.webgpuDisabled) {
1264
+ if (isMainRenderPath && this.device && this.manager && !this.deviceLost && !this.webgpuDisabled) {
1197
1265
  try {
1198
1266
  const commandEncoder = this.device.createCommandEncoder();
1199
1267
  const computePass = commandEncoder.beginComputePass();
@@ -1241,21 +1309,20 @@ var Scene = class _Scene {
1241
1309
  this.device = null;
1242
1310
  this.recreateWebGPUDeviceWithRetry(computeEntities);
1243
1311
  }
1244
- } else {
1312
+ } else if (isMainRenderPath) {
1245
1313
  for (const entity of computeEntities) {
1246
1314
  entity.updateCPU(dt / 1e3, this.mouseX, this.mouseY, this.width, this.height);
1247
1315
  }
1248
1316
  }
1249
1317
  }
1250
1318
  renderer.clear();
1251
- const isMainRenderer = renderer === this.renderer;
1252
1319
  if (isMainRenderer) {
1253
1320
  this.pointRenderer?.begin();
1254
1321
  }
1255
1322
  const vw = this.width;
1256
1323
  const vh = this.height;
1257
- const renderNode = (node, pa, pb, pc, pd, pe, pf) => {
1258
- node.update(dt, time);
1324
+ const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
1325
+ if (isMainRenderer) node.update(dt, time);
1259
1326
  const cos = Math.cos(node.rotation);
1260
1327
  const sin = Math.sin(node.rotation);
1261
1328
  const te = pa * node.x + pc * node.y + pe;
@@ -1264,16 +1331,24 @@ var Scene = class _Scene {
1264
1331
  const sxSin = node.scaleX * sin;
1265
1332
  const syCos = node.scaleY * cos;
1266
1333
  const sySin = node.scaleY * sin;
1267
- const a = pa * sxCos + pc * sxSin;
1268
- const b = pb * sxCos + pd * sxSin;
1269
- const c = pa * -sySin + pc * syCos;
1270
- const d = pb * -sySin + pd * syCos;
1271
- 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;
1272
1345
  if (a11yEl) {
1273
1346
  a11yEl.style.zIndex = String(this.renderOrderCounter++);
1274
1347
  }
1275
1348
  if (node.isDOMPortal) {
1276
- this.renderPortalDOM(node, te, tf, a, b, c, d);
1349
+ if (isMainRenderer) {
1350
+ this.renderPortalDOM(node, te, tf, a, b, c, d, worldOpacity);
1351
+ }
1277
1352
  return;
1278
1353
  }
1279
1354
  let visible = true;
@@ -1299,33 +1374,27 @@ var Scene = class _Scene {
1299
1374
  if (node.children.length === 0 && node.scaleX === node.scaleY) {
1300
1375
  const bc = node.getBatchCircle();
1301
1376
  if (bc) {
1302
- if (visible) {
1303
- if (isMainRenderer && this.pointRenderer) {
1304
- this.pointRenderer.addCircle(
1305
- te,
1306
- tf,
1307
- bc.radius * Math.hypot(a, b),
1308
- bc.color,
1309
- node.opacity
1310
- );
1311
- } else {
1312
- 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;
1313
1382
  }
1383
+ } else {
1384
+ renderer.fillCircle(node.x, node.y, bc.radius * node.scaleX, bc.color, worldOpacity);
1385
+ return;
1314
1386
  }
1315
- return;
1316
- }
1317
- if (isMainRenderer && this.pointRenderer) {
1387
+ } else if (isMainRenderer && this.pointRenderer) {
1318
1388
  const br = node.getBatchRect();
1319
- if (br) {
1389
+ if (br && isSimilarityTransform && a * d - b * c >= 0) {
1320
1390
  if (visible) {
1321
- const ws = Math.hypot(a, b);
1322
1391
  this.pointRenderer.addRect(
1323
1392
  te,
1324
1393
  tf,
1325
- br.width * ws,
1326
- br.height * ws,
1394
+ br.width * worldScaleX,
1395
+ br.height * worldScaleX,
1327
1396
  br.color,
1328
- node.opacity,
1397
+ worldOpacity,
1329
1398
  Math.atan2(b, a)
1330
1399
  );
1331
1400
  }
@@ -1338,11 +1407,11 @@ var Scene = class _Scene {
1338
1407
  renderer.translate(node.x, node.y);
1339
1408
  renderer.scale(node.scaleX, node.scaleY);
1340
1409
  renderer.rotate(node.rotation);
1341
- renderer.setGlobalAlpha(node.opacity);
1410
+ renderer.setGlobalAlpha(worldOpacity);
1342
1411
  if (visible) {
1343
1412
  if (node instanceof ComputeParticleEntity) {
1344
1413
  if (this.deviceLost || this.webgpuDisabled || !this.device || !this.manager) {
1345
- this.renderCPUParticles(renderer, node);
1414
+ this.renderCPUParticles(renderer, node, worldOpacity);
1346
1415
  }
1347
1416
  } else {
1348
1417
  node.render(renderer);
@@ -1352,16 +1421,16 @@ var Scene = class _Scene {
1352
1421
  renderer.clip(0, 0, node.width, node.height);
1353
1422
  }
1354
1423
  for (const child of node.children) {
1355
- renderNode(child, a, b, c, d, te, tf);
1424
+ renderNode(child, a, b, c, d, te, tf, worldOpacity);
1356
1425
  }
1357
1426
  renderer.flush();
1358
1427
  renderer.restore();
1359
1428
  };
1360
- renderNode(this.root, 1, 0, 0, 1, 0, 0);
1429
+ renderNode(this.root, 1, 0, 0, 1, 0, 0, 1);
1361
1430
  for (const overlay of this.overlayRoot.children) {
1362
- renderNode(overlay, 1, 0, 0, 1, 0, 0);
1431
+ renderNode(overlay, 1, 0, 0, 1, 0, 0, 1);
1363
1432
  }
1364
- this.reconcilePortals();
1433
+ if (isMainRenderer) this.reconcilePortals();
1365
1434
  renderer.flush();
1366
1435
  if (isMainRenderer) {
1367
1436
  this.pointRenderer?.flush();
@@ -1491,13 +1560,15 @@ var Scene = class _Scene {
1491
1560
  this.manager.initPipelines(format);
1492
1561
  for (const entity of entities) {
1493
1562
  this.manager.setupEntityResources(entity);
1494
- newDevice.queue.writeBuffer(entity.gpuStorageBuffer, 0, entity.particleData);
1563
+ if (entity.gpuStorageBuffer) {
1564
+ newDevice.queue.writeBuffer(entity.gpuStorageBuffer, 0, entity.particleData);
1565
+ }
1495
1566
  }
1496
1567
  }
1497
1568
  }).catch(() => this.recreateWebGPUDeviceWithRetry(entities, attempt + 1));
1498
1569
  }, backoff);
1499
1570
  }
1500
- renderCPUParticles(renderer, entity) {
1571
+ renderCPUParticles(renderer, entity, worldOpacity) {
1501
1572
  const data = entity.particleData;
1502
1573
  const size = entity.maxParticles;
1503
1574
  const isMain = renderer === this.renderer;
@@ -1508,7 +1579,7 @@ var Scene = class _Scene {
1508
1579
  const pSize = data[idx + 6];
1509
1580
  const life = data[idx + 7];
1510
1581
  if (life === 0) continue;
1511
- const opacity = life < 0 ? entity.opacity : entity.opacity * Math.min(1, life);
1582
+ const opacity = life < 0 ? worldOpacity : worldOpacity * Math.min(1, life);
1512
1583
  const scale = life >= 0 ? Math.min(1, life) : 1;
1513
1584
  if (isMain && this.pointRenderer) {
1514
1585
  this.pointRenderer.addCircle(x, y, pSize * scale, entity.baseColor, opacity);
@@ -1592,10 +1663,9 @@ var TextEntity = class extends Entity {
1592
1663
  this.a11yOffsetY = 0;
1593
1664
  }
1594
1665
  isPointInside(globalX, globalY) {
1595
- const pos = this.getGlobalPosition();
1596
- const lx = globalX - pos.x;
1597
- const ly = globalY - pos.y;
1598
- 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;
1599
1669
  }
1600
1670
  render(renderer) {
1601
1671
  const currentFill = this.isHovered ? this.hoveredFillStyle : this.fillStyle;
@@ -1810,10 +1880,9 @@ var SplineEntity = class extends Entity {
1810
1880
  * this method already calls it as a refinement when it is overridden.
1811
1881
  */
1812
1882
  isPointInside(globalX, globalY) {
1813
- const pos = this.getGlobalPosition();
1814
- const scale = this.getWorldScale();
1815
- const lx = (globalX - pos.x) / (scale.x || 1);
1816
- 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;
1817
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;
1818
1887
  if (!inAabb) return false;
1819
1888
  const refined = this.hitTestCurve(lx, ly);
@@ -2097,6 +2166,7 @@ var DOMPortalEntity = class extends Entity {
2097
2166
  lastHeight = "";
2098
2167
  lastTransform = "";
2099
2168
  lastZIndex = "";
2169
+ lastOpacity = "";
2100
2170
  constructor(domElement, width, height, id) {
2101
2171
  super(id);
2102
2172
  this.domElement = domElement;
@@ -2147,22 +2217,15 @@ var DOMPortalEntity = class extends Entity {
2147
2217
  }
2148
2218
  }
2149
2219
  isPointInside(globalX, globalY) {
2150
- const pos = this.getGlobalPosition();
2151
- const scale = this.getWorldScale();
2152
- const rot = this.getWorldRotation();
2153
- const dx = globalX - pos.x;
2154
- const dy = globalY - pos.y;
2155
- const cos = Math.cos(-rot);
2156
- const sin = Math.sin(-rot);
2157
- const lx = (dx * cos - dy * sin) / scale.x;
2158
- const ly = (dx * sin + dy * cos) / scale.y;
2159
2220
  const w = this.width > 0 ? this.width : this.cachedWidth;
2160
2221
  const h = this.height > 0 ? this.height : this.cachedHeight;
2161
- 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;
2162
2225
  }
2163
- add(child) {
2226
+ add(_child) {
2164
2227
  console.warn(`DOMPortalEntity (${this.id}) is a leaf node. Child entities are not supported.`);
2165
- return super.add(child);
2228
+ return this;
2166
2229
  }
2167
2230
  render() {
2168
2231
  }
@@ -2225,8 +2288,10 @@ export {
2225
2288
  computeLineSegments,
2226
2289
  createCanvasMeasurer,
2227
2290
  createWebGLPointRenderer,
2291
+ isSafeUrl,
2228
2292
  isTweenConfig,
2229
2293
  loadSpline,
2230
2294
  parseColorToRGBA,
2231
- polySegmentToBezier
2295
+ polySegmentToBezier,
2296
+ sanitizeUrl
2232
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;
package/dist/renderer.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  WebGPUParticleSystemManager,
5
5
  createWebGLPointRenderer,
6
6
  parseColorToRGBA
7
- } from "./chunk-2Y45S4JK.mjs";
7
+ } from "./chunk-NKOQV3RM.mjs";
8
8
  export {
9
9
  CanvasRenderer,
10
10
  SVGRenderer,
package/dist/text.js CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkLA3FJLP2js = require('./chunk-LA3FJLP2.js');
5
+ var _chunkKPWVPGHRjs = require('./chunk-KPWVPGHR.js');
6
6
 
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.ArabicShaper = _chunkRW6NC4RBjs.ArabicShaper; exports.BidiResolver = _chunkRW6NC4RBjs.BidiResolver; exports.MSDFFont = _chunkLA3FJLP2js.MSDFFont; exports.MSDFTextEntity = _chunkLA3FJLP2js.MSDFTextEntity; exports.SVGEntity = _chunkLA3FJLP2js.SVGEntity;
16
+ exports.ArabicShaper = _chunk6I53LI3Zjs.ArabicShaper; exports.BidiResolver = _chunk6I53LI3Zjs.BidiResolver; exports.MSDFFont = _chunkKPWVPGHRjs.MSDFFont; exports.MSDFTextEntity = _chunkKPWVPGHRjs.MSDFTextEntity; exports.SVGEntity = _chunkKPWVPGHRjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -2,11 +2,11 @@ import {
2
2
  MSDFFont,
3
3
  MSDFTextEntity,
4
4
  SVGEntity
5
- } from "./chunk-H3QIE77O.mjs";
5
+ } from "./chunk-ISGOYXPF.mjs";
6
6
  import {
7
7
  ArabicShaper,
8
8
  BidiResolver
9
- } from "./chunk-YA2J5ZH7.mjs";
9
+ } from "./chunk-YSS44ADQ.mjs";
10
10
  export {
11
11
  ArabicShaper,
12
12
  BidiResolver,