@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.
Files changed (45) hide show
  1. package/bin/svg-matrix.js +310 -61
  2. package/bin/svglinter.cjs +102 -3
  3. package/bin/svgm.js +236 -27
  4. package/package.json +1 -1
  5. package/src/animation-optimization.js +137 -17
  6. package/src/animation-references.js +123 -6
  7. package/src/arc-length.js +213 -4
  8. package/src/bezier-analysis.js +217 -21
  9. package/src/bezier-intersections.js +275 -12
  10. package/src/browser-verify.js +237 -4
  11. package/src/clip-path-resolver.js +168 -0
  12. package/src/convert-path-data.js +479 -28
  13. package/src/css-specificity.js +73 -10
  14. package/src/douglas-peucker.js +219 -2
  15. package/src/flatten-pipeline.js +284 -26
  16. package/src/geometry-to-path.js +250 -25
  17. package/src/gjk-collision.js +236 -33
  18. package/src/index.js +261 -3
  19. package/src/inkscape-support.js +86 -28
  20. package/src/logger.js +48 -3
  21. package/src/marker-resolver.js +278 -74
  22. package/src/mask-resolver.js +265 -66
  23. package/src/matrix.js +44 -5
  24. package/src/mesh-gradient.js +352 -102
  25. package/src/off-canvas-detection.js +382 -13
  26. package/src/path-analysis.js +192 -18
  27. package/src/path-data-plugins.js +309 -5
  28. package/src/path-optimization.js +129 -5
  29. package/src/path-simplification.js +188 -32
  30. package/src/pattern-resolver.js +454 -106
  31. package/src/polygon-clip.js +324 -1
  32. package/src/svg-boolean-ops.js +226 -9
  33. package/src/svg-collections.js +7 -5
  34. package/src/svg-flatten.js +386 -62
  35. package/src/svg-parser.js +179 -8
  36. package/src/svg-rendering-context.js +235 -6
  37. package/src/svg-toolbox.js +45 -8
  38. package/src/svg2-polyfills.js +40 -10
  39. package/src/transform-decomposition.js +258 -32
  40. package/src/transform-optimization.js +259 -13
  41. package/src/transforms2d.js +82 -9
  42. package/src/transforms3d.js +62 -10
  43. package/src/use-symbol-resolver.js +286 -42
  44. package/src/vector.js +64 -8
  45. package/src/verification.js +392 -1
