@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
@@ -131,6 +131,9 @@ export const MaskType = {
131
131
  * console.log(`Number of shapes: ${maskData.children.length}`);
132
132
  */
133
133
  export function parseMaskElement(maskElement) {
134
+ if (!maskElement) throw new Error('parseMaskElement: maskElement is required');
135
+ if (!maskElement.getAttribute) throw new Error('parseMaskElement: maskElement must be a DOM element');
136
+
134
137
  const data = {
135
138
  id: maskElement.getAttribute("id") || "",
136
139
  maskUnits: maskElement.getAttribute("maskUnits") || "objectBoundingBox",
@@ -148,32 +151,42 @@ export function parseMaskElement(maskElement) {
148
151
  children: [],
149
152
  };
150
153
 
154
+ // Helper to safely parse float with NaN check
155
+ const safeParseFloat = (value, defaultValue) => {
156
+ const parsed = parseFloat(value);
157
+ return isNaN(parsed) ? defaultValue : parsed;
158
+ };
159
+
151
160
  // Set defaults based on maskUnits
152
161
  if (data.maskUnits === "objectBoundingBox") {
153
- data.x = data.x !== null ? parseFloat(data.x) : DEFAULT_MASK_X;
154
- data.y = data.y !== null ? parseFloat(data.y) : DEFAULT_MASK_Y;
155
- data.width =
156
- data.width !== null ? parseFloat(data.width) : DEFAULT_MASK_WIDTH;
157
- data.height =
158
- data.height !== null ? parseFloat(data.height) : DEFAULT_MASK_HEIGHT;
162
+ data.x = data.x !== null ? safeParseFloat(data.x, DEFAULT_MASK_X) : DEFAULT_MASK_X;
163
+ data.y = data.y !== null ? safeParseFloat(data.y, DEFAULT_MASK_Y) : DEFAULT_MASK_Y;
164
+ data.width = data.width !== null ? safeParseFloat(data.width, DEFAULT_MASK_WIDTH) : DEFAULT_MASK_WIDTH;
165
+ data.height = data.height !== null ? safeParseFloat(data.height, DEFAULT_MASK_HEIGHT) : DEFAULT_MASK_HEIGHT;
159
166
  } else {
160
- data.x = data.x !== null ? parseFloat(data.x) : null;
161
- data.y = data.y !== null ? parseFloat(data.y) : null;
162
- data.width = data.width !== null ? parseFloat(data.width) : null;
163
- data.height = data.height !== null ? parseFloat(data.height) : null;
167
+ data.x = data.x !== null ? safeParseFloat(data.x, null) : null;
168
+ data.y = data.y !== null ? safeParseFloat(data.y, null) : null;
169
+ data.width = data.width !== null ? safeParseFloat(data.width, null) : null;
170
+ data.height = data.height !== null ? safeParseFloat(data.height, null) : null;
164
171
  }
165
172
 
166
173
  // Parse child elements
174
+ if (!maskElement.children) return data;
175
+
167
176
  for (const child of maskElement.children) {
177
+ if (!child.tagName) continue;
178
+
168
179
  const tagName = child.tagName.toLowerCase();
169
180
  const childData = {
170
181
  type: tagName,
171
182
  fill: child.getAttribute("fill") || child.style?.fill || "black",
172
- fillOpacity: parseFloat(
183
+ fillOpacity: safeParseFloat(
173
184
  child.getAttribute("fill-opacity") || child.style?.fillOpacity || "1",
185
+ 1
174
186
  ),
175
- opacity: parseFloat(
187
+ opacity: safeParseFloat(
176
188
  child.getAttribute("opacity") || child.style?.opacity || "1",
189
+ 1
177
190
  ),
178
191
  transform: child.getAttribute("transform") || null,
179
192
  };
@@ -181,23 +194,23 @@ export function parseMaskElement(maskElement) {
181
194
  // Parse shape-specific attributes
182
195
  switch (tagName) {
183
196
  case "rect":
184
- childData.x = parseFloat(child.getAttribute("x") || "0");
185
- childData.y = parseFloat(child.getAttribute("y") || "0");
186
- childData.width = parseFloat(child.getAttribute("width") || "0");
187
- childData.height = parseFloat(child.getAttribute("height") || "0");
188
- childData.rx = parseFloat(child.getAttribute("rx") || "0");
189
- childData.ry = parseFloat(child.getAttribute("ry") || "0");
197
+ childData.x = safeParseFloat(child.getAttribute("x") || "0", 0);
198
+ childData.y = safeParseFloat(child.getAttribute("y") || "0", 0);
199
+ childData.width = safeParseFloat(child.getAttribute("width") || "0", 0);
200
+ childData.height = safeParseFloat(child.getAttribute("height") || "0", 0);
201
+ childData.rx = safeParseFloat(child.getAttribute("rx") || "0", 0);
202
+ childData.ry = safeParseFloat(child.getAttribute("ry") || "0", 0);
190
203
  break;
191
204
  case "circle":
192
- childData.cx = parseFloat(child.getAttribute("cx") || "0");
193
- childData.cy = parseFloat(child.getAttribute("cy") || "0");
194
- childData.r = parseFloat(child.getAttribute("r") || "0");
205
+ childData.cx = safeParseFloat(child.getAttribute("cx") || "0", 0);
206
+ childData.cy = safeParseFloat(child.getAttribute("cy") || "0", 0);
207
+ childData.r = safeParseFloat(child.getAttribute("r") || "0", 0);
195
208
  break;
196
209
  case "ellipse":
197
- childData.cx = parseFloat(child.getAttribute("cx") || "0");
198
- childData.cy = parseFloat(child.getAttribute("cy") || "0");
199
- childData.rx = parseFloat(child.getAttribute("rx") || "0");
200
- childData.ry = parseFloat(child.getAttribute("ry") || "0");
210
+ childData.cx = safeParseFloat(child.getAttribute("cx") || "0", 0);
211
+ childData.cy = safeParseFloat(child.getAttribute("cy") || "0", 0);
212
+ childData.rx = safeParseFloat(child.getAttribute("rx") || "0", 0);
213
+ childData.ry = safeParseFloat(child.getAttribute("ry") || "0", 0);
201
214
  break;
202
215
  case "path":
203
216
  childData.d = child.getAttribute("d") || "";
@@ -211,14 +224,20 @@ export function parseMaskElement(maskElement) {
211
224
  case "g":
212
225
  // Recursively parse group children
213
226
  childData.children = [];
214
- for (const gc of child.children) {
215
- // Simplified - just store the tag and basic info
216
- childData.children.push({
217
- type: gc.tagName.toLowerCase(),
218
- fill: gc.getAttribute("fill") || "inherit",
219
- });
227
+ if (child.children) {
228
+ for (const gc of child.children) {
229
+ if (gc.tagName) {
230
+ childData.children.push({
231
+ type: gc.tagName.toLowerCase(),
232
+ fill: gc.getAttribute("fill") || "inherit",
233
+ });
234
+ }
235
+ }
220
236
  }
221
237
  break;
238
+ default:
239
+ // Unknown element type - childData will have basic properties only (type, fill, opacity, transform)
240
+ break;
222
241
  }
223
242
 
224
243
  data.children.push(childData);
@@ -273,6 +292,20 @@ export function parseMaskElement(maskElement) {
273
292
  * // region.y = 50 (absolute)
274
293
  */
275
294
  export function getMaskRegion(maskData, targetBBox) {
295
+ if (!maskData) throw new Error('getMaskRegion: maskData is required');
296
+ if (!targetBBox) throw new Error('getMaskRegion: targetBBox is required');
297
+ if (typeof targetBBox.x !== 'number' || typeof targetBBox.y !== 'number' ||
298
+ typeof targetBBox.width !== 'number' || typeof targetBBox.height !== 'number') {
299
+ throw new Error('getMaskRegion: targetBBox must have numeric x, y, width, height properties');
300
+ }
301
+ if (targetBBox.width <= 0 || targetBBox.height <= 0) {
302
+ throw new Error('getMaskRegion: targetBBox width and height must be positive');
303
+ }
304
+ if (!maskData.maskUnits) throw new Error('getMaskRegion: maskData.maskUnits is required');
305
+ if (maskData.x === undefined || maskData.y === undefined || maskData.width === undefined || maskData.height === undefined) {
306
+ throw new Error('getMaskRegion: maskData must have x, y, width, height properties');
307
+ }
308
+
276
309
  if (maskData.maskUnits === "objectBoundingBox") {
277
310
  // Coordinates are relative to target bounding box
278
311
  return {
@@ -340,6 +373,17 @@ export function maskChildToPolygon(
340
373
  contentUnits,
341
374
  samples = 20,
342
375
  ) {
376
+ if (!child) throw new Error('maskChildToPolygon: child is required');
377
+ if (!targetBBox) throw new Error('maskChildToPolygon: targetBBox is required');
378
+ if (!contentUnits) throw new Error('maskChildToPolygon: contentUnits is required');
379
+ if (typeof samples !== 'number' || samples <= 0) {
380
+ throw new Error('maskChildToPolygon: samples must be a positive number');
381
+ }
382
+ if (typeof targetBBox.x !== 'number' || typeof targetBBox.y !== 'number' ||
383
+ typeof targetBBox.width !== 'number' || typeof targetBBox.height !== 'number') {
384
+ throw new Error('maskChildToPolygon: targetBBox must have numeric x, y, width, height properties');
385
+ }
386
+
343
387
  // Create element-like object for ClipPathResolver
344
388
  const element = {
345
389
  type: child.type,
@@ -350,8 +394,16 @@ export function maskChildToPolygon(
350
394
  // Get polygon using ClipPathResolver
351
395
  let polygon = ClipPathResolver.shapeToPolygon(element, null, samples);
352
396
 
397
+ // Validate polygon is an array
398
+ if (!Array.isArray(polygon)) {
399
+ return []; // Return empty array if polygon conversion failed
400
+ }
401
+
353
402
  // Apply objectBoundingBox scaling if needed
354
403
  if (contentUnits === "objectBoundingBox" && polygon.length > 0) {
404
+ if (targetBBox.width === 0 || targetBBox.height === 0) {
405
+ return []; // Cannot scale to zero-size bbox
406
+ }
355
407
  polygon = polygon.map((p) => ({
356
408
  x: D(targetBBox.x).plus(p.x.mul(targetBBox.width)),
357
409
  y: D(targetBBox.y).plus(p.y.mul(targetBBox.height)),
@@ -412,13 +464,23 @@ export function colorToLuminance(colorStr) {
412
464
  return 0;
413
465
  }
414
466
 
467
+ if (typeof colorStr !== 'string') {
468
+ return 1; // Default to opaque if not a string
469
+ }
470
+
415
471
  // Parse RGB values
416
472
  const rgbMatch = colorStr.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
417
473
  if (rgbMatch) {
418
- const r = parseInt(rgbMatch[1], 10) / 255;
419
- const g = parseInt(rgbMatch[2], 10) / 255;
420
- const b = parseInt(rgbMatch[3], 10) / 255;
421
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
474
+ const r = parseInt(rgbMatch[1], 10);
475
+ const g = parseInt(rgbMatch[2], 10);
476
+ const b = parseInt(rgbMatch[3], 10);
477
+ // Validate parsed values and clamp to 0-255 range
478
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return 1;
479
+ const rClamped = Math.max(0, Math.min(255, r)) / 255;
480
+ const gClamped = Math.max(0, Math.min(255, g)) / 255;
481
+ const bClamped = Math.max(0, Math.min(255, b)) / 255;
482
+ const luminance = 0.2126 * rClamped + 0.7152 * gClamped + 0.0722 * bClamped;
483
+ return isNaN(luminance) ? 1 : luminance;
422
484
  }
423
485
 
424
486
  // Parse hex colors
@@ -428,10 +490,13 @@ export function colorToLuminance(colorStr) {
428
490
  if (hex.length === 3) {
429
491
  hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
430
492
  }
431
- const r = parseInt(hex.slice(0, 2), 16) / 255;
432
- const g = parseInt(hex.slice(2, 4), 16) / 255;
433
- const b = parseInt(hex.slice(4, 6), 16) / 255;
434
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
493
+ const r = parseInt(hex.slice(0, 2), 16);
494
+ const g = parseInt(hex.slice(2, 4), 16);
495
+ const b = parseInt(hex.slice(4, 6), 16);
496
+ // Validate parsed values
497
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return 1;
498
+ const luminance = 0.2126 * (r / 255) + 0.7152 * (g / 255) + 0.0722 * (b / 255);
499
+ return isNaN(luminance) ? 1 : luminance;
435
500
  }
436
501
 
437
502
  // Named colors (common ones)
@@ -499,8 +564,14 @@ export function colorToLuminance(colorStr) {
499
564
  * // Returns: 1.0 × 1.0 × 0.0 = 0.0 (black = fully transparent in luminance mask)
500
565
  */
501
566
  export function getMaskChildOpacity(child, maskType) {
502
- const fillOpacity = child.fillOpacity || 1;
503
- const opacity = child.opacity || 1;
567
+ if (!child) throw new Error('getMaskChildOpacity: child is required');
568
+ if (!maskType) throw new Error('getMaskChildOpacity: maskType is required');
569
+ if (maskType !== MaskType.LUMINANCE && maskType !== MaskType.ALPHA) {
570
+ throw new Error('getMaskChildOpacity: maskType must be "luminance" or "alpha"');
571
+ }
572
+
573
+ const fillOpacity = typeof child.fillOpacity === 'number' && !isNaN(child.fillOpacity) ? child.fillOpacity : 1;
574
+ const opacity = typeof child.opacity === 'number' && !isNaN(child.opacity) ? child.opacity : 1;
504
575
  const baseOpacity = fillOpacity * opacity;
505
576
 
506
577
  if (maskType === MaskType.ALPHA) {
@@ -509,7 +580,8 @@ export function getMaskChildOpacity(child, maskType) {
509
580
 
510
581
  // Luminance mask: multiply by luminance of fill color
511
582
  const luminance = colorToLuminance(child.fill);
512
- return baseOpacity * luminance;
583
+ const result = baseOpacity * luminance;
584
+ return isNaN(result) ? 0 : result;
513
585
  }
514
586
 
515
587
  /**
@@ -554,11 +626,21 @@ export function getMaskChildOpacity(child, maskType) {
554
626
  * });
555
627
  */
556
628
  export function resolveMask(maskData, targetBBox, options = {}) {
629
+ if (!maskData) throw new Error('resolveMask: maskData is required');
630
+ if (!targetBBox) throw new Error('resolveMask: targetBBox is required');
631
+ if (!Array.isArray(maskData.children)) throw new Error('resolveMask: maskData.children must be an array');
632
+
557
633
  const { samples = 20 } = options;
634
+ if (typeof samples !== 'number' || samples <= 0) {
635
+ throw new Error('resolveMask: samples must be a positive number');
636
+ }
637
+
558
638
  const maskType = maskData.maskType || MaskType.LUMINANCE;
559
639
  const result = [];
560
640
 
561
641
  for (const child of maskData.children) {
642
+ if (!child) continue; // Skip null/undefined children
643
+
562
644
  const polygon = maskChildToPolygon(
563
645
  child,
564
646
  targetBBox,
@@ -630,11 +712,18 @@ export function resolveMask(maskData, targetBBox, options = {}) {
630
712
  * });
631
713
  */
632
714
  export function applyMask(targetPolygon, maskData, targetBBox, options = {}) {
715
+ if (!targetPolygon) throw new Error('applyMask: targetPolygon is required');
716
+ if (!Array.isArray(targetPolygon)) throw new Error('applyMask: targetPolygon must be an array');
717
+ if (targetPolygon.length === 0) return []; // Empty polygon returns empty result
718
+ if (!maskData) throw new Error('applyMask: maskData is required');
719
+ if (!targetBBox) throw new Error('applyMask: targetBBox is required');
720
+
633
721
  const maskRegions = resolveMask(maskData, targetBBox, options);
634
722
  const result = [];
635
723
 
636
724
  for (const { polygon: maskPoly, opacity } of maskRegions) {
637
725
  if (opacity <= 0) continue;
726
+ if (!maskPoly || maskPoly.length < 3) continue; // Skip invalid mask polygons
638
727
 
639
728
  const intersection = PolygonClip.polygonIntersection(
640
729
  targetPolygon,
@@ -705,13 +794,19 @@ export function maskToClipPath(
705
794
  opacityThreshold = 0.5,
706
795
  options = {},
707
796
  ) {
797
+ if (!maskData) throw new Error('maskToClipPath: maskData is required');
798
+ if (!targetBBox) throw new Error('maskToClipPath: targetBBox is required');
799
+ if (typeof opacityThreshold !== 'number' || opacityThreshold < 0 || opacityThreshold > 1) {
800
+ throw new Error('maskToClipPath: opacityThreshold must be a number between 0 and 1');
801
+ }
802
+
708
803
  const maskRegions = resolveMask(maskData, targetBBox, options);
709
804
 
710
805
  // Union all regions above threshold
711
806
  let result = [];
712
807
 
713
808
  for (const { polygon, opacity } of maskRegions) {
714
- if (opacity >= opacityThreshold && polygon.length >= 3) {
809
+ if (opacity >= opacityThreshold && polygon && polygon.length >= 3) {
715
810
  if (result.length === 0) {
716
811
  result = polygon;
717
812
  } else {
@@ -767,6 +862,9 @@ export function maskToClipPath(
767
862
  * `;
768
863
  */
769
864
  export function maskToPathData(maskData, targetBBox, options = {}) {
865
+ if (!maskData) throw new Error('maskToPathData: maskData is required');
866
+ if (!targetBBox) throw new Error('maskToPathData: targetBBox is required');
867
+
770
868
  const polygon = maskToClipPath(maskData, targetBBox, 0.5, options);
771
869
 
772
870
  if (polygon.length < 3) return "";
@@ -774,8 +872,12 @@ export function maskToPathData(maskData, targetBBox, options = {}) {
774
872
  let d = "";
775
873
  for (let i = 0; i < polygon.length; i++) {
776
874
  const p = polygon[i];
777
- const x = Number(p.x).toFixed(6);
778
- const y = Number(p.y).toFixed(6);
875
+ if (!p || p.x === undefined || p.y === undefined) continue; // Skip invalid points
876
+ const xNum = Number(p.x);
877
+ const yNum = Number(p.y);
878
+ if (isNaN(xNum) || isNaN(yNum) || !isFinite(xNum) || !isFinite(yNum)) continue; // Skip NaN/Infinity
879
+ const x = xNum.toFixed(6);
880
+ const y = yNum.toFixed(6);
779
881
  d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
780
882
  }
781
883
  d += " Z";
@@ -860,10 +962,19 @@ export function parseGradientReference(fill) {
860
962
  */
861
963
  export function rgbToLuminance(color) {
862
964
  if (!color) return 0;
863
- const r = (color.r || 0) / 255;
864
- const g = (color.g || 0) / 255;
865
- const b = (color.b || 0) / 255;
866
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
965
+ if (typeof color !== 'object') return 0;
966
+
967
+ // Validate and clamp RGB values to 0-255 range
968
+ const rVal = typeof color.r === 'number' ? Math.max(0, Math.min(255, color.r)) : 0;
969
+ const gVal = typeof color.g === 'number' ? Math.max(0, Math.min(255, color.g)) : 0;
970
+ const bVal = typeof color.b === 'number' ? Math.max(0, Math.min(255, color.b)) : 0;
971
+
972
+ const r = rVal / 255;
973
+ const g = gVal / 255;
974
+ const b = bVal / 255;
975
+
976
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
977
+ return isNaN(luminance) ? 0 : luminance;
867
978
  }
868
979
 
869
980
  /**
@@ -921,7 +1032,17 @@ export function sampleMeshGradientForMask(
921
1032
  maskType = "luminance",
922
1033
  options = {},
923
1034
  ) {
1035
+ if (!meshData) throw new Error('sampleMeshGradientForMask: meshData is required');
1036
+ if (!shapeBBox) throw new Error('sampleMeshGradientForMask: shapeBBox is required');
1037
+ if (maskType !== MaskType.LUMINANCE && maskType !== MaskType.ALPHA) {
1038
+ throw new Error('sampleMeshGradientForMask: maskType must be "luminance" or "alpha"');
1039
+ }
1040
+
924
1041
  const { subdivisions = 4 } = options;
1042
+ if (typeof subdivisions !== 'number' || subdivisions <= 0) {
1043
+ throw new Error('sampleMeshGradientForMask: subdivisions must be a positive number');
1044
+ }
1045
+
925
1046
  const result = [];
926
1047
 
927
1048
  // Get mesh gradient polygons with colors
@@ -929,19 +1050,23 @@ export function sampleMeshGradientForMask(
929
1050
  subdivisions,
930
1051
  });
931
1052
 
1053
+ if (!Array.isArray(meshPolygons)) return result;
1054
+
932
1055
  for (const { polygon, color } of meshPolygons) {
933
- if (polygon.length < 3) continue;
1056
+ if (!polygon || polygon.length < 3) continue;
1057
+ if (!color) continue;
934
1058
 
935
1059
  let opacity;
936
1060
  if (maskType === MaskType.ALPHA) {
937
1061
  // Alpha mask: use alpha channel
938
- opacity = (color.a || 255) / 255;
1062
+ const alphaVal = typeof color.a === 'number' ? Math.max(0, Math.min(255, color.a)) : 255;
1063
+ opacity = alphaVal / 255;
939
1064
  } else {
940
1065
  // Luminance mask: calculate from RGB
941
1066
  opacity = rgbToLuminance(color);
942
1067
  }
943
1068
 
944
- if (opacity > 0) {
1069
+ if (opacity > 0 && !isNaN(opacity)) {
945
1070
  result.push({ polygon, opacity });
946
1071
  }
947
1072
  }
@@ -1001,6 +1126,12 @@ export function applyMeshGradientMask(
1001
1126
  maskType = "luminance",
1002
1127
  options = {},
1003
1128
  ) {
1129
+ if (!targetPolygon) throw new Error('applyMeshGradientMask: targetPolygon is required');
1130
+ if (!Array.isArray(targetPolygon)) throw new Error('applyMeshGradientMask: targetPolygon must be an array');
1131
+ if (targetPolygon.length === 0) return []; // Empty polygon returns empty result
1132
+ if (!meshData) throw new Error('applyMeshGradientMask: meshData is required');
1133
+ if (!targetBBox) throw new Error('applyMeshGradientMask: targetBBox is required');
1134
+
1004
1135
  const meshMaskRegions = sampleMeshGradientForMask(
1005
1136
  meshData,
1006
1137
  targetBBox,
@@ -1011,14 +1142,17 @@ export function applyMeshGradientMask(
1011
1142
 
1012
1143
  for (const { polygon: maskPoly, opacity } of meshMaskRegions) {
1013
1144
  if (opacity <= 0) continue;
1145
+ if (!maskPoly || maskPoly.length < 3) continue; // Skip invalid mask polygons
1014
1146
 
1015
1147
  const intersection = PolygonClip.polygonIntersection(
1016
1148
  targetPolygon,
1017
1149
  maskPoly,
1018
1150
  );
1019
1151
 
1152
+ if (!Array.isArray(intersection)) continue;
1153
+
1020
1154
  for (const clippedPoly of intersection) {
1021
- if (clippedPoly.length >= 3) {
1155
+ if (clippedPoly && clippedPoly.length >= 3) {
1022
1156
  result.push({ polygon: clippedPoly, opacity });
1023
1157
  }
1024
1158
  }
@@ -1097,16 +1231,30 @@ export function resolveMaskWithGradients(
1097
1231
  gradientDefs = {},
1098
1232
  options = {},
1099
1233
  ) {
1234
+ if (!maskData) throw new Error('resolveMaskWithGradients: maskData is required');
1235
+ if (!targetBBox) throw new Error('resolveMaskWithGradients: targetBBox is required');
1236
+ if (!Array.isArray(maskData.children)) throw new Error('resolveMaskWithGradients: maskData.children must be an array');
1237
+
1100
1238
  const { samples = 20, subdivisions = 4 } = options;
1239
+ if (typeof samples !== 'number' || samples <= 0) {
1240
+ throw new Error('resolveMaskWithGradients: samples must be a positive number');
1241
+ }
1242
+ if (typeof subdivisions !== 'number' || subdivisions <= 0) {
1243
+ throw new Error('resolveMaskWithGradients: subdivisions must be a positive number');
1244
+ }
1245
+
1101
1246
  const maskType = maskData.maskType || MaskType.LUMINANCE;
1102
1247
  const result = [];
1103
1248
 
1104
1249
  for (const child of maskData.children) {
1250
+ if (!child) continue; // Skip null/undefined children
1251
+
1105
1252
  // Check if fill references a gradient
1106
1253
  const gradientId = parseGradientReference(child.fill);
1107
1254
 
1108
1255
  if (gradientId && gradientDefs[gradientId]) {
1109
1256
  const gradientData = gradientDefs[gradientId];
1257
+ if (!gradientData) continue;
1110
1258
 
1111
1259
  // Get the shape polygon first
1112
1260
  const shapePolygon = maskChildToPolygon(
@@ -1133,17 +1281,22 @@ export function resolveMaskWithGradients(
1133
1281
  polygon: meshPoly,
1134
1282
  opacity: meshOpacity,
1135
1283
  } of meshMaskRegions) {
1284
+ if (!meshPoly || meshPoly.length < 3) continue;
1285
+
1136
1286
  const clipped = PolygonClip.polygonIntersection(
1137
1287
  shapePolygon,
1138
1288
  meshPoly,
1139
1289
  );
1140
1290
 
1291
+ if (!Array.isArray(clipped)) continue;
1292
+
1141
1293
  for (const clippedPoly of clipped) {
1142
- if (clippedPoly.length >= 3) {
1294
+ if (clippedPoly && clippedPoly.length >= 3) {
1143
1295
  // Combine mesh opacity with child opacity
1144
- const combinedOpacity =
1145
- meshOpacity * (child.opacity || 1) * (child.fillOpacity || 1);
1146
- if (combinedOpacity > 0) {
1296
+ const childOpacity = typeof child.opacity === 'number' ? child.opacity : 1;
1297
+ const childFillOpacity = typeof child.fillOpacity === 'number' ? child.fillOpacity : 1;
1298
+ const combinedOpacity = meshOpacity * childOpacity * childFillOpacity;
1299
+ if (combinedOpacity > 0 && !isNaN(combinedOpacity)) {
1147
1300
  result.push({ polygon: clippedPoly, opacity: combinedOpacity });
1148
1301
  }
1149
1302
  }
@@ -1229,6 +1382,13 @@ export function createMeshGradientMask(
1229
1382
  maskType = "luminance",
1230
1383
  options = {},
1231
1384
  ) {
1385
+ if (!meshData) throw new Error('createMeshGradientMask: meshData is required');
1386
+ if (!bounds) throw new Error('createMeshGradientMask: bounds is required');
1387
+ if (typeof bounds !== 'object' || typeof bounds.x !== 'number' || typeof bounds.y !== 'number' ||
1388
+ typeof bounds.width !== 'number' || typeof bounds.height !== 'number') {
1389
+ throw new Error('createMeshGradientMask: bounds must have numeric x, y, width, height properties');
1390
+ }
1391
+
1232
1392
  const regions = sampleMeshGradientForMask(
1233
1393
  meshData,
1234
1394
  bounds,
@@ -1291,9 +1451,14 @@ export function createMeshGradientMask(
1291
1451
  * console.log(`Boundary has ${boundary.length} vertices`);
1292
1452
  */
1293
1453
  export function getMeshGradientBoundary(meshData, options = {}) {
1454
+ if (!meshData) throw new Error('getMeshGradientBoundary: meshData is required');
1455
+
1294
1456
  const { samples = 20 } = options;
1457
+ if (typeof samples !== 'number' || samples <= 0) {
1458
+ throw new Error('getMeshGradientBoundary: samples must be a positive number');
1459
+ }
1295
1460
 
1296
- if (!meshData.patches || meshData.patches.length === 0) {
1461
+ if (!meshData.patches || !Array.isArray(meshData.patches) || meshData.patches.length === 0) {
1297
1462
  return [];
1298
1463
  }
1299
1464
 
@@ -1302,25 +1467,35 @@ export function getMeshGradientBoundary(meshData, options = {}) {
1302
1467
  const allPoints = [];
1303
1468
 
1304
1469
  for (const patch of meshData.patches) {
1470
+ if (!patch) continue; // Skip null/undefined patches
1471
+
1305
1472
  // Sample each edge of the patch
1306
1473
  if (patch.top) {
1307
1474
  const topPoints = MeshGradient.sampleBezierCurve(patch.top, samples);
1308
- allPoints.push(...topPoints);
1475
+ if (Array.isArray(topPoints)) {
1476
+ allPoints.push(...topPoints);
1477
+ }
1309
1478
  }
1310
1479
  if (patch.right) {
1311
1480
  const rightPoints = MeshGradient.sampleBezierCurve(patch.right, samples);
1312
- allPoints.push(...rightPoints);
1481
+ if (Array.isArray(rightPoints)) {
1482
+ allPoints.push(...rightPoints);
1483
+ }
1313
1484
  }
1314
1485
  if (patch.bottom) {
1315
1486
  const bottomPoints = MeshGradient.sampleBezierCurve(
1316
1487
  patch.bottom,
1317
1488
  samples,
1318
1489
  );
1319
- allPoints.push(...bottomPoints);
1490
+ if (Array.isArray(bottomPoints)) {
1491
+ allPoints.push(...bottomPoints);
1492
+ }
1320
1493
  }
1321
1494
  if (patch.left) {
1322
1495
  const leftPoints = MeshGradient.sampleBezierCurve(patch.left, samples);
1323
- allPoints.push(...leftPoints);
1496
+ if (Array.isArray(leftPoints)) {
1497
+ allPoints.push(...leftPoints);
1498
+ }
1324
1499
  }
1325
1500
  }
1326
1501
 
@@ -1394,26 +1569,40 @@ export function clipWithMeshGradientShape(
1394
1569
  meshData,
1395
1570
  options = {},
1396
1571
  ) {
1572
+ if (!targetPolygon) throw new Error('clipWithMeshGradientShape: targetPolygon is required');
1573
+ if (!Array.isArray(targetPolygon)) throw new Error('clipWithMeshGradientShape: targetPolygon must be an array');
1574
+ if (targetPolygon.length === 0) return []; // Empty polygon returns empty result
1575
+ if (!meshData) throw new Error('clipWithMeshGradientShape: meshData is required');
1576
+
1397
1577
  const { subdivisions = 4 } = options;
1578
+ if (typeof subdivisions !== 'number' || subdivisions <= 0) {
1579
+ throw new Error('clipWithMeshGradientShape: subdivisions must be a positive number');
1580
+ }
1398
1581
 
1399
1582
  // Get all patch polygons (ignoring colors)
1400
1583
  const meshPolygons = MeshGradient.meshGradientToPolygons(meshData, {
1401
1584
  subdivisions,
1402
1585
  });
1403
1586
 
1404
- if (meshPolygons.length === 0) {
1587
+ if (!Array.isArray(meshPolygons) || meshPolygons.length === 0) {
1405
1588
  return [];
1406
1589
  }
1407
1590
 
1408
1591
  // Union all patch polygons to get the complete mesh shape
1409
1592
  let meshShape = meshPolygons[0].polygon;
1593
+ if (!meshShape || meshShape.length < 3) {
1594
+ return [];
1595
+ }
1410
1596
 
1411
1597
  for (let i = 1; i < meshPolygons.length; i++) {
1598
+ if (!meshPolygons[i] || !meshPolygons[i].polygon || meshPolygons[i].polygon.length < 3) {
1599
+ continue; // Skip invalid polygons
1600
+ }
1412
1601
  const unionResult = PolygonClip.polygonUnion(
1413
1602
  meshShape,
1414
1603
  meshPolygons[i].polygon,
1415
1604
  );
1416
- if (unionResult.length > 0 && unionResult[0].length >= 3) {
1605
+ if (Array.isArray(unionResult) && unionResult.length > 0 && unionResult[0].length >= 3) {
1417
1606
  meshShape = unionResult[0];
1418
1607
  }
1419
1608
  }
@@ -1469,24 +1658,34 @@ export function clipWithMeshGradientShape(
1469
1658
  * const svgClipPath = `<clipPath id="mesh-shape"><path d="${pathData}"/></clipPath>`;
1470
1659
  */
1471
1660
  export function meshGradientToClipPath(meshData, options = {}) {
1661
+ if (!meshData) throw new Error('meshGradientToClipPath: meshData is required');
1662
+
1472
1663
  const { subdivisions = 4 } = options;
1664
+ if (typeof subdivisions !== 'number' || subdivisions <= 0) {
1665
+ throw new Error('meshGradientToClipPath: subdivisions must be a positive number');
1666
+ }
1473
1667
 
1474
1668
  const meshPolygons = MeshGradient.meshGradientToPolygons(meshData, {
1475
1669
  subdivisions,
1476
1670
  });
1477
1671
 
1478
- if (meshPolygons.length === 0) {
1672
+ if (!Array.isArray(meshPolygons) || meshPolygons.length === 0) {
1479
1673
  return [];
1480
1674
  }
1481
1675
 
1482
1676
  // Union all patches into one shape
1483
1677
  let result = meshPolygons[0].polygon;
1678
+ if (!result || result.length < 3) {
1679
+ return [];
1680
+ }
1484
1681
 
1485
1682
  for (let i = 1; i < meshPolygons.length; i++) {
1683
+ if (!meshPolygons[i] || !meshPolygons[i].polygon) continue; // Skip invalid polygons
1684
+
1486
1685
  const poly = meshPolygons[i].polygon;
1487
1686
  if (poly.length >= 3) {
1488
1687
  const unionResult = PolygonClip.polygonUnion(result, poly);
1489
- if (unionResult.length > 0 && unionResult[0].length >= 3) {
1688
+ if (Array.isArray(unionResult) && unionResult.length > 0 && unionResult[0].length >= 3) {
1490
1689
  result = unionResult[0];
1491
1690
  }
1492
1691
  }