@emasoft/svg-matrix 1.0.27 → 1.0.29

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.
Files changed (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +994 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +744 -184
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +404 -0
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +48 -19
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16411 -3298
  34. package/src/svg2-polyfills.js +114 -245
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
@@ -15,14 +15,12 @@
15
15
  * @module marker-resolver
16
16
  */
17
17
 
18
- import Decimal from 'decimal.js';
19
- import { Matrix } from './matrix.js';
20
- import * as Transforms2D from './transforms2d.js';
18
+ import Decimal from "decimal.js";
19
+ import { Matrix } from "./matrix.js";
20
+ import * as Transforms2D from "./transforms2d.js";
21
21
 
22
22
  Decimal.set({ precision: 80 });
23
23
 
24
- const D = x => (x instanceof Decimal ? x : new Decimal(x));
25
-
26
24
  /**
27
25
  * Parse an SVG marker element to extract all marker properties.
28
26
  *
@@ -71,34 +69,38 @@ const D = x => (x instanceof Decimal ? x : new Decimal(x));
71
69
  */
72
70
  export function parseMarkerElement(markerElement) {
73
71
  const data = {
74
- id: markerElement.getAttribute('id') || '',
75
- markerWidth: parseFloat(markerElement.getAttribute('markerWidth') || '3'),
76
- markerHeight: parseFloat(markerElement.getAttribute('markerHeight') || '3'),
77
- refX: parseFloat(markerElement.getAttribute('refX') || '0'),
78
- refY: parseFloat(markerElement.getAttribute('refY') || '0'),
79
- orient: markerElement.getAttribute('orient') || 'auto',
80
- markerUnits: markerElement.getAttribute('markerUnits') || 'strokeWidth',
72
+ id: markerElement.getAttribute("id") || "",
73
+ markerWidth: parseFloat(markerElement.getAttribute("markerWidth") || "3"),
74
+ markerHeight: parseFloat(markerElement.getAttribute("markerHeight") || "3"),
75
+ refX: parseFloat(markerElement.getAttribute("refX") || "0"),
76
+ refY: parseFloat(markerElement.getAttribute("refY") || "0"),
77
+ orient: markerElement.getAttribute("orient") || "auto",
78
+ markerUnits: markerElement.getAttribute("markerUnits") || "strokeWidth",
81
79
  viewBox: null,
82
- preserveAspectRatio: markerElement.getAttribute('preserveAspectRatio') || 'xMidYMid meet',
83
- children: []
80
+ preserveAspectRatio:
81
+ markerElement.getAttribute("preserveAspectRatio") || "xMidYMid meet",
82
+ children: [],
84
83
  };
85
84
 
86
85
  // Parse viewBox if present
87
- const viewBoxStr = markerElement.getAttribute('viewBox');
86
+ const viewBoxStr = markerElement.getAttribute("viewBox");
88
87
  if (viewBoxStr) {
89
- const parts = viewBoxStr.trim().split(/[\s,]+/).map(Number);
88
+ const parts = viewBoxStr
89
+ .trim()
90
+ .split(/[\s,]+/)
91
+ .map(Number);
90
92
  if (parts.length === 4) {
91
93
  data.viewBox = {
92
94
  x: parts[0],
93
95
  y: parts[1],
94
96
  width: parts[2],
95
- height: parts[3]
97
+ height: parts[3],
96
98
  };
97
99
  }
98
100
  }
99
101
 
100
102
  // Parse orient attribute
101
- if (data.orient !== 'auto' && data.orient !== 'auto-start-reverse') {
103
+ if (data.orient !== "auto" && data.orient !== "auto-start-reverse") {
102
104
  // Parse as angle in degrees
103
105
  const angle = parseFloat(data.orient);
104
106
  if (!isNaN(angle)) {
@@ -124,44 +126,44 @@ export function parseMarkerChild(element) {
124
126
  const tagName = element.tagName.toLowerCase();
125
127
  const data = {
126
128
  type: tagName,
127
- id: element.getAttribute('id') || null,
128
- transform: element.getAttribute('transform') || null,
129
- fill: element.getAttribute('fill') || null,
130
- stroke: element.getAttribute('stroke') || null,
131
- strokeWidth: element.getAttribute('stroke-width') || null,
132
- opacity: element.getAttribute('opacity') || null
129
+ id: element.getAttribute("id") || null,
130
+ transform: element.getAttribute("transform") || null,
131
+ fill: element.getAttribute("fill") || null,
132
+ stroke: element.getAttribute("stroke") || null,
133
+ strokeWidth: element.getAttribute("stroke-width") || null,
134
+ opacity: element.getAttribute("opacity") || null,
133
135
  };
134
136
 
135
137
  switch (tagName) {
136
- case 'path':
137
- data.d = element.getAttribute('d') || '';
138
+ case "path":
139
+ data.d = element.getAttribute("d") || "";
138
140
  break;
139
- case 'rect':
140
- data.x = parseFloat(element.getAttribute('x') || '0');
141
- data.y = parseFloat(element.getAttribute('y') || '0');
142
- data.width = parseFloat(element.getAttribute('width') || '0');
143
- data.height = parseFloat(element.getAttribute('height') || '0');
141
+ case "rect":
142
+ data.x = parseFloat(element.getAttribute("x") || "0");
143
+ data.y = parseFloat(element.getAttribute("y") || "0");
144
+ data.width = parseFloat(element.getAttribute("width") || "0");
145
+ data.height = parseFloat(element.getAttribute("height") || "0");
144
146
  break;
145
- case 'circle':
146
- data.cx = parseFloat(element.getAttribute('cx') || '0');
147
- data.cy = parseFloat(element.getAttribute('cy') || '0');
148
- data.r = parseFloat(element.getAttribute('r') || '0');
147
+ case "circle":
148
+ data.cx = parseFloat(element.getAttribute("cx") || "0");
149
+ data.cy = parseFloat(element.getAttribute("cy") || "0");
150
+ data.r = parseFloat(element.getAttribute("r") || "0");
149
151
  break;
150
- case 'ellipse':
151
- data.cx = parseFloat(element.getAttribute('cx') || '0');
152
- data.cy = parseFloat(element.getAttribute('cy') || '0');
153
- data.rx = parseFloat(element.getAttribute('rx') || '0');
154
- data.ry = parseFloat(element.getAttribute('ry') || '0');
152
+ case "ellipse":
153
+ data.cx = parseFloat(element.getAttribute("cx") || "0");
154
+ data.cy = parseFloat(element.getAttribute("cy") || "0");
155
+ data.rx = parseFloat(element.getAttribute("rx") || "0");
156
+ data.ry = parseFloat(element.getAttribute("ry") || "0");
155
157
  break;
156
- case 'line':
157
- data.x1 = parseFloat(element.getAttribute('x1') || '0');
158
- data.y1 = parseFloat(element.getAttribute('y1') || '0');
159
- data.x2 = parseFloat(element.getAttribute('x2') || '0');
160
- data.y2 = parseFloat(element.getAttribute('y2') || '0');
158
+ case "line":
159
+ data.x1 = parseFloat(element.getAttribute("x1") || "0");
160
+ data.y1 = parseFloat(element.getAttribute("y1") || "0");
161
+ data.x2 = parseFloat(element.getAttribute("x2") || "0");
162
+ data.y2 = parseFloat(element.getAttribute("y2") || "0");
161
163
  break;
162
- case 'polygon':
163
- case 'polyline':
164
- data.points = element.getAttribute('points') || '';
164
+ case "polygon":
165
+ case "polyline":
166
+ data.points = element.getAttribute("points") || "";
165
167
  break;
166
168
  default:
167
169
  // Store any additional attributes for unknown elements
@@ -202,8 +204,22 @@ export function parseMarkerChild(element) {
202
204
  * const strokeWidth = 2;
203
205
  * const transform = getMarkerTransform(markerDef, position, tangentAngle, strokeWidth, true);
204
206
  */
205
- export function getMarkerTransform(markerDef, position, tangentAngle, strokeWidth = 1, isStart = false) {
206
- const { markerWidth, markerHeight, refX, refY, orient, markerUnits, viewBox } = markerDef;
207
+ export function getMarkerTransform(
208
+ markerDef,
209
+ position,
210
+ tangentAngle,
211
+ strokeWidth = 1,
212
+ isStart = false,
213
+ ) {
214
+ const {
215
+ markerWidth,
216
+ markerHeight,
217
+ refX,
218
+ refY,
219
+ orient,
220
+ markerUnits,
221
+ viewBox,
222
+ } = markerDef;
207
223
 
208
224
  // Start with identity matrix
209
225
  let transform = Matrix.identity(3);
@@ -214,11 +230,11 @@ export function getMarkerTransform(markerDef, position, tangentAngle, strokeWidt
214
230
 
215
231
  // Step 2: Calculate rotation angle
216
232
  let rotationAngle = 0;
217
- if (orient === 'auto') {
233
+ if (orient === "auto") {
218
234
  rotationAngle = tangentAngle;
219
- } else if (orient === 'auto-start-reverse' && isStart) {
235
+ } else if (orient === "auto-start-reverse" && isStart) {
220
236
  rotationAngle = tangentAngle + Math.PI; // Add 180 degrees
221
- } else if (typeof orient === 'number') {
237
+ } else if (typeof orient === "number") {
222
238
  rotationAngle = orient * (Math.PI / 180); // Convert degrees to radians
223
239
  }
224
240
 
@@ -231,7 +247,7 @@ export function getMarkerTransform(markerDef, position, tangentAngle, strokeWidt
231
247
  // Step 3: Apply markerUnits scaling
232
248
  let scaleX = 1;
233
249
  let scaleY = 1;
234
- if (markerUnits === 'strokeWidth') {
250
+ if (markerUnits === "strokeWidth") {
235
251
  scaleX = strokeWidth;
236
252
  scaleY = strokeWidth;
237
253
  }
@@ -316,8 +332,8 @@ export function getPathVertices(pathData) {
316
332
  let currentY = 0;
317
333
  let startX = 0;
318
334
  let startY = 0;
319
- let lastControlX = 0;
320
- let lastControlY = 0;
335
+ let _lastControlX = 0;
336
+ let _lastControlY = 0;
321
337
 
322
338
  // Parse path commands
323
339
  const commands = parsePathCommands(pathData);
@@ -328,7 +344,7 @@ export function getPathVertices(pathData) {
328
344
  const prevY = currentY;
329
345
 
330
346
  switch (cmd.type) {
331
- case 'M': // moveto
347
+ case "M": // moveto
332
348
  currentX = cmd.x;
333
349
  currentY = cmd.y;
334
350
  startX = currentX;
@@ -347,11 +363,12 @@ export function getPathVertices(pathData) {
347
363
  y: currentY,
348
364
  tangentIn: 0,
349
365
  tangentOut: 0,
350
- index: vertices.length
366
+ index: vertices.length,
351
367
  });
352
368
  break;
353
369
 
354
- case 'L': // lineto
370
+ case "L": {
371
+ // lineto
355
372
  currentX = cmd.x;
356
373
  currentY = cmd.y;
357
374
 
@@ -369,13 +386,15 @@ export function getPathVertices(pathData) {
369
386
  y: currentY,
370
387
  tangentIn: lineAngle,
371
388
  tangentOut: lineAngle,
372
- index: vertices.length
389
+ index: vertices.length,
373
390
  });
374
391
  break;
392
+ }
375
393
 
376
- case 'C': // cubic bezier
377
- lastControlX = cmd.x2;
378
- lastControlY = cmd.y2;
394
+ case "C": {
395
+ // cubic bezier
396
+ _lastControlX = cmd.x2;
397
+ _lastControlY = cmd.y2;
379
398
  currentX = cmd.x;
380
399
  currentY = cmd.y;
381
400
 
@@ -396,13 +415,15 @@ export function getPathVertices(pathData) {
396
415
  y: currentY,
397
416
  tangentIn: endTangent,
398
417
  tangentOut: endTangent,
399
- index: vertices.length
418
+ index: vertices.length,
400
419
  });
401
420
  break;
421
+ }
402
422
 
403
- case 'Q': // quadratic bezier
404
- lastControlX = cmd.x1;
405
- lastControlY = cmd.y1;
423
+ case "Q": {
424
+ // quadratic bezier
425
+ _lastControlX = cmd.x1;
426
+ _lastControlY = cmd.y1;
406
427
  currentX = cmd.x;
407
428
  currentY = cmd.y;
408
429
 
@@ -423,11 +444,13 @@ export function getPathVertices(pathData) {
423
444
  y: currentY,
424
445
  tangentIn: qEndTangent,
425
446
  tangentOut: qEndTangent,
426
- index: vertices.length
447
+ index: vertices.length,
427
448
  });
428
449
  break;
450
+ }
429
451
 
430
- case 'A': // arc
452
+ case "A": {
453
+ // arc
431
454
  currentX = cmd.x;
432
455
  currentY = cmd.y;
433
456
 
@@ -445,11 +468,13 @@ export function getPathVertices(pathData) {
445
468
  y: currentY,
446
469
  tangentIn: arcAngle,
447
470
  tangentOut: arcAngle,
448
- index: vertices.length
471
+ index: vertices.length,
449
472
  });
450
473
  break;
474
+ }
451
475
 
452
- case 'Z': // closepath
476
+ case "Z": {
477
+ // closepath
453
478
  currentX = startX;
454
479
  currentY = startY;
455
480
 
@@ -466,6 +491,7 @@ export function getPathVertices(pathData) {
466
491
  vertices[0].tangentIn = closeAngle;
467
492
  }
468
493
  break;
494
+ }
469
495
  }
470
496
  }
471
497
 
@@ -483,11 +509,38 @@ export function parsePathCommands(pathData) {
483
509
 
484
510
  // Normalize path data: add spaces around command letters
485
511
  const normalized = pathData
486
- .replace(/([MmLlHhVvCcSsQqTtAaZz])/g, ' $1 ')
512
+ .replace(/([MmLlHhVvCcSsQqTtAaZz])/g, " $1 ")
487
513
  .trim();
488
514
 
489
- // Split into tokens
490
- const tokens = normalized.split(/[\s,]+/).filter(t => t.length > 0);
515
+ // FIX: Use regex to extract tokens (command letters and numbers)
516
+ // Handles implicit negative separators (e.g., "0.8-2.9" -> ["0.8", "-2.9"])
517
+ // Per W3C SVG spec, negative signs can act as delimiters without spaces
518
+ const commandRegex = /[MmLlHhVvCcSsQqTtAaZz]/;
519
+ const numRegex = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
520
+ const tokens = [];
521
+
522
+ let pos = 0;
523
+ while (pos < normalized.length) {
524
+ // Skip whitespace and commas
525
+ while (pos < normalized.length && /[\s,]/.test(normalized[pos])) pos++;
526
+ if (pos >= normalized.length) break;
527
+
528
+ // Check if it's a command letter
529
+ if (commandRegex.test(normalized[pos])) {
530
+ tokens.push(normalized[pos]);
531
+ pos++;
532
+ } else {
533
+ // Extract number
534
+ numRegex.lastIndex = pos;
535
+ const match = numRegex.exec(normalized);
536
+ if (match && match.index === pos) {
537
+ tokens.push(match[0]);
538
+ pos = numRegex.lastIndex;
539
+ } else {
540
+ pos++; // Skip invalid character
541
+ }
542
+ }
543
+ }
491
544
 
492
545
  let i = 0;
493
546
  let currentX = 0;
@@ -497,55 +550,64 @@ export function parsePathCommands(pathData) {
497
550
  const cmdType = tokens[i];
498
551
 
499
552
  switch (cmdType.toUpperCase()) {
500
- case 'M': // moveto
553
+ case "M": {
554
+ // moveto
501
555
  const mx = parseFloat(tokens[i + 1]);
502
556
  const my = parseFloat(tokens[i + 2]);
503
557
  commands.push({
504
- type: 'M',
505
- x: cmdType === 'M' ? mx : currentX + mx,
506
- y: cmdType === 'M' ? my : currentY + my
558
+ type: "M",
559
+ x: cmdType === "M" ? mx : currentX + mx,
560
+ y: cmdType === "M" ? my : currentY + my,
507
561
  });
508
562
  currentX = commands[commands.length - 1].x;
509
563
  currentY = commands[commands.length - 1].y;
510
564
  i += 3;
511
565
  break;
566
+ }
512
567
 
513
- case 'L': // lineto
568
+ case "L": {
569
+ // lineto
514
570
  const lx = parseFloat(tokens[i + 1]);
515
571
  const ly = parseFloat(tokens[i + 2]);
516
572
  commands.push({
517
- type: 'L',
518
- x: cmdType === 'L' ? lx : currentX + lx,
519
- y: cmdType === 'L' ? ly : currentY + ly
573
+ type: "L",
574
+ x: cmdType === "L" ? lx : currentX + lx,
575
+ y: cmdType === "L" ? ly : currentY + ly,
520
576
  });
521
577
  currentX = commands[commands.length - 1].x;
522
578
  currentY = commands[commands.length - 1].y;
523
579
  i += 3;
524
580
  break;
581
+ }
525
582
 
526
- case 'H': // horizontal lineto
583
+ case "H": {
584
+ // horizontal lineto
527
585
  const hx = parseFloat(tokens[i + 1]);
528
586
  commands.push({
529
- type: 'L',
530
- x: cmdType === 'H' ? hx : currentX + hx,
531
- y: currentY
587
+ type: "L",
588
+ x: cmdType === "H" ? hx : currentX + hx,
589
+ y: currentY,
532
590
  });
533
591
  currentX = commands[commands.length - 1].x;
534
592
  i += 2;
535
593
  break;
594
+ }
536
595
 
537
- case 'V': // vertical lineto
596
+ case "V": {
597
+ // vertical lineto
538
598
  const vy = parseFloat(tokens[i + 1]);
539
599
  commands.push({
540
- type: 'L',
600
+ type: "L",
541
601
  x: currentX,
542
- y: cmdType === 'V' ? vy : currentY + vy
602
+ y: cmdType === "V" ? vy : currentY + vy,
543
603
  });
544
604
  currentY = commands[commands.length - 1].y;
545
605
  i += 2;
546
606
  break;
607
+ }
547
608
 
548
- case 'C': // cubic bezier
609
+ case "C": {
610
+ // cubic bezier
549
611
  const c1x = parseFloat(tokens[i + 1]);
550
612
  const c1y = parseFloat(tokens[i + 2]);
551
613
  const c2x = parseFloat(tokens[i + 3]);
@@ -553,61 +615,66 @@ export function parsePathCommands(pathData) {
553
615
  const cx = parseFloat(tokens[i + 5]);
554
616
  const cy = parseFloat(tokens[i + 6]);
555
617
  commands.push({
556
- type: 'C',
557
- x1: cmdType === 'C' ? c1x : currentX + c1x,
558
- y1: cmdType === 'C' ? c1y : currentY + c1y,
559
- x2: cmdType === 'C' ? c2x : currentX + c2x,
560
- y2: cmdType === 'C' ? c2y : currentY + c2y,
561
- x: cmdType === 'C' ? cx : currentX + cx,
562
- y: cmdType === 'C' ? cy : currentY + cy
618
+ type: "C",
619
+ x1: cmdType === "C" ? c1x : currentX + c1x,
620
+ y1: cmdType === "C" ? c1y : currentY + c1y,
621
+ x2: cmdType === "C" ? c2x : currentX + c2x,
622
+ y2: cmdType === "C" ? c2y : currentY + c2y,
623
+ x: cmdType === "C" ? cx : currentX + cx,
624
+ y: cmdType === "C" ? cy : currentY + cy,
563
625
  });
564
626
  currentX = commands[commands.length - 1].x;
565
627
  currentY = commands[commands.length - 1].y;
566
628
  i += 7;
567
629
  break;
630
+ }
568
631
 
569
- case 'Q': // quadratic bezier
632
+ case "Q": {
633
+ // quadratic bezier
570
634
  const q1x = parseFloat(tokens[i + 1]);
571
635
  const q1y = parseFloat(tokens[i + 2]);
572
636
  const qx = parseFloat(tokens[i + 3]);
573
637
  const qy = parseFloat(tokens[i + 4]);
574
638
  commands.push({
575
- type: 'Q',
576
- x1: cmdType === 'Q' ? q1x : currentX + q1x,
577
- y1: cmdType === 'Q' ? q1y : currentY + q1y,
578
- x: cmdType === 'Q' ? qx : currentX + qx,
579
- y: cmdType === 'Q' ? qy : currentY + qy
639
+ type: "Q",
640
+ x1: cmdType === "Q" ? q1x : currentX + q1x,
641
+ y1: cmdType === "Q" ? q1y : currentY + q1y,
642
+ x: cmdType === "Q" ? qx : currentX + qx,
643
+ y: cmdType === "Q" ? qy : currentY + qy,
580
644
  });
581
645
  currentX = commands[commands.length - 1].x;
582
646
  currentY = commands[commands.length - 1].y;
583
647
  i += 5;
584
648
  break;
649
+ }
585
650
 
586
- case 'A': // arc
651
+ case "A": {
652
+ // arc
587
653
  const rx = parseFloat(tokens[i + 1]);
588
654
  const ry = parseFloat(tokens[i + 2]);
589
655
  const xAxisRotation = parseFloat(tokens[i + 3]);
590
- const largeArcFlag = parseInt(tokens[i + 4]);
591
- const sweepFlag = parseInt(tokens[i + 5]);
656
+ const largeArcFlag = parseInt(tokens[i + 4], 10);
657
+ const sweepFlag = parseInt(tokens[i + 5], 10);
592
658
  const ax = parseFloat(tokens[i + 6]);
593
659
  const ay = parseFloat(tokens[i + 7]);
594
660
  commands.push({
595
- type: 'A',
661
+ type: "A",
596
662
  rx,
597
663
  ry,
598
664
  xAxisRotation,
599
665
  largeArcFlag,
600
666
  sweepFlag,
601
- x: cmdType === 'A' ? ax : currentX + ax,
602
- y: cmdType === 'A' ? ay : currentY + ay
667
+ x: cmdType === "A" ? ax : currentX + ax,
668
+ y: cmdType === "A" ? ay : currentY + ay,
603
669
  });
604
670
  currentX = commands[commands.length - 1].x;
605
671
  currentY = commands[commands.length - 1].y;
606
672
  i += 8;
607
673
  break;
674
+ }
608
675
 
609
- case 'Z': // closepath
610
- commands.push({ type: 'Z' });
676
+ case "Z": // closepath
677
+ commands.push({ type: "Z" });
611
678
  i += 1;
612
679
  break;
613
680
 
@@ -654,15 +721,17 @@ export function resolveMarkers(pathElement, defsMap) {
654
721
  const instances = [];
655
722
 
656
723
  // Get marker references
657
- const markerStart = pathElement.getAttribute('marker-start');
658
- const markerMid = pathElement.getAttribute('marker-mid');
659
- const markerEnd = pathElement.getAttribute('marker-end');
724
+ const markerStart = pathElement.getAttribute("marker-start");
725
+ const markerMid = pathElement.getAttribute("marker-mid");
726
+ const markerEnd = pathElement.getAttribute("marker-end");
660
727
 
661
728
  // Get stroke width for scaling
662
- const strokeWidth = parseFloat(pathElement.getAttribute('stroke-width') || '1');
729
+ const strokeWidth = parseFloat(
730
+ pathElement.getAttribute("stroke-width") || "1",
731
+ );
663
732
 
664
733
  // Get path data and extract vertices
665
- const pathData = pathElement.getAttribute('d') || '';
734
+ const pathData = pathElement.getAttribute("d") || "";
666
735
  const vertices = getPathVertices(pathData);
667
736
 
668
737
  if (vertices.length === 0) {
@@ -683,14 +752,21 @@ export function resolveMarkers(pathElement, defsMap) {
683
752
 
684
753
  if (markerDef && vertices.length > 0) {
685
754
  const vertex = vertices[0];
686
- const tangent = vertices.length > 1 ? vertex.tangentOut : vertex.tangentIn;
755
+ const tangent =
756
+ vertices.length > 1 ? vertex.tangentOut : vertex.tangentIn;
687
757
 
688
758
  instances.push({
689
759
  markerDef,
690
760
  position: { x: vertex.x, y: vertex.y },
691
- transform: getMarkerTransform(markerDef, { x: vertex.x, y: vertex.y }, tangent, strokeWidth, true),
692
- type: 'start',
693
- vertex
761
+ transform: getMarkerTransform(
762
+ markerDef,
763
+ { x: vertex.x, y: vertex.y },
764
+ tangent,
765
+ strokeWidth,
766
+ true,
767
+ ),
768
+ type: "start",
769
+ vertex,
694
770
  });
695
771
  }
696
772
  }
@@ -709,9 +785,15 @@ export function resolveMarkers(pathElement, defsMap) {
709
785
  instances.push({
710
786
  markerDef,
711
787
  position: { x: vertex.x, y: vertex.y },
712
- transform: getMarkerTransform(markerDef, { x: vertex.x, y: vertex.y }, tangent, strokeWidth, false),
713
- type: 'mid',
714
- vertex
788
+ transform: getMarkerTransform(
789
+ markerDef,
790
+ { x: vertex.x, y: vertex.y },
791
+ tangent,
792
+ strokeWidth,
793
+ false,
794
+ ),
795
+ type: "mid",
796
+ vertex,
715
797
  });
716
798
  }
717
799
  }
@@ -729,9 +811,15 @@ export function resolveMarkers(pathElement, defsMap) {
729
811
  instances.push({
730
812
  markerDef,
731
813
  position: { x: vertex.x, y: vertex.y },
732
- transform: getMarkerTransform(markerDef, { x: vertex.x, y: vertex.y }, tangent, strokeWidth, false),
733
- type: 'end',
734
- vertex
814
+ transform: getMarkerTransform(
815
+ markerDef,
816
+ { x: vertex.x, y: vertex.y },
817
+ tangent,
818
+ strokeWidth,
819
+ false,
820
+ ),
821
+ type: "end",
822
+ vertex,
735
823
  });
736
824
  }
737
825
  }
@@ -771,52 +859,58 @@ export function markerToPolygons(markerInstance, options = {}) {
771
859
  let points = [];
772
860
 
773
861
  switch (child.type) {
774
- case 'path':
862
+ case "path":
775
863
  // Parse path and convert to points
776
864
  points = pathToPoints(child.d, curveSegments);
777
865
  break;
778
866
 
779
- case 'rect':
867
+ case "rect":
780
868
  // Convert rect to 4 corner points
781
869
  points = [
782
870
  { x: child.x, y: child.y },
783
871
  { x: child.x + child.width, y: child.y },
784
872
  { x: child.x + child.width, y: child.y + child.height },
785
- { x: child.x, y: child.y + child.height }
873
+ { x: child.x, y: child.y + child.height },
786
874
  ];
787
875
  break;
788
876
 
789
- case 'circle':
877
+ case "circle":
790
878
  // Approximate circle with polygon
791
879
  points = circleToPoints(child.cx, child.cy, child.r, curveSegments * 4);
792
880
  break;
793
881
 
794
- case 'ellipse':
882
+ case "ellipse":
795
883
  // Approximate ellipse with polygon
796
- points = ellipseToPoints(child.cx, child.cy, child.rx, child.ry, curveSegments * 4);
884
+ points = ellipseToPoints(
885
+ child.cx,
886
+ child.cy,
887
+ child.rx,
888
+ child.ry,
889
+ curveSegments * 4,
890
+ );
797
891
  break;
798
892
 
799
- case 'line':
893
+ case "line":
800
894
  points = [
801
895
  { x: child.x1, y: child.y1 },
802
- { x: child.x2, y: child.y2 }
896
+ { x: child.x2, y: child.y2 },
803
897
  ];
804
898
  break;
805
899
 
806
- case 'polygon':
807
- case 'polyline':
900
+ case "polygon":
901
+ case "polyline":
808
902
  points = parsePoints(child.points);
809
903
  break;
810
904
  }
811
905
 
812
906
  // Apply transform to all points
813
907
  if (points.length > 0) {
814
- const transformedPoints = points.map(p => {
908
+ const transformedPoints = points.map((p) => {
815
909
  const result = Transforms2D.applyTransform(transform, p.x, p.y);
816
910
  // result is an array [x, y] with Decimal values
817
911
  return {
818
912
  x: parseFloat(result[0].toFixed(precision)),
819
- y: parseFloat(result[1].toFixed(precision))
913
+ y: parseFloat(result[1].toFixed(precision)),
820
914
  };
821
915
  });
822
916
 
@@ -842,55 +936,53 @@ export function pathToPoints(pathData, segments = 10) {
842
936
 
843
937
  for (const cmd of commands) {
844
938
  switch (cmd.type) {
845
- case 'M':
939
+ case "M":
846
940
  currentX = cmd.x;
847
941
  currentY = cmd.y;
848
942
  points.push({ x: currentX, y: currentY });
849
943
  break;
850
944
 
851
- case 'L':
945
+ case "L":
852
946
  currentX = cmd.x;
853
947
  currentY = cmd.y;
854
948
  points.push({ x: currentX, y: currentY });
855
949
  break;
856
950
 
857
- case 'C':
951
+ case "C":
858
952
  // Approximate cubic bezier with line segments
859
953
  for (let i = 1; i <= segments; i++) {
860
954
  const t = i / segments;
861
955
  const t1 = 1 - t;
862
- const x = t1 * t1 * t1 * currentX +
863
- 3 * t1 * t1 * t * cmd.x1 +
864
- 3 * t1 * t * t * cmd.x2 +
865
- t * t * t * cmd.x;
866
- const y = t1 * t1 * t1 * currentY +
867
- 3 * t1 * t1 * t * cmd.y1 +
868
- 3 * t1 * t * t * cmd.y2 +
869
- t * t * t * cmd.y;
956
+ const x =
957
+ t1 * t1 * t1 * currentX +
958
+ 3 * t1 * t1 * t * cmd.x1 +
959
+ 3 * t1 * t * t * cmd.x2 +
960
+ t * t * t * cmd.x;
961
+ const y =
962
+ t1 * t1 * t1 * currentY +
963
+ 3 * t1 * t1 * t * cmd.y1 +
964
+ 3 * t1 * t * t * cmd.y2 +
965
+ t * t * t * cmd.y;
870
966
  points.push({ x, y });
871
967
  }
872
968
  currentX = cmd.x;
873
969
  currentY = cmd.y;
874
970
  break;
875
971
 
876
- case 'Q':
972
+ case "Q":
877
973
  // Approximate quadratic bezier with line segments
878
974
  for (let i = 1; i <= segments; i++) {
879
975
  const t = i / segments;
880
976
  const t1 = 1 - t;
881
- const x = t1 * t1 * currentX +
882
- 2 * t1 * t * cmd.x1 +
883
- t * t * cmd.x;
884
- const y = t1 * t1 * currentY +
885
- 2 * t1 * t * cmd.y1 +
886
- t * t * cmd.y;
977
+ const x = t1 * t1 * currentX + 2 * t1 * t * cmd.x1 + t * t * cmd.x;
978
+ const y = t1 * t1 * currentY + 2 * t1 * t * cmd.y1 + t * t * cmd.y;
887
979
  points.push({ x, y });
888
980
  }
889
981
  currentX = cmd.x;
890
982
  currentY = cmd.y;
891
983
  break;
892
984
 
893
- case 'A':
985
+ case "A":
894
986
  // Simplified arc approximation
895
987
  currentX = cmd.x;
896
988
  currentY = cmd.y;
@@ -917,7 +1009,7 @@ export function circleToPoints(cx, cy, r, segments = 32) {
917
1009
  const angle = (i / segments) * 2 * Math.PI;
918
1010
  points.push({
919
1011
  x: cx + r * Math.cos(angle),
920
- y: cy + r * Math.sin(angle)
1012
+ y: cy + r * Math.sin(angle),
921
1013
  });
922
1014
  }
923
1015
  return points;
@@ -939,7 +1031,7 @@ export function ellipseToPoints(cx, cy, rx, ry, segments = 32) {
939
1031
  const angle = (i / segments) * 2 * Math.PI;
940
1032
  points.push({
941
1033
  x: cx + rx * Math.cos(angle),
942
- y: cy + ry * Math.sin(angle)
1034
+ y: cy + ry * Math.sin(angle),
943
1035
  });
944
1036
  }
945
1037
  return points;
@@ -953,7 +1045,10 @@ export function ellipseToPoints(cx, cy, rx, ry, segments = 32) {
953
1045
  */
954
1046
  export function parsePoints(pointsStr) {
955
1047
  const points = [];
956
- const coords = pointsStr.trim().split(/[\s,]+/).map(Number);
1048
+ const coords = pointsStr
1049
+ .trim()
1050
+ .split(/[\s,]+/)
1051
+ .map(Number);
957
1052
 
958
1053
  for (let i = 0; i < coords.length - 1; i += 2) {
959
1054
  points.push({ x: coords[i], y: coords[i + 1] });
@@ -989,18 +1084,37 @@ export function markersToPathData(markerInstances, precision = 2) {
989
1084
 
990
1085
  // Start with M (moveto) command
991
1086
  const first = polygon[0];
992
- pathParts.push(`M ${first.x.toFixed(precision)} ${first.y.toFixed(precision)}`);
1087
+ pathParts.push(
1088
+ `M ${first.x.toFixed(precision)} ${first.y.toFixed(precision)}`,
1089
+ );
993
1090
 
994
1091
  // Add L (lineto) commands for remaining points
995
1092
  for (let i = 1; i < polygon.length; i++) {
996
1093
  const pt = polygon[i];
997
- pathParts.push(`L ${pt.x.toFixed(precision)} ${pt.y.toFixed(precision)}`);
1094
+ pathParts.push(
1095
+ `L ${pt.x.toFixed(precision)} ${pt.y.toFixed(precision)}`,
1096
+ );
998
1097
  }
999
1098
 
1000
1099
  // Close path
1001
- pathParts.push('Z');
1100
+ pathParts.push("Z");
1002
1101
  }
1003
1102
  }
1004
1103
 
1005
- return pathParts.join(' ');
1104
+ return pathParts.join(" ");
1006
1105
  }
1106
+
1107
+ export default {
1108
+ parseMarkerElement,
1109
+ parseMarkerChild,
1110
+ getMarkerTransform,
1111
+ getPathVertices,
1112
+ parsePathCommands,
1113
+ resolveMarkers,
1114
+ markerToPolygons,
1115
+ pathToPoints,
1116
+ circleToPoints,
1117
+ ellipseToPoints,
1118
+ parsePoints,
1119
+ markersToPathData,
1120
+ };