@@ -46,7 +46,6 @@ export function parseSelector(selectorString) {
46
46
 
47
47
  const selector = selectorString.trim();
48
48
  const components = [];
49
- const _i = 0;
50
49
 
51
50
  // Split by combinators first (>, +, ~, space) to handle complex selectors
52
51
  const parts = splitByCombinators(selector);
@@ -69,6 +68,10 @@ export function parseSelector(selectorString) {
69
68
  * @returns {Array<string>} Parts split by combinators
70
69
  */
71
70
  function splitByCombinators(selector) {
71
+ if (typeof selector !== "string") {
72
+ throw new Error("Selector must be a string");
73
+ }
74
+
72
75
  // Match combinators outside of brackets and parentheses
73
76
  const parts = [];
74
77
  let current = "";
@@ -84,12 +87,18 @@ function splitByCombinators(selector) {
84
87
  depth++;
85
88
  } else if (char === "]") {
86
89
  depth--;
90
+ if (depth < 0) {
91
+ throw new Error(`Unbalanced brackets at position ${i}`);
92
+ }
87
93
  if (depth === 0) inBracket = false;
88
94
  } else if (char === "(") {
89
95
  inParen = true;
90
96
  depth++;
91
97
  } else if (char === ")") {
92
98
  depth--;
99
+ if (depth < 0) {
100
+ throw new Error(`Unbalanced parentheses at position ${i}`);
101
+ }
93
102
  if (depth === 0) inParen = false;
94
103
  }
95
104
 
@@ -111,6 +120,10 @@ function splitByCombinators(selector) {
111
120
  current += char;
112
121
  }
113
122
 
123
+ if (depth !== 0) {
124
+ throw new Error("Unclosed brackets or parentheses in selector");
125
+ }
126
+
114
127
  if (current.trim()) {
115
128
  parts.push(current.trim());
116
129
  }
@@ -125,6 +138,10 @@ function splitByCombinators(selector) {
125
138
  * @returns {Array<Object>} Array of component objects
126
139
  */
127
140
  function parseSimpleSelector(selector) {
141
+ if (typeof selector !== "string") {
142
+ throw new Error("Selector must be a string");
143
+ }
144
+
128
145
  const components = [];
129
146
  let i = 0;
130
147
 
@@ -156,7 +173,7 @@ function parseSimpleSelector(selector) {
156
173
  }
157
174
  // Pseudo-element (::) or pseudo-class (:)
158
175
  else if (char === ":") {
159
- if (selector[i + 1] === ":") {
176
+ if (i + 1 < selector.length && selector[i + 1] === ":") {
160
177
  // Pseudo-element
161
178
  const match = selector.slice(i).match(/^::([\w-]+)/);
162
179
  if (!match) throw new Error(`Invalid pseudo-element at position ${i}`);
@@ -174,7 +191,7 @@ function parseSimpleSelector(selector) {
174
191
  i += match[0].length;
175
192
 
176
193
  // Check for function notation like :not()
177
- if (selector[i] === "(") {
194
+ if (i < selector.length && selector[i] === "(") {
178
195
  const endIdx = findMatchingParen(selector, i);
179
196
  if (endIdx === -1)
180
197
  throw new Error(`Unclosed pseudo-class function at position ${i}`);
@@ -219,6 +236,13 @@ function parseSimpleSelector(selector) {
219
236
  * @returns {number} Index of closing bracket or -1 if not found
220
237
  */
221
238
  function findMatchingBracket(str, startIdx) {
239
+ if (typeof str !== "string") {
240
+ throw new Error("str must be a string");
241
+ }
242
+ if (typeof startIdx !== "number" || startIdx < 0 || startIdx >= str.length) {
243
+ throw new Error("startIdx must be a valid index within the string");
244
+ }
245
+
222
246
  let depth = 0;
223
247
  for (let i = startIdx; i < str.length; i++) {
224
248
  if (str[i] === "[") depth++;
@@ -238,6 +262,13 @@ function findMatchingBracket(str, startIdx) {
238
262
  * @returns {number} Index of closing parenthesis or -1 if not found
239
263
  */
240
264
  function findMatchingParen(str, startIdx) {
265
+ if (typeof str !== "string") {
266
+ throw new Error("str must be a string");
267
+ }
268
+ if (typeof startIdx !== "number" || startIdx < 0 || startIdx >= str.length) {
269
+ throw new Error("startIdx must be a valid index within the string");
270
+ }
271
+
241
272
  let depth = 0;
242
273
  for (let i = startIdx; i < str.length; i++) {
243
274
  if (str[i] === "(") depth++;
@@ -272,11 +303,18 @@ export function calculateSpecificity(selector) {
272
303
  const components =
273
304
  typeof selector === "string" ? parseSelector(selector) : selector;
274
305
 
306
+ if (!Array.isArray(components)) {
307
+ throw new Error("Selector must be a string or an array of components");
308
+ }
309
+
275
310
  let a = 0; // IDs
276
311
  let b = 0; // Classes, attributes, pseudo-classes
277
312
  let c = 0; // Types, pseudo-elements
278
313
 
279
314
  for (const component of components) {
315
+ if (!component || typeof component.type !== "string") {
316
+ throw new Error("Invalid component: must have a 'type' property");
317
+ }
280
318
  switch (component.type) {
281
319
  case SELECTOR_TYPES.ID:
282
320
  a++;
@@ -289,12 +327,15 @@ export function calculateSpecificity(selector) {
289
327
 
290
328
  case SELECTOR_TYPES.PSEUDO_CLASS:
291
329
  // Handle :not() - it doesn't count itself, but its argument does
292
- if (component.value.startsWith("not(")) {
330
+ if (component.value.startsWith("not(") && component.value.endsWith(")")) {
293
331
  const notContent = component.value.slice(4, -1); // Extract content inside :not()
294
- const notSpec = calculateSpecificity(notContent);
295
- a += notSpec[0];
296
- b += notSpec[1];
297
- c += notSpec[2];
332
+ if (notContent.trim()) {
333
+ // Only process non-empty :not() content
334
+ const notSpec = calculateSpecificity(notContent);
335
+ a += notSpec[0];
336
+ b += notSpec[1];
337
+ c += notSpec[2];
338
+ }
298
339
  } else {
299
340
  b++;
300
341
  }
@@ -310,7 +351,8 @@ export function calculateSpecificity(selector) {
310
351
  break;
311
352
 
312
353
  default:
313
- // Unknown type, ignore
354
+ // No default case needed - all known SELECTOR_TYPES handled above
355
+ // Unknown types are silently ignored per CSS spec
314
356
  break;
315
357
  }
316
358
  }
@@ -337,6 +379,16 @@ export function compareSpecificity(spec1, spec2) {
337
379
  throw new Error("spec2 must be an array of 3 numbers");
338
380
  }
339
381
 
382
+ // Validate all elements are valid numbers
383
+ for (let i = 0; i < 3; i++) {
384
+ if (typeof spec1[i] !== "number" || !Number.isFinite(spec1[i]) || spec1[i] < 0) {
385
+ throw new Error(`spec1[${i}] must be a non-negative finite number`);
386
+ }
387
+ if (typeof spec2[i] !== "number" || !Number.isFinite(spec2[i]) || spec2[i] < 0) {
388
+ throw new Error(`spec2[${i}] must be a non-negative finite number`);
389
+ }
390
+ }
391
+
340
392
  // Compare lexicographically: a first, then b, then c
341
393
  for (let i = 0; i < 3; i++) {
342
394
  if (spec1[i] < spec2[i]) return -1;
@@ -400,7 +452,13 @@ export function stringifySelector(components) {
400
452
  }
401
453
 
402
454
  return components
403
- .map((component) => {
455
+ .map((component, index) => {
456
+ if (!component || typeof component.type !== "string") {
457
+ throw new Error(`Component at index ${index} must have a 'type' property`);
458
+ }
459
+ if (component.value === undefined) {
460
+ throw new Error(`Component at index ${index} must have a 'value' property`);
461
+ }
404
462
  switch (component.type) {
405
463
  case SELECTOR_TYPES.ID:
406
464
  return `#${component.value}`;
@@ -417,6 +475,7 @@ export function stringifySelector(components) {
417
475
  case SELECTOR_TYPES.UNIVERSAL:
418
476
  return "*";
419
477
  default:
478
+ // Unknown types stringify to empty string for graceful degradation
420
479
  return "";
421
480
  }
422
481
  })
@@ -431,6 +490,10 @@ export function stringifySelector(components) {
431
490
  * @returns {boolean} True if round-trip matches
432
491
  */
433
492
  export function verifySelector(selector) {
493
+ if (typeof selector !== "string") {
494
+ throw new Error("Selector must be a string");
495
+ }
496
+
434
497
  const components = parseSelector(selector);
435
498
  const reconstructed = stringifySelector(components);
436
499
 
@@ -18,6 +18,24 @@
18
18
  * @returns {number} Perpendicular distance
19
19
  */
20
20
  export function perpendicularDistance(point, lineStart, lineEnd) {
21
+ // Validate parameters to prevent undefined access and ensure numeric properties
22
+ if (!point || typeof point.x !== 'number' || typeof point.y !== 'number') {
23
+ throw new TypeError('perpendicularDistance: point must be an object with numeric x and y properties');
24
+ }
25
+ if (!lineStart || typeof lineStart.x !== 'number' || typeof lineStart.y !== 'number') {
26
+ throw new TypeError('perpendicularDistance: lineStart must be an object with numeric x and y properties');
27
+ }
28
+ if (!lineEnd || typeof lineEnd.x !== 'number' || typeof lineEnd.y !== 'number') {
29
+ throw new TypeError('perpendicularDistance: lineEnd must be an object with numeric x and y properties');
30
+ }
31
+
32
+ // Check for NaN/Infinity in coordinates to prevent invalid calculations
33
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y) ||
34
+ !Number.isFinite(lineStart.x) || !Number.isFinite(lineStart.y) ||
35
+ !Number.isFinite(lineEnd.x) || !Number.isFinite(lineEnd.y)) {
36
+ throw new RangeError('perpendicularDistance: all coordinates must be finite numbers');
37
+ }
38
+
21
39
  const dx = lineEnd.x - lineStart.x;
22
40
  const dy = lineEnd.y - lineStart.y;
23
41
 
@@ -46,6 +64,36 @@ export function perpendicularDistance(point, lineStart, lineEnd) {
46
64
  * @returns {Array<{x: number, y: number}>} Simplified points
47
65
  */
48
66
  export function douglasPeucker(points, tolerance) {
67
+ // Validate points parameter to prevent crashes on invalid input
68
+ if (!Array.isArray(points)) {
69
+ throw new TypeError('douglasPeucker: points must be an array');
70
+ }
71
+ if (points.length === 0) {
72
+ throw new RangeError('douglasPeucker: points array cannot be empty');
73
+ }
74
+
75
+ // Validate each point has x and y properties with finite numeric values
76
+ for (let i = 0; i < points.length; i++) {
77
+ const p = points[i];
78
+ if (!p || typeof p !== 'object') {
79
+ throw new TypeError(`douglasPeucker: point at index ${i} must be an object`);
80
+ }
81
+ if (typeof p.x !== 'number' || !Number.isFinite(p.x)) {
82
+ throw new TypeError(`douglasPeucker: point at index ${i} must have a finite numeric x property`);
83
+ }
84
+ if (typeof p.y !== 'number' || !Number.isFinite(p.y)) {
85
+ throw new TypeError(`douglasPeucker: point at index ${i} must have a finite numeric y property`);
86
+ }
87
+ }
88
+
89
+ // Validate tolerance parameter to ensure valid numeric simplification threshold
90
+ if (typeof tolerance !== 'number' || !Number.isFinite(tolerance)) {
91
+ throw new TypeError('douglasPeucker: tolerance must be a finite number');
92
+ }
93
+ if (tolerance < 0) {
94
+ throw new RangeError('douglasPeucker: tolerance cannot be negative');
95
+ }
96
+
49
97
  if (points.length <= 2) {
50
98
  return points;
51
99
  }
@@ -90,6 +138,36 @@ export function douglasPeucker(points, tolerance) {
90
138
  * @returns {Array<{x: number, y: number}>} Simplified points
91
139
  */
92
140
  export function visvalingamWhyatt(points, minArea) {
141
+ // Validate points parameter to prevent crashes on invalid input
142
+ if (!Array.isArray(points)) {
143
+ throw new TypeError('visvalingamWhyatt: points must be an array');
144
+ }
145
+ if (points.length === 0) {
146
+ throw new RangeError('visvalingamWhyatt: points array cannot be empty');
147
+ }
148
+
149
+ // Validate each point has x and y properties with finite numeric values
150
+ for (let i = 0; i < points.length; i++) {
151
+ const p = points[i];
152
+ if (!p || typeof p !== 'object') {
153
+ throw new TypeError(`visvalingamWhyatt: point at index ${i} must be an object`);
154
+ }
155
+ if (typeof p.x !== 'number' || !Number.isFinite(p.x)) {
156
+ throw new TypeError(`visvalingamWhyatt: point at index ${i} must have a finite numeric x property`);
157
+ }
158
+ if (typeof p.y !== 'number' || !Number.isFinite(p.y)) {
159
+ throw new TypeError(`visvalingamWhyatt: point at index ${i} must have a finite numeric y property`);
160
+ }
161
+ }
162
+
163
+ // Validate minArea parameter to ensure valid numeric threshold
164
+ if (typeof minArea !== 'number' || !Number.isFinite(minArea)) {
165
+ throw new TypeError('visvalingamWhyatt: minArea must be a finite number');
166
+ }
167
+ if (minArea < 0) {
168
+ throw new RangeError('visvalingamWhyatt: minArea cannot be negative');
169
+ }
170
+
93
171
  if (points.length <= 2) {
94
172
  return points;
95
173
  }
@@ -171,6 +249,31 @@ export function visvalingamWhyatt(points, minArea) {
171
249
  * @returns {Array<{x: number, y: number}>} Simplified points
172
250
  */
173
251
  export function simplifyPolyline(points, tolerance, algorithm = 'douglas-peucker') {
252
+ // Validate points parameter to prevent crashes on invalid input
253
+ if (!Array.isArray(points)) {
254
+ throw new TypeError('simplifyPolyline: points must be an array');
255
+ }
256
+ if (points.length === 0) {
257
+ throw new RangeError('simplifyPolyline: points array cannot be empty');
258
+ }
259
+
260
+ // Validate tolerance parameter to ensure valid numeric threshold
261
+ if (typeof tolerance !== 'number' || !Number.isFinite(tolerance)) {
262
+ throw new TypeError('simplifyPolyline: tolerance must be a finite number');
263
+ }
264
+ if (tolerance < 0) {
265
+ throw new RangeError('simplifyPolyline: tolerance cannot be negative');
266
+ }
267
+
268
+ // Validate algorithm parameter to ensure only valid algorithms are used
269
+ if (typeof algorithm !== 'string') {
270
+ throw new TypeError('simplifyPolyline: algorithm must be a string');
271
+ }
272
+ const validAlgorithms = ['douglas-peucker', 'visvalingam'];
273
+ if (!validAlgorithms.includes(algorithm)) {
274
+ throw new RangeError(`simplifyPolyline: algorithm must be one of: ${validAlgorithms.join(', ')}`);
275
+ }
276
+
174
277
  if (algorithm === 'visvalingam') {
175
278
  // For Visvalingam, tolerance is the minimum triangle area
176
279
  return visvalingamWhyatt(points, tolerance * tolerance);
@@ -184,43 +287,91 @@ export function simplifyPolyline(points, tolerance, algorithm = 'douglas-peucker
184
287
  * @returns {Array<{x: number, y: number}>} Extracted points
185
288
  */
186
289
  export function extractPolylinePoints(commands) {
290
+ // Validate commands parameter to prevent crashes on invalid input
291
+ if (!Array.isArray(commands)) {
292
+ throw new TypeError('extractPolylinePoints: commands must be an array');
293
+ }
294
+
187
295
  const points = [];
188
296
  let cx = 0, cy = 0;
189
297
  let startX = 0, startY = 0;
190
298
 
191
- for (const { command, args } of commands) {
299
+ for (const cmd of commands) {
300
+ // Validate each command object to prevent undefined access
301
+ if (!cmd || typeof cmd !== 'object') {
302
+ throw new TypeError('extractPolylinePoints: each command must be an object');
303
+ }
304
+ if (typeof cmd.command !== 'string') {
305
+ throw new TypeError('extractPolylinePoints: each command must have a string "command" property');
306
+ }
307
+ if (!Array.isArray(cmd.args)) {
308
+ throw new TypeError('extractPolylinePoints: each command must have an "args" array');
309
+ }
310
+
311
+ const { command, args } = cmd;
312
+
313
+ // Helper to validate args length to prevent out-of-bounds access
314
+ const requireArgs = (count) => {
315
+ if (args.length < count) {
316
+ throw new RangeError(`extractPolylinePoints: command "${command}" requires at least ${count} arguments, got ${args.length}`);
317
+ }
318
+ };
319
+
320
+ // Helper to validate arg is a finite number to prevent NaN/Infinity in calculations
321
+ const requireFiniteNumber = (index) => {
322
+ if (typeof args[index] !== 'number' || !Number.isFinite(args[index])) {
323
+ throw new TypeError(`extractPolylinePoints: command "${command}" argument at index ${index} must be a finite number, got ${args[index]}`);
324
+ }
325
+ };
326
+
192
327
  switch (command) {
193
328
  case 'M':
329
+ requireArgs(2);
330
+ requireFiniteNumber(0); requireFiniteNumber(1);
194
331
  cx = args[0]; cy = args[1];
195
332
  startX = cx; startY = cy;
196
333
  points.push({ x: cx, y: cy });
197
334
  break;
198
335
  case 'm':
336
+ requireArgs(2);
337
+ requireFiniteNumber(0); requireFiniteNumber(1);
199
338
  cx += args[0]; cy += args[1];
200
339
  startX = cx; startY = cy;
201
340
  points.push({ x: cx, y: cy });
202
341
  break;
203
342
  case 'L':
343
+ requireArgs(2);
344
+ requireFiniteNumber(0); requireFiniteNumber(1);
204
345
  cx = args[0]; cy = args[1];
205
346
  points.push({ x: cx, y: cy });
206
347
  break;
207
348
  case 'l':
349
+ requireArgs(2);
350
+ requireFiniteNumber(0); requireFiniteNumber(1);
208
351
  cx += args[0]; cy += args[1];
209
352
  points.push({ x: cx, y: cy });
210
353
  break;
211
354
  case 'H':
355
+ requireArgs(1);
356
+ requireFiniteNumber(0);
212
357
  cx = args[0];
213
358
  points.push({ x: cx, y: cy });
214
359
  break;
215
360
  case 'h':
361
+ requireArgs(1);
362
+ requireFiniteNumber(0);
216
363
  cx += args[0];
217
364
  points.push({ x: cx, y: cy });
218
365
  break;
219
366
  case 'V':
367
+ requireArgs(1);
368
+ requireFiniteNumber(0);
220
369
  cy = args[0];
221
370
  points.push({ x: cx, y: cy });
222
371
  break;
223
372
  case 'v':
373
+ requireArgs(1);
374
+ requireFiniteNumber(0);
224
375
  cy += args[0];
225
376
  points.push({ x: cx, y: cy });
226
377
  break;
@@ -233,21 +384,39 @@ export function extractPolylinePoints(commands) {
233
384
  break;
234
385
  // For curves (C, S, Q, T, A), we just track the endpoint
235
386
  case 'C':
387
+ requireArgs(6);
388
+ requireFiniteNumber(4); requireFiniteNumber(5);
236
389
  cx = args[4]; cy = args[5]; break;
237
390
  case 'c':
391
+ requireArgs(6);
392
+ requireFiniteNumber(4); requireFiniteNumber(5);
238
393
  cx += args[4]; cy += args[5]; break;
239
394
  case 'S': case 'Q':
395
+ requireArgs(4);
396
+ requireFiniteNumber(2); requireFiniteNumber(3);
240
397
  cx = args[2]; cy = args[3]; break;
241
398
  case 's': case 'q':
399
+ requireArgs(4);
400
+ requireFiniteNumber(2); requireFiniteNumber(3);
242
401
  cx += args[2]; cy += args[3]; break;
243
402
  case 'T':
403
+ requireArgs(2);
404
+ requireFiniteNumber(0); requireFiniteNumber(1);
244
405
  cx = args[0]; cy = args[1]; break;
245
406
  case 't':
407
+ requireArgs(2);
408
+ requireFiniteNumber(0); requireFiniteNumber(1);
246
409
  cx += args[0]; cy += args[1]; break;
247
410
  case 'A':
411
+ requireArgs(7);
412
+ requireFiniteNumber(5); requireFiniteNumber(6);
248
413
  cx = args[5]; cy = args[6]; break;
249
414
  case 'a':
415
+ requireArgs(7);
416
+ requireFiniteNumber(5); requireFiniteNumber(6);
250
417
  cx += args[5]; cy += args[6]; break;
418
+ default:
419
+ break;
251
420
  }
252
421
  }
253
422
 
@@ -261,8 +430,32 @@ export function extractPolylinePoints(commands) {
261
430
  * @returns {Array<{command: string, args: number[]}>} Path commands
262
431
  */
263
432
  export function rebuildPathFromPoints(points, closed = false) {
433
+ // Validate points parameter to prevent crashes on invalid input
434
+ if (!Array.isArray(points)) {
435
+ throw new TypeError('rebuildPathFromPoints: points must be an array');
436
+ }
437
+
264
438
  if (points.length === 0) return [];
265
439
 
440
+ // Validate closed parameter to ensure boolean type
441
+ if (typeof closed !== 'boolean') {
442
+ throw new TypeError('rebuildPathFromPoints: closed must be a boolean');
443
+ }
444
+
445
+ // Validate each point has x and y properties with finite numeric values
446
+ for (let i = 0; i < points.length; i++) {
447
+ const p = points[i];
448
+ if (!p || typeof p !== 'object') {
449
+ throw new TypeError(`rebuildPathFromPoints: point at index ${i} must be an object`);
450
+ }
451
+ if (typeof p.x !== 'number' || !Number.isFinite(p.x)) {
452
+ throw new TypeError(`rebuildPathFromPoints: point at index ${i} must have a finite numeric x property`);
453
+ }
454
+ if (typeof p.y !== 'number' || !Number.isFinite(p.y)) {
455
+ throw new TypeError(`rebuildPathFromPoints: point at index ${i} must have a finite numeric y property`);
456
+ }
457
+ }
458
+
266
459
  const commands = [];
267
460
 
268
461
  // First point is M
@@ -286,8 +479,19 @@ export function rebuildPathFromPoints(points, closed = false) {
286
479
  * @returns {boolean} True if pure polyline
287
480
  */
288
481
  export function isPurePolyline(commands) {
482
+ // Validate commands parameter to prevent crashes on invalid input
483
+ if (!Array.isArray(commands)) {
484
+ throw new TypeError('isPurePolyline: commands must be an array');
485
+ }
486
+
289
487
  const polylineCommands = new Set(['M', 'm', 'L', 'l', 'H', 'h', 'V', 'v', 'Z', 'z']);
290
- return commands.every(cmd => polylineCommands.has(cmd.command));
488
+ return commands.every(cmd => {
489
+ // Validate each command has the required structure to prevent undefined access
490
+ if (!cmd || typeof cmd !== 'object' || typeof cmd.command !== 'string') {
491
+ return false;
492
+ }
493
+ return polylineCommands.has(cmd.command);
494
+ });
291
495
  }
292
496
 
293
497
  /**
@@ -298,6 +502,19 @@ export function isPurePolyline(commands) {
298
502
  * @returns {{commands: Array<{command: string, args: number[]}>, simplified: boolean, originalPoints: number, simplifiedPoints: number}}
299
503
  */
300
504
  export function simplifyPath(commands, tolerance, algorithm = 'douglas-peucker') {
505
+ // Validate commands parameter to prevent crashes on invalid input
506
+ if (!Array.isArray(commands)) {
507
+ throw new TypeError('simplifyPath: commands must be an array');
508
+ }
509
+
510
+ // Validate tolerance parameter to ensure valid numeric threshold
511
+ if (typeof tolerance !== 'number' || !Number.isFinite(tolerance)) {
512
+ throw new TypeError('simplifyPath: tolerance must be a finite number');
513
+ }
514
+ if (tolerance < 0) {
515
+ throw new RangeError('simplifyPath: tolerance cannot be negative');
516
+ }
517
+
301
518
  if (!isPurePolyline(commands) || commands.length < 3) {
302
519
  return {
303
520
  commands,