@emasoft/svg-matrix 1.0.30 → 1.0.31
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/bin/svg-matrix.js +310 -61
- package/bin/svglinter.cjs +102 -3
- package/bin/svgm.js +236 -27
- package/package.json +1 -1
- package/src/animation-optimization.js +137 -17
- package/src/animation-references.js +123 -6
- package/src/arc-length.js +213 -4
- package/src/bezier-analysis.js +217 -21
- package/src/bezier-intersections.js +275 -12
- package/src/browser-verify.js +237 -4
- package/src/clip-path-resolver.js +168 -0
- package/src/convert-path-data.js +479 -28
- package/src/css-specificity.js +73 -10
- package/src/douglas-peucker.js +219 -2
- package/src/flatten-pipeline.js +284 -26
- package/src/geometry-to-path.js +250 -25
- package/src/gjk-collision.js +236 -33
- package/src/index.js +261 -3
- package/src/inkscape-support.js +86 -28
- package/src/logger.js +48 -3
- package/src/marker-resolver.js +278 -74
- package/src/mask-resolver.js +265 -66
- package/src/matrix.js +44 -5
- package/src/mesh-gradient.js +352 -102
- package/src/off-canvas-detection.js +382 -13
- package/src/path-analysis.js +192 -18
- package/src/path-data-plugins.js +309 -5
- package/src/path-optimization.js +129 -5
- package/src/path-simplification.js +188 -32
- package/src/pattern-resolver.js +454 -106
- package/src/polygon-clip.js +324 -1
- package/src/svg-boolean-ops.js +226 -9
- package/src/svg-collections.js +7 -5
- package/src/svg-flatten.js +386 -62
- package/src/svg-parser.js +179 -8
- package/src/svg-rendering-context.js +235 -6
- package/src/svg-toolbox.js +45 -8
- package/src/svg2-polyfills.js +40 -10
- package/src/transform-decomposition.js +258 -32
- package/src/transform-optimization.js +259 -13
- package/src/transforms2d.js +82 -9
- package/src/transforms3d.js +62 -10
- package/src/use-symbol-resolver.js +286 -42
- package/src/vector.js +64 -8
- package/src/verification.js +392 -1
package/src/marker-resolver.js
CHANGED
|
@@ -68,12 +68,34 @@ Decimal.set({ precision: 80 });
|
|
|
68
68
|
* // }
|
|
69
69
|
*/
|
|
70
70
|
export function parseMarkerElement(markerElement) {
|
|
71
|
+
// Validate required parameter
|
|
72
|
+
if (!markerElement) throw new Error('parseMarkerElement: markerElement is required');
|
|
73
|
+
if (typeof markerElement.getAttribute !== 'function') {
|
|
74
|
+
throw new Error('parseMarkerElement: markerElement must be a DOM element');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Parse numeric attributes with validation - NaN check ensures valid numbers
|
|
78
|
+
const markerWidth = parseFloat(markerElement.getAttribute("markerWidth") || "3");
|
|
79
|
+
const markerHeight = parseFloat(markerElement.getAttribute("markerHeight") || "3");
|
|
80
|
+
const refX = parseFloat(markerElement.getAttribute("refX") || "0");
|
|
81
|
+
const refY = parseFloat(markerElement.getAttribute("refY") || "0");
|
|
82
|
+
|
|
83
|
+
// Validate numeric values to prevent NaN propagation
|
|
84
|
+
if (isNaN(markerWidth) || markerWidth <= 0) {
|
|
85
|
+
throw new Error('parseMarkerElement: markerWidth must be a positive number');
|
|
86
|
+
}
|
|
87
|
+
if (isNaN(markerHeight) || markerHeight <= 0) {
|
|
88
|
+
throw new Error('parseMarkerElement: markerHeight must be a positive number');
|
|
89
|
+
}
|
|
90
|
+
if (isNaN(refX)) throw new Error('parseMarkerElement: refX must be a valid number');
|
|
91
|
+
if (isNaN(refY)) throw new Error('parseMarkerElement: refY must be a valid number');
|
|
92
|
+
|
|
71
93
|
const data = {
|
|
72
94
|
id: markerElement.getAttribute("id") || "",
|
|
73
|
-
markerWidth
|
|
74
|
-
markerHeight
|
|
75
|
-
refX
|
|
76
|
-
refY
|
|
95
|
+
markerWidth,
|
|
96
|
+
markerHeight,
|
|
97
|
+
refX,
|
|
98
|
+
refY,
|
|
77
99
|
orient: markerElement.getAttribute("orient") || "auto",
|
|
78
100
|
markerUnits: markerElement.getAttribute("markerUnits") || "strokeWidth",
|
|
79
101
|
viewBox: null,
|
|
@@ -89,7 +111,8 @@ export function parseMarkerElement(markerElement) {
|
|
|
89
111
|
.trim()
|
|
90
112
|
.split(/[\s,]+/)
|
|
91
113
|
.map(Number);
|
|
92
|
-
|
|
114
|
+
// Validate viewBox has exactly 4 valid finite numbers
|
|
115
|
+
if (parts.length === 4 && parts.every(n => !isNaN(n) && isFinite(n))) {
|
|
93
116
|
data.viewBox = {
|
|
94
117
|
x: parts[0],
|
|
95
118
|
y: parts[1],
|
|
@@ -103,14 +126,25 @@ export function parseMarkerElement(markerElement) {
|
|
|
103
126
|
if (data.orient !== "auto" && data.orient !== "auto-start-reverse") {
|
|
104
127
|
// Parse as angle in degrees
|
|
105
128
|
const angle = parseFloat(data.orient);
|
|
106
|
-
|
|
129
|
+
// Validate angle is a finite number
|
|
130
|
+
if (!isNaN(angle) && isFinite(angle)) {
|
|
107
131
|
data.orient = angle;
|
|
132
|
+
} else {
|
|
133
|
+
// Invalid orient value defaults to auto
|
|
134
|
+
data.orient = "auto";
|
|
108
135
|
}
|
|
109
136
|
}
|
|
110
137
|
|
|
111
|
-
// Parse child elements
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
// Parse child elements with error handling
|
|
139
|
+
if (markerElement.children) {
|
|
140
|
+
for (const child of markerElement.children) {
|
|
141
|
+
try {
|
|
142
|
+
data.children.push(parseMarkerChild(child));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Skip invalid children but continue processing
|
|
145
|
+
console.warn(`Failed to parse marker child: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
114
148
|
}
|
|
115
149
|
|
|
116
150
|
return data;
|
|
@@ -123,6 +157,10 @@ export function parseMarkerElement(markerElement) {
|
|
|
123
157
|
* @returns {Object} Parsed element data
|
|
124
158
|
*/
|
|
125
159
|
export function parseMarkerChild(element) {
|
|
160
|
+
// Validate required parameter
|
|
161
|
+
if (!element) throw new Error('parseMarkerChild: element is required');
|
|
162
|
+
if (!element.tagName) throw new Error('parseMarkerChild: element must have a tagName property');
|
|
163
|
+
|
|
126
164
|
const tagName = element.tagName.toLowerCase();
|
|
127
165
|
const data = {
|
|
128
166
|
type: tagName,
|
|
@@ -134,32 +172,45 @@ export function parseMarkerChild(element) {
|
|
|
134
172
|
opacity: element.getAttribute("opacity") || null,
|
|
135
173
|
};
|
|
136
174
|
|
|
175
|
+
// Helper to parse and validate float attributes
|
|
176
|
+
const parseValidFloat = (value, defaultVal, name) => {
|
|
177
|
+
const parsed = parseFloat(value || String(defaultVal));
|
|
178
|
+
if (isNaN(parsed) || !isFinite(parsed)) {
|
|
179
|
+
throw new Error(`parseMarkerChild: ${name} must be a valid finite number`);
|
|
180
|
+
}
|
|
181
|
+
return parsed;
|
|
182
|
+
};
|
|
183
|
+
|
|
137
184
|
switch (tagName) {
|
|
138
185
|
case "path":
|
|
139
186
|
data.d = element.getAttribute("d") || "";
|
|
140
187
|
break;
|
|
141
188
|
case "rect":
|
|
142
|
-
|
|
143
|
-
data.
|
|
144
|
-
data.
|
|
145
|
-
data.
|
|
189
|
+
// Validate all rect attributes are valid numbers
|
|
190
|
+
data.x = parseValidFloat(element.getAttribute("x"), 0, "x");
|
|
191
|
+
data.y = parseValidFloat(element.getAttribute("y"), 0, "y");
|
|
192
|
+
data.width = parseValidFloat(element.getAttribute("width"), 0, "width");
|
|
193
|
+
data.height = parseValidFloat(element.getAttribute("height"), 0, "height");
|
|
146
194
|
break;
|
|
147
195
|
case "circle":
|
|
148
|
-
|
|
149
|
-
data.
|
|
150
|
-
data.
|
|
196
|
+
// Validate all circle attributes are valid numbers
|
|
197
|
+
data.cx = parseValidFloat(element.getAttribute("cx"), 0, "cx");
|
|
198
|
+
data.cy = parseValidFloat(element.getAttribute("cy"), 0, "cy");
|
|
199
|
+
data.r = parseValidFloat(element.getAttribute("r"), 0, "r");
|
|
151
200
|
break;
|
|
152
201
|
case "ellipse":
|
|
153
|
-
|
|
154
|
-
data.
|
|
155
|
-
data.
|
|
156
|
-
data.
|
|
202
|
+
// Validate all ellipse attributes are valid numbers
|
|
203
|
+
data.cx = parseValidFloat(element.getAttribute("cx"), 0, "cx");
|
|
204
|
+
data.cy = parseValidFloat(element.getAttribute("cy"), 0, "cy");
|
|
205
|
+
data.rx = parseValidFloat(element.getAttribute("rx"), 0, "rx");
|
|
206
|
+
data.ry = parseValidFloat(element.getAttribute("ry"), 0, "ry");
|
|
157
207
|
break;
|
|
158
208
|
case "line":
|
|
159
|
-
|
|
160
|
-
data.
|
|
161
|
-
data.
|
|
162
|
-
data.
|
|
209
|
+
// Validate all line attributes are valid numbers
|
|
210
|
+
data.x1 = parseValidFloat(element.getAttribute("x1"), 0, "x1");
|
|
211
|
+
data.y1 = parseValidFloat(element.getAttribute("y1"), 0, "y1");
|
|
212
|
+
data.x2 = parseValidFloat(element.getAttribute("x2"), 0, "x2");
|
|
213
|
+
data.y2 = parseValidFloat(element.getAttribute("y2"), 0, "y2");
|
|
163
214
|
break;
|
|
164
215
|
case "polygon":
|
|
165
216
|
case "polyline":
|
|
@@ -168,8 +219,10 @@ export function parseMarkerChild(element) {
|
|
|
168
219
|
default:
|
|
169
220
|
// Store any additional attributes for unknown elements
|
|
170
221
|
data.attributes = {};
|
|
171
|
-
|
|
172
|
-
|
|
222
|
+
if (element.attributes) {
|
|
223
|
+
for (const attr of element.attributes) {
|
|
224
|
+
data.attributes[attr.name] = attr.value;
|
|
225
|
+
}
|
|
173
226
|
}
|
|
174
227
|
}
|
|
175
228
|
|
|
@@ -211,6 +264,19 @@ export function getMarkerTransform(
|
|
|
211
264
|
strokeWidth = 1,
|
|
212
265
|
isStart = false,
|
|
213
266
|
) {
|
|
267
|
+
// Validate required parameters
|
|
268
|
+
if (!markerDef) throw new Error('getMarkerTransform: markerDef is required');
|
|
269
|
+
if (!position || typeof position.x !== 'number' || typeof position.y !== 'number') {
|
|
270
|
+
throw new Error('getMarkerTransform: position must be an object with x and y numeric properties');
|
|
271
|
+
}
|
|
272
|
+
if (typeof tangentAngle !== 'number' || isNaN(tangentAngle) || !isFinite(tangentAngle)) {
|
|
273
|
+
throw new Error('getMarkerTransform: tangentAngle must be a valid finite number');
|
|
274
|
+
}
|
|
275
|
+
if (typeof strokeWidth !== 'number' || isNaN(strokeWidth) || strokeWidth <= 0) {
|
|
276
|
+
throw new Error('getMarkerTransform: strokeWidth must be a positive number');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Extract markerDef properties with validation
|
|
214
280
|
const {
|
|
215
281
|
markerWidth,
|
|
216
282
|
markerHeight,
|
|
@@ -221,6 +287,20 @@ export function getMarkerTransform(
|
|
|
221
287
|
viewBox,
|
|
222
288
|
} = markerDef;
|
|
223
289
|
|
|
290
|
+
// Validate markerDef has required numeric properties
|
|
291
|
+
if (typeof markerWidth !== 'number' || markerWidth <= 0) {
|
|
292
|
+
throw new Error('getMarkerTransform: markerDef.markerWidth must be a positive number');
|
|
293
|
+
}
|
|
294
|
+
if (typeof markerHeight !== 'number' || markerHeight <= 0) {
|
|
295
|
+
throw new Error('getMarkerTransform: markerDef.markerHeight must be a positive number');
|
|
296
|
+
}
|
|
297
|
+
if (typeof refX !== 'number' || !isFinite(refX)) {
|
|
298
|
+
throw new Error('getMarkerTransform: markerDef.refX must be a finite number');
|
|
299
|
+
}
|
|
300
|
+
if (typeof refY !== 'number' || !isFinite(refY)) {
|
|
301
|
+
throw new Error('getMarkerTransform: markerDef.refY must be a finite number');
|
|
302
|
+
}
|
|
303
|
+
|
|
224
304
|
// Start with identity matrix
|
|
225
305
|
let transform = Matrix.identity(3);
|
|
226
306
|
|
|
@@ -258,20 +338,24 @@ export function getMarkerTransform(
|
|
|
258
338
|
const vbWidth = viewBox.width;
|
|
259
339
|
const vbHeight = viewBox.height;
|
|
260
340
|
|
|
261
|
-
|
|
341
|
+
// Prevent division by zero
|
|
342
|
+
if (vbWidth > 0 && vbHeight > 0 && isFinite(vbWidth) && isFinite(vbHeight)) {
|
|
262
343
|
// Calculate uniform scale factor based on preserveAspectRatio
|
|
263
344
|
const scaleFactorX = markerWidth / vbWidth;
|
|
264
345
|
const scaleFactorY = markerHeight / vbHeight;
|
|
265
346
|
|
|
266
|
-
//
|
|
267
|
-
|
|
347
|
+
// Validate scale factors are finite
|
|
348
|
+
if (isFinite(scaleFactorX) && isFinite(scaleFactorY)) {
|
|
349
|
+
// For now, use uniform scaling (can be enhanced with full preserveAspectRatio parsing)
|
|
350
|
+
const scaleFactor = Math.min(scaleFactorX, scaleFactorY);
|
|
268
351
|
|
|
269
|
-
|
|
270
|
-
|
|
352
|
+
scaleX *= scaleFactor;
|
|
353
|
+
scaleY *= scaleFactor;
|
|
271
354
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
355
|
+
// Translate to account for viewBox origin
|
|
356
|
+
const viewBoxTranslate = Transforms2D.translation(-viewBox.x, -viewBox.y);
|
|
357
|
+
transform = transform.mul(viewBoxTranslate);
|
|
358
|
+
}
|
|
275
359
|
}
|
|
276
360
|
}
|
|
277
361
|
|
|
@@ -327,13 +411,16 @@ export function getMarkerTransform(
|
|
|
327
411
|
* // ]
|
|
328
412
|
*/
|
|
329
413
|
export function getPathVertices(pathData) {
|
|
414
|
+
// Validate pathData parameter
|
|
415
|
+
if (typeof pathData !== 'string') {
|
|
416
|
+
throw new Error('getPathVertices: pathData must be a string');
|
|
417
|
+
}
|
|
418
|
+
|
|
330
419
|
const vertices = [];
|
|
331
420
|
let currentX = 0;
|
|
332
421
|
let currentY = 0;
|
|
333
422
|
let startX = 0;
|
|
334
423
|
let startY = 0;
|
|
335
|
-
let _lastControlX = 0;
|
|
336
|
-
let _lastControlY = 0;
|
|
337
424
|
|
|
338
425
|
// Parse path commands
|
|
339
426
|
const commands = parsePathCommands(pathData);
|
|
@@ -393,8 +480,6 @@ export function getPathVertices(pathData) {
|
|
|
393
480
|
|
|
394
481
|
case "C": {
|
|
395
482
|
// cubic bezier
|
|
396
|
-
_lastControlX = cmd.x2;
|
|
397
|
-
_lastControlY = cmd.y2;
|
|
398
483
|
currentX = cmd.x;
|
|
399
484
|
currentY = cmd.y;
|
|
400
485
|
|
|
@@ -422,8 +507,6 @@ export function getPathVertices(pathData) {
|
|
|
422
507
|
|
|
423
508
|
case "Q": {
|
|
424
509
|
// quadratic bezier
|
|
425
|
-
_lastControlX = cmd.x1;
|
|
426
|
-
_lastControlY = cmd.y1;
|
|
427
510
|
currentX = cmd.x;
|
|
428
511
|
currentY = cmd.y;
|
|
429
512
|
|
|
@@ -492,6 +575,8 @@ export function getPathVertices(pathData) {
|
|
|
492
575
|
}
|
|
493
576
|
break;
|
|
494
577
|
}
|
|
578
|
+
default:
|
|
579
|
+
break;
|
|
495
580
|
}
|
|
496
581
|
}
|
|
497
582
|
|
|
@@ -505,8 +590,18 @@ export function getPathVertices(pathData) {
|
|
|
505
590
|
* @returns {Array<Object>} Array of command objects
|
|
506
591
|
*/
|
|
507
592
|
export function parsePathCommands(pathData) {
|
|
593
|
+
// Validate pathData parameter
|
|
594
|
+
if (typeof pathData !== 'string') {
|
|
595
|
+
throw new Error('parsePathCommands: pathData must be a string');
|
|
596
|
+
}
|
|
597
|
+
|
|
508
598
|
const commands = [];
|
|
509
599
|
|
|
600
|
+
// Handle empty path data
|
|
601
|
+
if (pathData.trim() === '') {
|
|
602
|
+
return commands;
|
|
603
|
+
}
|
|
604
|
+
|
|
510
605
|
// Normalize path data: add spaces around command letters
|
|
511
606
|
const normalized = pathData
|
|
512
607
|
.replace(/([MmLlHhVvCcSsQqTtAaZz])/g, " $1 ")
|
|
@@ -546,14 +641,26 @@ export function parsePathCommands(pathData) {
|
|
|
546
641
|
let currentX = 0;
|
|
547
642
|
let currentY = 0;
|
|
548
643
|
|
|
644
|
+
// Helper to safely parse token with bounds checking
|
|
645
|
+
const parseToken = (index, name) => {
|
|
646
|
+
if (index >= tokens.length) {
|
|
647
|
+
throw new Error(`parsePathCommands: missing ${name} parameter at token ${index}`);
|
|
648
|
+
}
|
|
649
|
+
const value = parseFloat(tokens[index]);
|
|
650
|
+
if (isNaN(value) || !isFinite(value)) {
|
|
651
|
+
throw new Error(`parsePathCommands: invalid ${name} value "${tokens[index]}" at token ${index}`);
|
|
652
|
+
}
|
|
653
|
+
return value;
|
|
654
|
+
};
|
|
655
|
+
|
|
549
656
|
while (i < tokens.length) {
|
|
550
657
|
const cmdType = tokens[i];
|
|
551
658
|
|
|
552
659
|
switch (cmdType.toUpperCase()) {
|
|
553
660
|
case "M": {
|
|
554
|
-
// moveto
|
|
555
|
-
const mx =
|
|
556
|
-
const my =
|
|
661
|
+
// moveto - requires 2 parameters (x, y)
|
|
662
|
+
const mx = parseToken(i + 1, 'M.x');
|
|
663
|
+
const my = parseToken(i + 2, 'M.y');
|
|
557
664
|
commands.push({
|
|
558
665
|
type: "M",
|
|
559
666
|
x: cmdType === "M" ? mx : currentX + mx,
|
|
@@ -566,9 +673,9 @@ export function parsePathCommands(pathData) {
|
|
|
566
673
|
}
|
|
567
674
|
|
|
568
675
|
case "L": {
|
|
569
|
-
// lineto
|
|
570
|
-
const lx =
|
|
571
|
-
const ly =
|
|
676
|
+
// lineto - requires 2 parameters (x, y)
|
|
677
|
+
const lx = parseToken(i + 1, 'L.x');
|
|
678
|
+
const ly = parseToken(i + 2, 'L.y');
|
|
572
679
|
commands.push({
|
|
573
680
|
type: "L",
|
|
574
681
|
x: cmdType === "L" ? lx : currentX + lx,
|
|
@@ -581,8 +688,8 @@ export function parsePathCommands(pathData) {
|
|
|
581
688
|
}
|
|
582
689
|
|
|
583
690
|
case "H": {
|
|
584
|
-
// horizontal lineto
|
|
585
|
-
const hx =
|
|
691
|
+
// horizontal lineto - requires 1 parameter (x)
|
|
692
|
+
const hx = parseToken(i + 1, 'H.x');
|
|
586
693
|
commands.push({
|
|
587
694
|
type: "L",
|
|
588
695
|
x: cmdType === "H" ? hx : currentX + hx,
|
|
@@ -594,8 +701,8 @@ export function parsePathCommands(pathData) {
|
|
|
594
701
|
}
|
|
595
702
|
|
|
596
703
|
case "V": {
|
|
597
|
-
// vertical lineto
|
|
598
|
-
const vy =
|
|
704
|
+
// vertical lineto - requires 1 parameter (y)
|
|
705
|
+
const vy = parseToken(i + 1, 'V.y');
|
|
599
706
|
commands.push({
|
|
600
707
|
type: "L",
|
|
601
708
|
x: currentX,
|
|
@@ -607,13 +714,13 @@ export function parsePathCommands(pathData) {
|
|
|
607
714
|
}
|
|
608
715
|
|
|
609
716
|
case "C": {
|
|
610
|
-
// cubic bezier
|
|
611
|
-
const c1x =
|
|
612
|
-
const c1y =
|
|
613
|
-
const c2x =
|
|
614
|
-
const c2y =
|
|
615
|
-
const cx =
|
|
616
|
-
const cy =
|
|
717
|
+
// cubic bezier - requires 6 parameters (x1, y1, x2, y2, x, y)
|
|
718
|
+
const c1x = parseToken(i + 1, 'C.x1');
|
|
719
|
+
const c1y = parseToken(i + 2, 'C.y1');
|
|
720
|
+
const c2x = parseToken(i + 3, 'C.x2');
|
|
721
|
+
const c2y = parseToken(i + 4, 'C.y2');
|
|
722
|
+
const cx = parseToken(i + 5, 'C.x');
|
|
723
|
+
const cy = parseToken(i + 6, 'C.y');
|
|
617
724
|
commands.push({
|
|
618
725
|
type: "C",
|
|
619
726
|
x1: cmdType === "C" ? c1x : currentX + c1x,
|
|
@@ -630,11 +737,11 @@ export function parsePathCommands(pathData) {
|
|
|
630
737
|
}
|
|
631
738
|
|
|
632
739
|
case "Q": {
|
|
633
|
-
// quadratic bezier
|
|
634
|
-
const q1x =
|
|
635
|
-
const q1y =
|
|
636
|
-
const qx =
|
|
637
|
-
const qy =
|
|
740
|
+
// quadratic bezier - requires 4 parameters (x1, y1, x, y)
|
|
741
|
+
const q1x = parseToken(i + 1, 'Q.x1');
|
|
742
|
+
const q1y = parseToken(i + 2, 'Q.y1');
|
|
743
|
+
const qx = parseToken(i + 3, 'Q.x');
|
|
744
|
+
const qy = parseToken(i + 4, 'Q.y');
|
|
638
745
|
commands.push({
|
|
639
746
|
type: "Q",
|
|
640
747
|
x1: cmdType === "Q" ? q1x : currentX + q1x,
|
|
@@ -649,14 +756,21 @@ export function parsePathCommands(pathData) {
|
|
|
649
756
|
}
|
|
650
757
|
|
|
651
758
|
case "A": {
|
|
652
|
-
// arc
|
|
653
|
-
const rx =
|
|
654
|
-
const ry =
|
|
655
|
-
const xAxisRotation =
|
|
759
|
+
// arc - requires 7 parameters (rx, ry, rotation, largeArc, sweep, x, y)
|
|
760
|
+
const rx = parseToken(i + 1, 'A.rx');
|
|
761
|
+
const ry = parseToken(i + 2, 'A.ry');
|
|
762
|
+
const xAxisRotation = parseToken(i + 3, 'A.rotation');
|
|
763
|
+
// Flags must be 0 or 1
|
|
764
|
+
if (i + 4 >= tokens.length || i + 5 >= tokens.length) {
|
|
765
|
+
throw new Error(`parsePathCommands: missing arc flag parameters`);
|
|
766
|
+
}
|
|
656
767
|
const largeArcFlag = parseInt(tokens[i + 4], 10);
|
|
657
768
|
const sweepFlag = parseInt(tokens[i + 5], 10);
|
|
658
|
-
|
|
659
|
-
|
|
769
|
+
if (isNaN(largeArcFlag) || isNaN(sweepFlag)) {
|
|
770
|
+
throw new Error(`parsePathCommands: invalid arc flag values`);
|
|
771
|
+
}
|
|
772
|
+
const ax = parseToken(i + 6, 'A.x');
|
|
773
|
+
const ay = parseToken(i + 7, 'A.y');
|
|
660
774
|
commands.push({
|
|
661
775
|
type: "A",
|
|
662
776
|
rx,
|
|
@@ -718,6 +832,12 @@ export function parsePathCommands(pathData) {
|
|
|
718
832
|
* // Returns array of marker instances ready to be rendered
|
|
719
833
|
*/
|
|
720
834
|
export function resolveMarkers(pathElement, defsMap) {
|
|
835
|
+
// Validate required parameters
|
|
836
|
+
if (!pathElement) throw new Error('resolveMarkers: pathElement is required');
|
|
837
|
+
if (!defsMap || typeof defsMap !== 'object') {
|
|
838
|
+
throw new Error('resolveMarkers: defsMap must be an object');
|
|
839
|
+
}
|
|
840
|
+
|
|
721
841
|
const instances = [];
|
|
722
842
|
|
|
723
843
|
// Get marker references
|
|
@@ -725,10 +845,13 @@ export function resolveMarkers(pathElement, defsMap) {
|
|
|
725
845
|
const markerMid = pathElement.getAttribute("marker-mid");
|
|
726
846
|
const markerEnd = pathElement.getAttribute("marker-end");
|
|
727
847
|
|
|
728
|
-
// Get stroke width for scaling
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
848
|
+
// Get stroke width for scaling with validation
|
|
849
|
+
const strokeWidthStr = pathElement.getAttribute("stroke-width") || "1";
|
|
850
|
+
const strokeWidth = parseFloat(strokeWidthStr);
|
|
851
|
+
// Validate strokeWidth is a positive number
|
|
852
|
+
if (isNaN(strokeWidth) || strokeWidth <= 0) {
|
|
853
|
+
throw new Error('resolveMarkers: stroke-width must be a positive number');
|
|
854
|
+
}
|
|
732
855
|
|
|
733
856
|
// Get path data and extract vertices
|
|
734
857
|
const pathData = pathElement.getAttribute("d") || "";
|
|
@@ -850,10 +973,29 @@ export function resolveMarkers(pathElement, defsMap) {
|
|
|
850
973
|
* // Returns: [[{x: 100.123, y: 200.456}, ...], ...]
|
|
851
974
|
*/
|
|
852
975
|
export function markerToPolygons(markerInstance, options = {}) {
|
|
976
|
+
// Validate markerInstance parameter
|
|
977
|
+
if (!markerInstance) throw new Error('markerToPolygons: markerInstance is required');
|
|
978
|
+
if (!markerInstance.markerDef) throw new Error('markerToPolygons: markerInstance.markerDef is required');
|
|
979
|
+
if (!markerInstance.transform) throw new Error('markerToPolygons: markerInstance.transform is required');
|
|
980
|
+
|
|
853
981
|
const { precision = 2, curveSegments = 10 } = options;
|
|
982
|
+
|
|
983
|
+
// Validate options
|
|
984
|
+
if (typeof precision !== 'number' || precision < 0) {
|
|
985
|
+
throw new Error('markerToPolygons: precision must be a non-negative number');
|
|
986
|
+
}
|
|
987
|
+
if (typeof curveSegments !== 'number' || curveSegments <= 0) {
|
|
988
|
+
throw new Error('markerToPolygons: curveSegments must be a positive number');
|
|
989
|
+
}
|
|
990
|
+
|
|
854
991
|
const polygons = [];
|
|
855
992
|
const { markerDef, transform } = markerInstance;
|
|
856
993
|
|
|
994
|
+
// Validate markerDef has children array
|
|
995
|
+
if (!Array.isArray(markerDef.children)) {
|
|
996
|
+
throw new Error('markerToPolygons: markerDef.children must be an array');
|
|
997
|
+
}
|
|
998
|
+
|
|
857
999
|
// Process each child element
|
|
858
1000
|
for (const child of markerDef.children) {
|
|
859
1001
|
let points = [];
|
|
@@ -901,6 +1043,8 @@ export function markerToPolygons(markerInstance, options = {}) {
|
|
|
901
1043
|
case "polyline":
|
|
902
1044
|
points = parsePoints(child.points);
|
|
903
1045
|
break;
|
|
1046
|
+
default:
|
|
1047
|
+
break;
|
|
904
1048
|
}
|
|
905
1049
|
|
|
906
1050
|
// Apply transform to all points
|
|
@@ -929,6 +1073,14 @@ export function markerToPolygons(markerInstance, options = {}) {
|
|
|
929
1073
|
* @returns {Array<Object>} Array of {x, y} points
|
|
930
1074
|
*/
|
|
931
1075
|
export function pathToPoints(pathData, segments = 10) {
|
|
1076
|
+
// Validate parameters
|
|
1077
|
+
if (typeof pathData !== 'string') {
|
|
1078
|
+
throw new Error('pathToPoints: pathData must be a string');
|
|
1079
|
+
}
|
|
1080
|
+
if (typeof segments !== 'number' || segments <= 0) {
|
|
1081
|
+
throw new Error('pathToPoints: segments must be a positive number');
|
|
1082
|
+
}
|
|
1083
|
+
|
|
932
1084
|
const points = [];
|
|
933
1085
|
const commands = parsePathCommands(pathData);
|
|
934
1086
|
let currentX = 0;
|
|
@@ -988,6 +1140,8 @@ export function pathToPoints(pathData, segments = 10) {
|
|
|
988
1140
|
currentY = cmd.y;
|
|
989
1141
|
points.push({ x: currentX, y: currentY });
|
|
990
1142
|
break;
|
|
1143
|
+
default:
|
|
1144
|
+
break;
|
|
991
1145
|
}
|
|
992
1146
|
}
|
|
993
1147
|
|
|
@@ -1004,6 +1158,20 @@ export function pathToPoints(pathData, segments = 10) {
|
|
|
1004
1158
|
* @returns {Array<Object>} Array of {x, y} points
|
|
1005
1159
|
*/
|
|
1006
1160
|
export function circleToPoints(cx, cy, r, segments = 32) {
|
|
1161
|
+
// Validate parameters
|
|
1162
|
+
if (typeof cx !== 'number' || !isFinite(cx)) {
|
|
1163
|
+
throw new Error('circleToPoints: cx must be a finite number');
|
|
1164
|
+
}
|
|
1165
|
+
if (typeof cy !== 'number' || !isFinite(cy)) {
|
|
1166
|
+
throw new Error('circleToPoints: cy must be a finite number');
|
|
1167
|
+
}
|
|
1168
|
+
if (typeof r !== 'number' || !isFinite(r) || r < 0) {
|
|
1169
|
+
throw new Error('circleToPoints: r must be a non-negative finite number');
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof segments !== 'number' || segments <= 0) {
|
|
1172
|
+
throw new Error('circleToPoints: segments must be a positive number');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1007
1175
|
const points = [];
|
|
1008
1176
|
for (let i = 0; i < segments; i++) {
|
|
1009
1177
|
const angle = (i / segments) * 2 * Math.PI;
|
|
@@ -1026,6 +1194,23 @@ export function circleToPoints(cx, cy, r, segments = 32) {
|
|
|
1026
1194
|
* @returns {Array<Object>} Array of {x, y} points
|
|
1027
1195
|
*/
|
|
1028
1196
|
export function ellipseToPoints(cx, cy, rx, ry, segments = 32) {
|
|
1197
|
+
// Validate parameters
|
|
1198
|
+
if (typeof cx !== 'number' || !isFinite(cx)) {
|
|
1199
|
+
throw new Error('ellipseToPoints: cx must be a finite number');
|
|
1200
|
+
}
|
|
1201
|
+
if (typeof cy !== 'number' || !isFinite(cy)) {
|
|
1202
|
+
throw new Error('ellipseToPoints: cy must be a finite number');
|
|
1203
|
+
}
|
|
1204
|
+
if (typeof rx !== 'number' || !isFinite(rx) || rx < 0) {
|
|
1205
|
+
throw new Error('ellipseToPoints: rx must be a non-negative finite number');
|
|
1206
|
+
}
|
|
1207
|
+
if (typeof ry !== 'number' || !isFinite(ry) || ry < 0) {
|
|
1208
|
+
throw new Error('ellipseToPoints: ry must be a non-negative finite number');
|
|
1209
|
+
}
|
|
1210
|
+
if (typeof segments !== 'number' || segments <= 0) {
|
|
1211
|
+
throw new Error('ellipseToPoints: segments must be a positive number');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1029
1214
|
const points = [];
|
|
1030
1215
|
for (let i = 0; i < segments; i++) {
|
|
1031
1216
|
const angle = (i / segments) * 2 * Math.PI;
|
|
@@ -1044,13 +1229,24 @@ export function ellipseToPoints(cx, cy, rx, ry, segments = 32) {
|
|
|
1044
1229
|
* @returns {Array<Object>} Array of {x, y} points
|
|
1045
1230
|
*/
|
|
1046
1231
|
export function parsePoints(pointsStr) {
|
|
1232
|
+
// Validate parameter
|
|
1233
|
+
if (typeof pointsStr !== 'string') {
|
|
1234
|
+
throw new Error('parsePoints: pointsStr must be a string');
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1047
1237
|
const points = [];
|
|
1048
1238
|
const coords = pointsStr
|
|
1049
1239
|
.trim()
|
|
1050
1240
|
.split(/[\s,]+/)
|
|
1051
|
-
.map(Number)
|
|
1241
|
+
.map(Number)
|
|
1242
|
+
.filter(n => !isNaN(n) && isFinite(n)); // Filter out invalid numbers
|
|
1052
1243
|
|
|
1053
|
-
|
|
1244
|
+
// Ensure we have an even number of coordinates
|
|
1245
|
+
if (coords.length % 2 !== 0) {
|
|
1246
|
+
throw new Error('parsePoints: pointsStr must contain an even number of valid coordinates');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
for (let i = 0; i < coords.length; i += 2) {
|
|
1054
1250
|
points.push({ x: coords[i], y: coords[i + 1] });
|
|
1055
1251
|
}
|
|
1056
1252
|
|
|
@@ -1074,6 +1270,14 @@ export function parsePoints(pointsStr) {
|
|
|
1074
1270
|
* // Returns: "M 100.000 200.000 L 105.000 205.000 ... Z M ..."
|
|
1075
1271
|
*/
|
|
1076
1272
|
export function markersToPathData(markerInstances, precision = 2) {
|
|
1273
|
+
// Validate parameters
|
|
1274
|
+
if (!Array.isArray(markerInstances)) {
|
|
1275
|
+
throw new Error('markersToPathData: markerInstances must be an array');
|
|
1276
|
+
}
|
|
1277
|
+
if (typeof precision !== 'number' || precision < 0) {
|
|
1278
|
+
throw new Error('markersToPathData: precision must be a non-negative number');
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1077
1281
|
const pathParts = [];
|
|
1078
1282
|
|
|
1079
1283
|
for (const instance of markerInstances) {
|