@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/transforms3d.js
CHANGED
|
@@ -5,8 +5,20 @@ import { Matrix } from "./matrix.js";
|
|
|
5
5
|
* Helper to convert any numeric input to Decimal.
|
|
6
6
|
* @param {number|string|Decimal} x - The value to convert
|
|
7
7
|
* @returns {Decimal} The Decimal representation
|
|
8
|
+
* @throws {Error} If value cannot be converted to a valid Decimal
|
|
8
9
|
*/
|
|
9
|
-
const D = (x) =>
|
|
10
|
+
const D = (x) => {
|
|
11
|
+
if (x instanceof Decimal) return x;
|
|
12
|
+
try {
|
|
13
|
+
const result = new Decimal(x);
|
|
14
|
+
if (!result.isFinite()) {
|
|
15
|
+
throw new Error(`Value must be finite, got ${x}`);
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
} catch (_err) {
|
|
19
|
+
throw new Error(`Invalid numeric value: ${x}`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Validates that a value is a valid numeric type (number, string, or Decimal).
|
|
@@ -198,9 +210,14 @@ export function scale(sx, sy = null, sz = null) {
|
|
|
198
210
|
* const pitch = rotateX(-0.1); // Slight downward tilt
|
|
199
211
|
*/
|
|
200
212
|
export function rotateX(theta) {
|
|
213
|
+
validateNumeric(theta, 'theta');
|
|
201
214
|
const t = D(theta);
|
|
202
|
-
const
|
|
203
|
-
|
|
215
|
+
const tNum = t.toNumber();
|
|
216
|
+
if (!Number.isFinite(tNum)) {
|
|
217
|
+
throw new Error(`theta must produce a finite angle, got ${theta}`);
|
|
218
|
+
}
|
|
219
|
+
const c = new Decimal(Math.cos(tNum));
|
|
220
|
+
const s = new Decimal(Math.sin(tNum));
|
|
204
221
|
return Matrix.from([
|
|
205
222
|
[new Decimal(1), new Decimal(0), new Decimal(0), new Decimal(0)],
|
|
206
223
|
[new Decimal(0), c, s.negated(), new Decimal(0)],
|
|
@@ -242,9 +259,14 @@ export function rotateX(theta) {
|
|
|
242
259
|
* const yaw = rotateY(0.5); // Turn right by ~28.6°
|
|
243
260
|
*/
|
|
244
261
|
export function rotateY(theta) {
|
|
262
|
+
validateNumeric(theta, 'theta');
|
|
245
263
|
const t = D(theta);
|
|
246
|
-
const
|
|
247
|
-
|
|
264
|
+
const tNum = t.toNumber();
|
|
265
|
+
if (!Number.isFinite(tNum)) {
|
|
266
|
+
throw new Error(`theta must produce a finite angle, got ${theta}`);
|
|
267
|
+
}
|
|
268
|
+
const c = new Decimal(Math.cos(tNum));
|
|
269
|
+
const s = new Decimal(Math.sin(tNum));
|
|
248
270
|
return Matrix.from([
|
|
249
271
|
[c, new Decimal(0), s, new Decimal(0)],
|
|
250
272
|
[new Decimal(0), new Decimal(1), new Decimal(0), new Decimal(0)],
|
|
@@ -287,9 +309,14 @@ export function rotateY(theta) {
|
|
|
287
309
|
* const roll = rotateZ(0.2); // Slight clockwise tilt from viewer perspective
|
|
288
310
|
*/
|
|
289
311
|
export function rotateZ(theta) {
|
|
312
|
+
validateNumeric(theta, 'theta');
|
|
290
313
|
const t = D(theta);
|
|
291
|
-
const
|
|
292
|
-
|
|
314
|
+
const tNum = t.toNumber();
|
|
315
|
+
if (!Number.isFinite(tNum)) {
|
|
316
|
+
throw new Error(`theta must produce a finite angle, got ${theta}`);
|
|
317
|
+
}
|
|
318
|
+
const c = new Decimal(Math.cos(tNum));
|
|
319
|
+
const s = new Decimal(Math.sin(tNum));
|
|
293
320
|
return Matrix.from([
|
|
294
321
|
[c, s.negated(), new Decimal(0), new Decimal(0)],
|
|
295
322
|
[s, c, new Decimal(0), new Decimal(0)],
|
|
@@ -366,8 +393,12 @@ export function rotateAroundAxis(ux, uy, uz, theta) {
|
|
|
366
393
|
u[2] = u[2].div(norm);
|
|
367
394
|
|
|
368
395
|
const t = D(theta);
|
|
369
|
-
const
|
|
370
|
-
|
|
396
|
+
const tNum = t.toNumber();
|
|
397
|
+
if (!Number.isFinite(tNum)) {
|
|
398
|
+
throw new Error(`theta must produce a finite angle, got ${theta}`);
|
|
399
|
+
}
|
|
400
|
+
const c = new Decimal(Math.cos(tNum));
|
|
401
|
+
const s = new Decimal(Math.sin(tNum));
|
|
371
402
|
const one = new Decimal(1);
|
|
372
403
|
|
|
373
404
|
// Rodrigues' rotation formula components
|
|
@@ -435,6 +466,13 @@ export function rotateAroundAxis(ux, uy, uz, theta) {
|
|
|
435
466
|
* // Complex rotation around axis (1,1,1) passing through (10,20,30)
|
|
436
467
|
*/
|
|
437
468
|
export function rotateAroundPoint(ux, uy, uz, theta, px, py, pz) {
|
|
469
|
+
validateNumeric(ux, 'ux');
|
|
470
|
+
validateNumeric(uy, 'uy');
|
|
471
|
+
validateNumeric(uz, 'uz');
|
|
472
|
+
validateNumeric(theta, 'theta');
|
|
473
|
+
validateNumeric(px, 'px');
|
|
474
|
+
validateNumeric(py, 'py');
|
|
475
|
+
validateNumeric(pz, 'pz');
|
|
438
476
|
const pxD = D(px),
|
|
439
477
|
pyD = D(py),
|
|
440
478
|
pzD = D(pz);
|
|
@@ -489,13 +527,27 @@ export function rotateAroundPoint(ux, uy, uz, theta, px, py, pz) {
|
|
|
489
527
|
* const transformed = vertices.map(([x,y,z]) => applyTransform(R, x, y, z));
|
|
490
528
|
*/
|
|
491
529
|
export function applyTransform(M, x, y, z) {
|
|
530
|
+
if (!(M instanceof Matrix)) {
|
|
531
|
+
throw new Error('M must be a Matrix instance');
|
|
532
|
+
}
|
|
533
|
+
if (M.rows !== 4 || M.cols !== 4) {
|
|
534
|
+
throw new Error(`M must be a 4x4 matrix, got ${M.rows}x${M.cols}`);
|
|
535
|
+
}
|
|
536
|
+
validateNumeric(x, 'x');
|
|
537
|
+
validateNumeric(y, 'y');
|
|
538
|
+
validateNumeric(z, 'z');
|
|
539
|
+
|
|
492
540
|
const P = Matrix.from([[D(x)], [D(y)], [D(z)], [new Decimal(1)]]);
|
|
493
541
|
const R = M.mul(P);
|
|
494
542
|
const rx = R.data[0][0],
|
|
495
543
|
ry = R.data[1][0],
|
|
496
544
|
rz = R.data[2][0],
|
|
497
545
|
rw = R.data[3][0];
|
|
498
|
-
|
|
546
|
+
|
|
547
|
+
if (rw.isZero()) {
|
|
548
|
+
throw new Error('Perspective division by zero: transformation results in point at infinity');
|
|
549
|
+
}
|
|
550
|
+
|
|
499
551
|
return [rx.div(rw), ry.div(rw), rz.div(rw)];
|
|
500
552
|
}
|
|
501
553
|
|
|
@@ -35,6 +35,19 @@ Decimal.set({ precision: 80 });
|
|
|
35
35
|
* @returns {boolean} True if circular reference detected
|
|
36
36
|
*/
|
|
37
37
|
function hasCircularReference(startId, getNextId, maxDepth = 100) {
|
|
38
|
+
// Parameter validation: startId must be a non-empty string
|
|
39
|
+
if (!startId || typeof startId !== 'string') {
|
|
40
|
+
throw new Error('hasCircularReference: startId must be a non-empty string');
|
|
41
|
+
}
|
|
42
|
+
// Parameter validation: getNextId must be a function
|
|
43
|
+
if (typeof getNextId !== 'function') {
|
|
44
|
+
throw new Error('hasCircularReference: getNextId must be a function');
|
|
45
|
+
}
|
|
46
|
+
// Parameter validation: maxDepth must be a positive finite number
|
|
47
|
+
if (typeof maxDepth !== 'number' || maxDepth <= 0 || !isFinite(maxDepth)) {
|
|
48
|
+
throw new Error('hasCircularReference: maxDepth must be a positive finite number');
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
const visited = new Set();
|
|
39
52
|
let currentId = startId;
|
|
40
53
|
let depth = 0;
|
|
@@ -86,21 +99,49 @@ function hasCircularReference(startId, getNextId, maxDepth = 100) {
|
|
|
86
99
|
* // }
|
|
87
100
|
*/
|
|
88
101
|
export function parseUseElement(useElement) {
|
|
102
|
+
// Parameter validation: useElement must be defined
|
|
103
|
+
if (!useElement) throw new Error('parseUseElement: useElement is required');
|
|
104
|
+
|
|
89
105
|
const href =
|
|
90
106
|
useElement.getAttribute("href") ||
|
|
91
107
|
useElement.getAttribute("xlink:href") ||
|
|
92
108
|
"";
|
|
93
109
|
|
|
110
|
+
const parsedHref = href.startsWith("#") ? href.slice(1) : href;
|
|
111
|
+
|
|
112
|
+
// Parse numeric attributes and validate for NaN
|
|
113
|
+
const x = parseFloat(useElement.getAttribute("x") || "0");
|
|
114
|
+
const y = parseFloat(useElement.getAttribute("y") || "0");
|
|
115
|
+
|
|
116
|
+
// Validate that x and y are not NaN
|
|
117
|
+
if (isNaN(x) || isNaN(y)) {
|
|
118
|
+
throw new Error('parseUseElement: x and y attributes must be valid numbers');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Parse width and height if present, validate for NaN
|
|
122
|
+
let width = null;
|
|
123
|
+
let height = null;
|
|
124
|
+
|
|
125
|
+
if (useElement.getAttribute("width")) {
|
|
126
|
+
width = parseFloat(useElement.getAttribute("width"));
|
|
127
|
+
if (isNaN(width)) {
|
|
128
|
+
throw new Error('parseUseElement: width attribute must be a valid number');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (useElement.getAttribute("height")) {
|
|
133
|
+
height = parseFloat(useElement.getAttribute("height"));
|
|
134
|
+
if (isNaN(height)) {
|
|
135
|
+
throw new Error('parseUseElement: height attribute must be a valid number');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
94
139
|
return {
|
|
95
|
-
href:
|
|
96
|
-
x
|
|
97
|
-
y
|
|
98
|
-
width
|
|
99
|
-
|
|
100
|
-
: null,
|
|
101
|
-
height: useElement.getAttribute("height")
|
|
102
|
-
? parseFloat(useElement.getAttribute("height"))
|
|
103
|
-
: null,
|
|
140
|
+
href: parsedHref,
|
|
141
|
+
x,
|
|
142
|
+
y,
|
|
143
|
+
width,
|
|
144
|
+
height,
|
|
104
145
|
transform: useElement.getAttribute("transform") || null,
|
|
105
146
|
style: extractStyleAttributes(useElement),
|
|
106
147
|
};
|
|
@@ -146,14 +187,25 @@ export function parseUseElement(useElement) {
|
|
|
146
187
|
* // }
|
|
147
188
|
*/
|
|
148
189
|
export function parseSymbolElement(symbolElement) {
|
|
190
|
+
// Parameter validation: symbolElement must be defined
|
|
191
|
+
if (!symbolElement) throw new Error('parseSymbolElement: symbolElement is required');
|
|
192
|
+
|
|
193
|
+
// Parse refX and refY with NaN validation
|
|
194
|
+
const refX = parseFloat(symbolElement.getAttribute("refX") || "0");
|
|
195
|
+
const refY = parseFloat(symbolElement.getAttribute("refY") || "0");
|
|
196
|
+
|
|
197
|
+
if (isNaN(refX) || isNaN(refY)) {
|
|
198
|
+
throw new Error('parseSymbolElement: refX and refY must be valid numbers');
|
|
199
|
+
}
|
|
200
|
+
|
|
149
201
|
const data = {
|
|
150
202
|
id: symbolElement.getAttribute("id") || "",
|
|
151
203
|
viewBox: symbolElement.getAttribute("viewBox") || null,
|
|
152
204
|
preserveAspectRatio:
|
|
153
205
|
symbolElement.getAttribute("preserveAspectRatio") || "xMidYMid meet",
|
|
154
206
|
children: [],
|
|
155
|
-
refX
|
|
156
|
-
refY
|
|
207
|
+
refX,
|
|
208
|
+
refY,
|
|
157
209
|
};
|
|
158
210
|
|
|
159
211
|
// Parse viewBox
|
|
@@ -162,7 +214,8 @@ export function parseSymbolElement(symbolElement) {
|
|
|
162
214
|
.trim()
|
|
163
215
|
.split(/[\s,]+/)
|
|
164
216
|
.map(Number);
|
|
165
|
-
|
|
217
|
+
// Validate viewBox has exactly 4 parts and all are valid numbers
|
|
218
|
+
if (parts.length === 4 && parts.every((p) => !isNaN(p) && isFinite(p))) {
|
|
166
219
|
data.viewBoxParsed = {
|
|
167
220
|
x: parts[0],
|
|
168
221
|
y: parts[1],
|
|
@@ -242,6 +295,11 @@ export function parseSymbolElement(symbolElement) {
|
|
|
242
295
|
* // }
|
|
243
296
|
*/
|
|
244
297
|
export function parseChildElement(element) {
|
|
298
|
+
// Parameter validation: element must be defined and have a tagName
|
|
299
|
+
if (!element || !element.tagName) {
|
|
300
|
+
throw new Error('parseChildElement: element with tagName is required');
|
|
301
|
+
}
|
|
302
|
+
|
|
245
303
|
const tagName = element.tagName.toLowerCase();
|
|
246
304
|
|
|
247
305
|
const data = {
|
|
@@ -251,25 +309,34 @@ export function parseChildElement(element) {
|
|
|
251
309
|
style: extractStyleAttributes(element),
|
|
252
310
|
};
|
|
253
311
|
|
|
312
|
+
// Helper to safely parse float with NaN check
|
|
313
|
+
const safeParseFloat = (attrName, defaultValue = "0") => {
|
|
314
|
+
const value = parseFloat(element.getAttribute(attrName) || defaultValue);
|
|
315
|
+
if (isNaN(value)) {
|
|
316
|
+
throw new Error(`parseChildElement: ${attrName} must be a valid number in ${tagName} element`);
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
};
|
|
320
|
+
|
|
254
321
|
switch (tagName) {
|
|
255
322
|
case "rect":
|
|
256
|
-
data.x =
|
|
257
|
-
data.y =
|
|
258
|
-
data.width =
|
|
259
|
-
data.height =
|
|
260
|
-
data.rx =
|
|
261
|
-
data.ry =
|
|
323
|
+
data.x = safeParseFloat("x");
|
|
324
|
+
data.y = safeParseFloat("y");
|
|
325
|
+
data.width = safeParseFloat("width");
|
|
326
|
+
data.height = safeParseFloat("height");
|
|
327
|
+
data.rx = safeParseFloat("rx");
|
|
328
|
+
data.ry = safeParseFloat("ry");
|
|
262
329
|
break;
|
|
263
330
|
case "circle":
|
|
264
|
-
data.cx =
|
|
265
|
-
data.cy =
|
|
266
|
-
data.r =
|
|
331
|
+
data.cx = safeParseFloat("cx");
|
|
332
|
+
data.cy = safeParseFloat("cy");
|
|
333
|
+
data.r = safeParseFloat("r");
|
|
267
334
|
break;
|
|
268
335
|
case "ellipse":
|
|
269
|
-
data.cx =
|
|
270
|
-
data.cy =
|
|
271
|
-
data.rx =
|
|
272
|
-
data.ry =
|
|
336
|
+
data.cx = safeParseFloat("cx");
|
|
337
|
+
data.cy = safeParseFloat("cy");
|
|
338
|
+
data.rx = safeParseFloat("rx");
|
|
339
|
+
data.ry = safeParseFloat("ry");
|
|
273
340
|
break;
|
|
274
341
|
case "path":
|
|
275
342
|
data.d = element.getAttribute("d") || "";
|
|
@@ -281,10 +348,10 @@ export function parseChildElement(element) {
|
|
|
281
348
|
data.points = element.getAttribute("points") || "";
|
|
282
349
|
break;
|
|
283
350
|
case "line":
|
|
284
|
-
data.x1 =
|
|
285
|
-
data.y1 =
|
|
286
|
-
data.x2 =
|
|
287
|
-
data.y2 =
|
|
351
|
+
data.x1 = safeParseFloat("x1");
|
|
352
|
+
data.y1 = safeParseFloat("y1");
|
|
353
|
+
data.x2 = safeParseFloat("x2");
|
|
354
|
+
data.y2 = safeParseFloat("y2");
|
|
288
355
|
break;
|
|
289
356
|
case "g":
|
|
290
357
|
data.children = [];
|
|
@@ -298,15 +365,18 @@ export function parseChildElement(element) {
|
|
|
298
365
|
element.getAttribute("xlink:href") ||
|
|
299
366
|
""
|
|
300
367
|
).replace("#", "");
|
|
301
|
-
data.x =
|
|
302
|
-
data.y =
|
|
368
|
+
data.x = safeParseFloat("x");
|
|
369
|
+
data.y = safeParseFloat("y");
|
|
370
|
+
// Width and height can be null
|
|
303
371
|
data.width = element.getAttribute("width")
|
|
304
|
-
?
|
|
372
|
+
? safeParseFloat("width")
|
|
305
373
|
: null;
|
|
306
374
|
data.height = element.getAttribute("height")
|
|
307
|
-
?
|
|
375
|
+
? safeParseFloat("height")
|
|
308
376
|
: null;
|
|
309
377
|
break;
|
|
378
|
+
default:
|
|
379
|
+
break;
|
|
310
380
|
}
|
|
311
381
|
|
|
312
382
|
return data;
|
|
@@ -351,6 +421,11 @@ export function parseChildElement(element) {
|
|
|
351
421
|
* // }
|
|
352
422
|
*/
|
|
353
423
|
export function extractStyleAttributes(element) {
|
|
424
|
+
// Parameter validation: element must be defined
|
|
425
|
+
if (!element) {
|
|
426
|
+
throw new Error('extractStyleAttributes: element is required');
|
|
427
|
+
}
|
|
428
|
+
|
|
354
429
|
return {
|
|
355
430
|
fill: element.getAttribute("fill"),
|
|
356
431
|
stroke: element.getAttribute("stroke"),
|
|
@@ -416,16 +491,23 @@ export function calculateViewBoxTransform(
|
|
|
416
491
|
targetHeight,
|
|
417
492
|
preserveAspectRatio = "xMidYMid meet",
|
|
418
493
|
) {
|
|
494
|
+
// Parameter validation: viewBox must have required properties
|
|
419
495
|
if (!viewBox || !targetWidth || !targetHeight) {
|
|
420
496
|
return Matrix.identity(3);
|
|
421
497
|
}
|
|
422
498
|
|
|
499
|
+
// Validate targetWidth and targetHeight are finite positive numbers
|
|
500
|
+
if (!isFinite(targetWidth) || !isFinite(targetHeight) || targetWidth <= 0 || targetHeight <= 0) {
|
|
501
|
+
return Matrix.identity(3);
|
|
502
|
+
}
|
|
503
|
+
|
|
423
504
|
const vbW = viewBox.width;
|
|
424
505
|
const vbH = viewBox.height;
|
|
425
506
|
const vbX = viewBox.x;
|
|
426
507
|
const vbY = viewBox.y;
|
|
427
508
|
|
|
428
|
-
|
|
509
|
+
// Validate viewBox dimensions are finite and positive
|
|
510
|
+
if (!isFinite(vbW) || !isFinite(vbH) || !isFinite(vbX) || !isFinite(vbY) || vbW <= 0 || vbH <= 0) {
|
|
429
511
|
return Matrix.identity(3);
|
|
430
512
|
}
|
|
431
513
|
|
|
@@ -549,15 +631,59 @@ export function calculateViewBoxTransform(
|
|
|
549
631
|
* // Recursively resolves ref1 → shape, composing transforms
|
|
550
632
|
*/
|
|
551
633
|
export function resolveUse(useData, defs, options = {}) {
|
|
634
|
+
// Parameter validation: useData must be defined with href property
|
|
635
|
+
if (!useData || !useData.href) {
|
|
636
|
+
throw new Error('resolveUse: useData with href property is required');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Parameter validation: defs must be defined
|
|
640
|
+
if (!defs) {
|
|
641
|
+
throw new Error('resolveUse: defs map is required');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Validate useData.x and useData.y are valid numbers
|
|
645
|
+
if (typeof useData.x !== 'number' || isNaN(useData.x) || !isFinite(useData.x)) {
|
|
646
|
+
throw new Error('resolveUse: useData.x must be a valid finite number');
|
|
647
|
+
}
|
|
648
|
+
if (typeof useData.y !== 'number' || isNaN(useData.y) || !isFinite(useData.y)) {
|
|
649
|
+
throw new Error('resolveUse: useData.y must be a valid finite number');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Validate useData.width and useData.height are null or valid numbers
|
|
653
|
+
if (useData.width !== null && (typeof useData.width !== 'number' || isNaN(useData.width) || !isFinite(useData.width) || useData.width <= 0)) {
|
|
654
|
+
throw new Error('resolveUse: useData.width must be null or a positive finite number');
|
|
655
|
+
}
|
|
656
|
+
if (useData.height !== null && (typeof useData.height !== 'number' || isNaN(useData.height) || !isFinite(useData.height) || useData.height <= 0)) {
|
|
657
|
+
throw new Error('resolveUse: useData.height must be null or a positive finite number');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Validate useData.style is an object or null/undefined
|
|
661
|
+
if (useData.style !== null && useData.style !== undefined) {
|
|
662
|
+
if (typeof useData.style !== 'object' || Array.isArray(useData.style)) {
|
|
663
|
+
throw new Error('resolveUse: useData.style must be null, undefined, or a valid non-array object');
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Validate useData.transform is a string or null/undefined
|
|
668
|
+
if (useData.transform !== null && useData.transform !== undefined && typeof useData.transform !== 'string') {
|
|
669
|
+
throw new Error('resolveUse: useData.transform must be null, undefined, or a string');
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Validate options parameter
|
|
673
|
+
if (options && typeof options !== 'object') {
|
|
674
|
+
throw new Error('resolveUse: options must be an object or undefined');
|
|
675
|
+
}
|
|
676
|
+
|
|
552
677
|
const { maxDepth = 10 } = options;
|
|
553
678
|
|
|
554
|
-
|
|
555
|
-
|
|
679
|
+
// Validate maxDepth is a positive finite number
|
|
680
|
+
if (typeof maxDepth !== 'number' || maxDepth <= 0 || !isFinite(maxDepth)) {
|
|
681
|
+
throw new Error('resolveUse: maxDepth must be a positive finite number');
|
|
556
682
|
}
|
|
557
683
|
|
|
558
684
|
const target = defs[useData.href];
|
|
559
685
|
if (!target) {
|
|
560
|
-
return null;
|
|
686
|
+
return null; // Target element not found
|
|
561
687
|
}
|
|
562
688
|
|
|
563
689
|
// CORRECT ORDER per SVG spec:
|
|
@@ -581,6 +707,11 @@ export function resolveUse(useData, defs, options = {}) {
|
|
|
581
707
|
// Handle symbol with viewBox (step 3)
|
|
582
708
|
// ViewBox transform applies LAST (after translation and useTransform)
|
|
583
709
|
if (target.type === "symbol" && target.viewBoxParsed) {
|
|
710
|
+
// Validate viewBoxParsed has required properties
|
|
711
|
+
if (typeof target.viewBoxParsed.width !== 'number' || typeof target.viewBoxParsed.height !== 'number') {
|
|
712
|
+
throw new Error('resolveUse: target.viewBoxParsed must have valid width and height properties');
|
|
713
|
+
}
|
|
714
|
+
|
|
584
715
|
const width = useData.width || target.viewBoxParsed.width;
|
|
585
716
|
const height = useData.height || target.viewBoxParsed.height;
|
|
586
717
|
|
|
@@ -588,7 +719,7 @@ export function resolveUse(useData, defs, options = {}) {
|
|
|
588
719
|
target.viewBoxParsed,
|
|
589
720
|
width,
|
|
590
721
|
height,
|
|
591
|
-
target.preserveAspectRatio,
|
|
722
|
+
target.preserveAspectRatio || "xMidYMid meet",
|
|
592
723
|
);
|
|
593
724
|
|
|
594
725
|
// ViewBox transform is applied LAST, so it's the leftmost in multiplication
|
|
@@ -677,9 +808,25 @@ export function resolveUse(useData, defs, options = {}) {
|
|
|
677
808
|
export function flattenResolvedUse(resolved, samples = 20) {
|
|
678
809
|
const results = [];
|
|
679
810
|
|
|
811
|
+
// Edge case: resolved is null or undefined
|
|
680
812
|
if (!resolved) return results;
|
|
681
813
|
|
|
814
|
+
// Parameter validation: samples must be a positive number
|
|
815
|
+
if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
|
|
816
|
+
throw new Error('flattenResolvedUse: samples must be a positive finite number');
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Validate required properties exist
|
|
820
|
+
if (!resolved.children || !resolved.transform) {
|
|
821
|
+
return results;
|
|
822
|
+
}
|
|
823
|
+
|
|
682
824
|
for (const child of resolved.children) {
|
|
825
|
+
// Validate child has required properties
|
|
826
|
+
if (!child || !child.transform) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
|
|
683
830
|
const childTransform = resolved.transform.mul(child.transform);
|
|
684
831
|
const element = child.element;
|
|
685
832
|
|
|
@@ -747,10 +894,20 @@ export function flattenResolvedUse(resolved, samples = 20) {
|
|
|
747
894
|
* // [{ x: 0, y: 0 }, { x: 50, y: 0 }, { x: 50, y: 30 }, { x: 0, y: 30 }]
|
|
748
895
|
*/
|
|
749
896
|
export function elementToPolygon(element, transform, samples = 20) {
|
|
897
|
+
// Parameter validation: element must be defined
|
|
898
|
+
if (!element) {
|
|
899
|
+
throw new Error('elementToPolygon: element is required');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Parameter validation: samples must be a positive finite number
|
|
903
|
+
if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
|
|
904
|
+
throw new Error('elementToPolygon: samples must be a positive finite number');
|
|
905
|
+
}
|
|
906
|
+
|
|
750
907
|
// Use ClipPathResolver's shapeToPolygon
|
|
751
908
|
let polygon = ClipPathResolver.shapeToPolygon(element, null, samples);
|
|
752
909
|
|
|
753
|
-
// Apply transform
|
|
910
|
+
// Apply transform if provided and polygon is not empty
|
|
754
911
|
if (transform && polygon.length > 0) {
|
|
755
912
|
polygon = polygon.map((p) => {
|
|
756
913
|
const [x, y] = Transforms2D.applyTransform(transform, p.x, p.y);
|
|
@@ -806,8 +963,21 @@ export function elementToPolygon(element, transform, samples = 20) {
|
|
|
806
963
|
* // { fill: 'blue' }
|
|
807
964
|
*/
|
|
808
965
|
export function mergeStyles(inherited, element) {
|
|
966
|
+
// Parameter validation: element must be defined (inherited can be null)
|
|
967
|
+
if (!element || typeof element !== 'object' || Array.isArray(element)) {
|
|
968
|
+
throw new Error('mergeStyles: element must be a valid non-array object');
|
|
969
|
+
}
|
|
970
|
+
|
|
809
971
|
const result = { ...element };
|
|
810
972
|
|
|
973
|
+
// Inherited can be null/undefined, handle gracefully
|
|
974
|
+
// Also validate inherited is an object if not null/undefined
|
|
975
|
+
if (inherited !== null && inherited !== undefined) {
|
|
976
|
+
if (typeof inherited !== 'object' || Array.isArray(inherited)) {
|
|
977
|
+
throw new Error('mergeStyles: inherited must be null, undefined, or a valid non-array object');
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
811
981
|
for (const [key, value] of Object.entries(inherited || {})) {
|
|
812
982
|
// Inherit if value is not null and element doesn't have a value (null or undefined)
|
|
813
983
|
if (value !== null && (result[key] === null || result[key] === undefined)) {
|
|
@@ -862,6 +1032,11 @@ export function mergeStyles(inherited, element) {
|
|
|
862
1032
|
* // Axis-aligned bbox enclosing the rotated rectangle (wider than original)
|
|
863
1033
|
*/
|
|
864
1034
|
export function getResolvedBBox(resolved, samples = 20) {
|
|
1035
|
+
// Parameter validation: samples must be a positive finite number
|
|
1036
|
+
if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
|
|
1037
|
+
throw new Error('getResolvedBBox: samples must be a positive finite number');
|
|
1038
|
+
}
|
|
1039
|
+
|
|
865
1040
|
const polygons = flattenResolvedUse(resolved, samples);
|
|
866
1041
|
|
|
867
1042
|
let minX = Infinity;
|
|
@@ -873,6 +1048,12 @@ export function getResolvedBBox(resolved, samples = 20) {
|
|
|
873
1048
|
for (const p of polygon) {
|
|
874
1049
|
const x = Number(p.x);
|
|
875
1050
|
const y = Number(p.y);
|
|
1051
|
+
|
|
1052
|
+
// Skip NaN values to prevent corrupting bounding box
|
|
1053
|
+
if (isNaN(x) || isNaN(y)) {
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
876
1057
|
minX = Math.min(minX, x);
|
|
877
1058
|
minY = Math.min(minY, y);
|
|
878
1059
|
maxX = Math.max(maxX, x);
|
|
@@ -880,6 +1061,7 @@ export function getResolvedBBox(resolved, samples = 20) {
|
|
|
880
1061
|
}
|
|
881
1062
|
}
|
|
882
1063
|
|
|
1064
|
+
// Return empty bbox if no valid points found
|
|
883
1065
|
if (minX === Infinity) {
|
|
884
1066
|
return { x: 0, y: 0, width: 0, height: 0 };
|
|
885
1067
|
}
|
|
@@ -940,6 +1122,21 @@ export function getResolvedBBox(resolved, samples = 20) {
|
|
|
940
1122
|
* // May produce multiple disjoint polygons if use element spans outside triangle
|
|
941
1123
|
*/
|
|
942
1124
|
export function clipResolvedUse(resolved, clipPolygon, samples = 20) {
|
|
1125
|
+
// Parameter validation: clipPolygon must be defined and be an array
|
|
1126
|
+
if (!clipPolygon || !Array.isArray(clipPolygon)) {
|
|
1127
|
+
throw new Error('clipResolvedUse: clipPolygon must be a valid array');
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Validate clipPolygon has at least 3 points
|
|
1131
|
+
if (clipPolygon.length < 3) {
|
|
1132
|
+
throw new Error('clipResolvedUse: clipPolygon must have at least 3 vertices');
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Parameter validation: samples must be a positive finite number
|
|
1136
|
+
if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
|
|
1137
|
+
throw new Error('clipResolvedUse: samples must be a positive finite number');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
943
1140
|
const polygons = flattenResolvedUse(resolved, samples);
|
|
944
1141
|
const result = [];
|
|
945
1142
|
|
|
@@ -1009,6 +1206,11 @@ export function clipResolvedUse(resolved, clipPolygon, samples = 20) {
|
|
|
1009
1206
|
* // Two closed subpaths (rectangle + circle)
|
|
1010
1207
|
*/
|
|
1011
1208
|
export function resolvedUseToPathData(resolved, samples = 20) {
|
|
1209
|
+
// Parameter validation: samples must be a positive finite number
|
|
1210
|
+
if (typeof samples !== 'number' || samples <= 0 || !isFinite(samples)) {
|
|
1211
|
+
throw new Error('resolvedUseToPathData: samples must be a positive finite number');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1012
1214
|
const polygons = flattenResolvedUse(resolved, samples);
|
|
1013
1215
|
const paths = [];
|
|
1014
1216
|
|
|
@@ -1017,9 +1219,15 @@ export function resolvedUseToPathData(resolved, samples = 20) {
|
|
|
1017
1219
|
let d = "";
|
|
1018
1220
|
for (let i = 0; i < polygon.length; i++) {
|
|
1019
1221
|
const p = polygon[i];
|
|
1020
|
-
const x = Number(p.x)
|
|
1021
|
-
const y = Number(p.y)
|
|
1022
|
-
|
|
1222
|
+
const x = Number(p.x);
|
|
1223
|
+
const y = Number(p.y);
|
|
1224
|
+
|
|
1225
|
+
// Skip invalid points with NaN coordinates
|
|
1226
|
+
if (isNaN(x) || isNaN(y)) {
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
d += i === 0 ? `M ${x.toFixed(6)} ${y.toFixed(6)}` : ` L ${x.toFixed(6)} ${y.toFixed(6)}`;
|
|
1023
1231
|
}
|
|
1024
1232
|
d += " Z";
|
|
1025
1233
|
paths.push(d);
|
|
@@ -1081,6 +1289,11 @@ export function resolvedUseToPathData(resolved, samples = 20) {
|
|
|
1081
1289
|
* const resolved = resolveUse(useData, defs);
|
|
1082
1290
|
*/
|
|
1083
1291
|
export function buildDefsMap(svgRoot) {
|
|
1292
|
+
// Parameter validation: svgRoot must be defined and have querySelectorAll method
|
|
1293
|
+
if (!svgRoot || typeof svgRoot.querySelectorAll !== 'function') {
|
|
1294
|
+
throw new Error('buildDefsMap: svgRoot must be a valid DOM element');
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1084
1297
|
const defs = {};
|
|
1085
1298
|
|
|
1086
1299
|
// Find all elements with id
|
|
@@ -1088,6 +1301,17 @@ export function buildDefsMap(svgRoot) {
|
|
|
1088
1301
|
|
|
1089
1302
|
for (const element of elementsWithId) {
|
|
1090
1303
|
const id = element.getAttribute("id");
|
|
1304
|
+
|
|
1305
|
+
// Skip elements without a valid id
|
|
1306
|
+
if (!id) {
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Validate element has tagName
|
|
1311
|
+
if (!element.tagName) {
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1091
1315
|
const tagName = element.tagName.toLowerCase();
|
|
1092
1316
|
|
|
1093
1317
|
if (tagName === "symbol") {
|
|
@@ -1166,12 +1390,27 @@ export function buildDefsMap(svgRoot) {
|
|
|
1166
1390
|
* }
|
|
1167
1391
|
*/
|
|
1168
1392
|
export function resolveAllUses(svgRoot, options = {}) {
|
|
1393
|
+
// Parameter validation: svgRoot must be defined and have querySelectorAll method
|
|
1394
|
+
if (!svgRoot || typeof svgRoot.querySelectorAll !== 'function') {
|
|
1395
|
+
throw new Error('resolveAllUses: svgRoot must be a valid DOM element');
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Validate options parameter
|
|
1399
|
+
if (options && typeof options !== 'object') {
|
|
1400
|
+
throw new Error('resolveAllUses: options must be an object or undefined');
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1169
1403
|
const defs = buildDefsMap(svgRoot);
|
|
1170
1404
|
const useElements = svgRoot.querySelectorAll("use");
|
|
1171
1405
|
const resolved = [];
|
|
1172
1406
|
|
|
1173
1407
|
// Helper to get the next use reference from a definition
|
|
1174
1408
|
const getUseRef = (id) => {
|
|
1409
|
+
// Validate id parameter
|
|
1410
|
+
if (!id || typeof id !== 'string') {
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1175
1414
|
const target = defs[id];
|
|
1176
1415
|
if (!target) return null;
|
|
1177
1416
|
|
|
@@ -1195,6 +1434,11 @@ export function resolveAllUses(svgRoot, options = {}) {
|
|
|
1195
1434
|
for (const useEl of useElements) {
|
|
1196
1435
|
const useData = parseUseElement(useEl);
|
|
1197
1436
|
|
|
1437
|
+
// Skip use elements without valid href
|
|
1438
|
+
if (!useData.href) {
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1198
1442
|
// Check for circular reference before attempting to resolve
|
|
1199
1443
|
if (hasCircularReference(useData.href, getUseRef)) {
|
|
1200
1444
|
console.warn(
|