@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.
@@ -185,8 +185,50 @@ var CanvasRenderer = (_class = 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
  }, _class.__initStatic(), _class);
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 = (_class2 = class {
192
234
 
@@ -395,7 +437,7 @@ var SVGRenderer = (_class2 = 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 = (_class2 = 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 = (_class2 = 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 = (_class2 = class {
446
488
  }
447
489
  drawImage(source, dx, dy, dw, dh) {
448
490
  this.flush();
449
- const href = typeof _optionalChain([source, 'optionalAccess', _ => _.toDataURL]) === "function" ? source.toDataURL() : _optionalChain([source, 'optionalAccess', _2 => _2.src]) || "";
491
+ const fromCanvas2 = typeof _optionalChain([source, 'optionalAccess', _ => _.toDataURL]) === "function";
492
+ const rawHref = fromCanvas2 ? source.toDataURL() : _optionalChain([source, 'optionalAccess', _2 => _2.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 = (_class2 = 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 = (_class2 = 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
  }, _class2);
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 = (_class3 = 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;
@@ -1377,4 +1454,6 @@ var WebGPUParticleSystemManager = (_class3 = class {
1377
1454
 
1378
1455
 
1379
1456
 
1380
- exports.CanvasRenderer = CanvasRenderer; exports.SVGRenderer = SVGRenderer; exports.parseColorToRGBA = parseColorToRGBA; exports.createWebGLPointRenderer = createWebGLPointRenderer; exports.WebGPUParticleSystemManager = WebGPUParticleSystemManager;
1457
+
1458
+
1459
+ exports.CanvasRenderer = CanvasRenderer; exports.sanitizeUrl = sanitizeUrl; exports.isSafeUrl = isSafeUrl; exports.SVGRenderer = SVGRenderer; exports.parseColorToRGBA = parseColorToRGBA; exports.createWebGLPointRenderer = createWebGLPointRenderer; exports.WebGPUParticleSystemManager = WebGPUParticleSystemManager;
@@ -1,7 +1,7 @@
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;
2
2
 
3
3
 
4
- var _chunkRW6NC4RBjs = require('./chunk-RW6NC4RB.js');
4
+ var _chunk6I53LI3Zjs = require('./chunk-6I53LI3Z.js');
5
5
 
6
6
  // src/layout/LayoutEngine.ts
7
7
  function computeLineSegments(top, bottom, maxWidth, exclusions) {
@@ -142,8 +142,8 @@ var LayoutEngine = (_class = class {
142
142
  offset += paragraph.length + 1;
143
143
  continue;
144
144
  }
145
- const { shapedText, indexMap } = _chunkRW6NC4RBjs.ArabicShaper.shapeArabic(paragraph);
146
- const levels = _chunkRW6NC4RBjs.BidiResolver.resolveLevels(shapedText);
145
+ const { shapedText, indexMap } = _chunk6I53LI3Zjs.ArabicShaper.shapeArabic(paragraph);
146
+ const levels = _chunk6I53LI3Zjs.BidiResolver.resolveLevels(shapedText);
147
147
  const words = [];
148
148
  let shapedCharIdx = 0;
149
149
  let pFallback = false;
@@ -192,7 +192,7 @@ var LayoutEngine = (_class = class {
192
192
  words,
193
193
  isEmpty: false,
194
194
  fallbackToCanvas: pFallback || void 0,
195
- baseLevel: _chunkRW6NC4RBjs.BidiResolver.getBaseLevel(shapedText)
195
+ baseLevel: _chunk6I53LI3Zjs.BidiResolver.getBaseLevel(shapedText)
196
196
  };
197
197
  if (this.paragraphCache.size > 1e3) this.paragraphCache.clear();
198
198
  this.paragraphCache.set(key, prepared);
@@ -264,8 +264,8 @@ var LayoutEngine = (_class = class {
264
264
  offset += paragraph.length + 1;
265
265
  continue;
266
266
  }
267
- const { shapedText, indexMap } = _chunkRW6NC4RBjs.ArabicShaper.shapeArabic(paragraph);
268
- const levels = _chunkRW6NC4RBjs.BidiResolver.resolveLevels(shapedText);
267
+ const { shapedText, indexMap } = _chunk6I53LI3Zjs.ArabicShaper.shapeArabic(paragraph);
268
+ const levels = _chunk6I53LI3Zjs.BidiResolver.resolveLevels(shapedText);
269
269
  const words = [];
270
270
  let shapedCharIdx = 0;
271
271
  let pFallback = false;
@@ -317,7 +317,7 @@ var LayoutEngine = (_class = class {
317
317
  words,
318
318
  isEmpty: false,
319
319
  fallbackToCanvas: pFallback || void 0,
320
- baseLevel: _chunkRW6NC4RBjs.BidiResolver.getBaseLevel(shapedText)
320
+ baseLevel: _chunk6I53LI3Zjs.BidiResolver.getBaseLevel(shapedText)
321
321
  };
322
322
  if (this.richParagraphCache.size > 1e3) this.richParagraphCache.clear();
323
323
  this.richParagraphCache.set(key, prepared);
@@ -372,7 +372,7 @@ var LayoutEngine = (_class = class {
372
372
  }
373
373
  for (const run of runs) {
374
374
  const runStartX = run[0].x;
375
- _chunkRW6NC4RBjs.BidiResolver.reorderVisual(run, paragraphBaseLevel);
375
+ _chunk6I53LI3Zjs.BidiResolver.reorderVisual(run, paragraphBaseLevel);
376
376
  let x = runStartX;
377
377
  for (const node of run) {
378
378
  node.x = x;
@@ -301,24 +301,57 @@ var WORKER_SOURCE_STRING = '"use strict";(()=>{var b=new Map;self.onmessage=x=>{
301
301
  // src/layout/LayoutWorkerManager.ts
302
302
  var LayoutWorkerManager = class _LayoutWorkerManager {
303
303
  static instance;
304
- worker;
304
+ worker = null;
305
305
  registeredFonts = /* @__PURE__ */ new Set();
306
306
  pendingCallbacks = /* @__PURE__ */ new Map();
307
307
  seqIdCounter = /* @__PURE__ */ new Map();
308
308
  debounceTimers = /* @__PURE__ */ new Map();
309
309
  constructor() {
310
+ this.worker = this.createWorker();
311
+ }
312
+ createWorker() {
310
313
  const workerBlob = new Blob([WORKER_SOURCE_STRING], { type: "application/javascript" });
311
314
  const workerURL = URL.createObjectURL(workerBlob);
312
- this.worker = new Worker(workerURL);
313
- setTimeout(() => URL.revokeObjectURL(workerURL), 2e3);
314
- this.worker.onmessage = (e) => {
315
+ let worker;
316
+ try {
317
+ worker = new Worker(workerURL);
318
+ } finally {
319
+ URL.revokeObjectURL(workerURL);
320
+ }
321
+ worker.onmessage = (e) => {
322
+ if (this.worker !== worker) return;
315
323
  const response = e.data;
316
- const callback = this.pendingCallbacks.get(`${response.id}-${response.seqId}`);
324
+ const key = `${response.id}-${response.seqId}`;
325
+ const callback = this.pendingCallbacks.get(key);
317
326
  if (callback) {
327
+ this.pendingCallbacks.delete(key);
318
328
  callback(response);
319
- this.pendingCallbacks.delete(`${response.id}-${response.seqId}`);
320
329
  }
321
330
  };
331
+ worker.onerror = () => this.handleWorkerFailure(worker);
332
+ worker.onmessageerror = () => this.handleWorkerFailure(worker);
333
+ return worker;
334
+ }
335
+ ensureWorker() {
336
+ if (!this.worker) this.worker = this.createWorker();
337
+ return this.worker;
338
+ }
339
+ handleWorkerFailure(worker) {
340
+ if (this.worker !== worker) return;
341
+ worker.terminate();
342
+ this.worker = null;
343
+ this.pendingCallbacks.clear();
344
+ this.registeredFonts.clear();
345
+ }
346
+ destroy() {
347
+ for (const timer of this.debounceTimers.values()) clearTimeout(timer);
348
+ this.debounceTimers.clear();
349
+ this.pendingCallbacks.clear();
350
+ this.seqIdCounter.clear();
351
+ this.registeredFonts.clear();
352
+ this.worker?.terminate();
353
+ this.worker = null;
354
+ if (_LayoutWorkerManager.instance === this) _LayoutWorkerManager.instance = void 0;
322
355
  }
323
356
  static getInstance() {
324
357
  if (!_LayoutWorkerManager.instance) {
@@ -332,6 +365,7 @@ var LayoutWorkerManager = class _LayoutWorkerManager {
332
365
  clearTimeout(existingTimer);
333
366
  }
334
367
  const runLayout = () => {
368
+ this.debounceTimers.delete(entityId);
335
369
  const nextSeqId = (this.seqIdCounter.get(entityId) ?? 0) + 1;
336
370
  this.seqIdCounter.set(entityId, nextSeqId);
337
371
  const request = {
@@ -350,7 +384,12 @@ var LayoutWorkerManager = class _LayoutWorkerManager {
350
384
  this.registeredFonts.add(options.fontId);
351
385
  }
352
386
  this.pendingCallbacks.set(`${entityId}-${nextSeqId}`, options.callback);
353
- this.worker.postMessage(request);
387
+ try {
388
+ this.ensureWorker().postMessage(request);
389
+ } catch {
390
+ const worker = this.worker;
391
+ if (worker) this.handleWorkerFailure(worker);
392
+ }
354
393
  };
355
394
  if (!this.seqIdCounter.has(entityId)) {
356
395
  runLayout();
@@ -365,6 +404,9 @@ var LayoutWorkerManager = class _LayoutWorkerManager {
365
404
  clearTimeout(existingTimer);
366
405
  this.debounceTimers.delete(entityId);
367
406
  }
407
+ for (const key of this.pendingCallbacks.keys()) {
408
+ if (key.startsWith(`${entityId}-`)) this.pendingCallbacks.delete(key);
409
+ }
368
410
  this.seqIdCounter.delete(entityId);
369
411
  }
370
412
  };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './renderer/SVGRenderer';
4
4
  export * from './renderer/WebGLPointRenderer';
5
5
  export * from './renderer/WebGPUParticleSystemManager';
6
6
  export * from './renderer/colorParse';
7
+ export * from './renderer/url';
7
8
  export * from './tree/Entity';
8
9
  export * from './tree/Scene';
9
10
  export * from './components/TextEntity';