@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
@@ -193,15 +193,41 @@ export function parseViewBox(viewBoxString) {
193
193
  * @param {Decimal} maxX - Maximum X coordinate
194
194
  * @param {Decimal} maxY - Maximum Y coordinate
195
195
  * @returns {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal, width: Decimal, height: Decimal}}
196
+ * @throws {Error} If parameters are invalid or bounds are inverted
196
197
  */
197
198
  function createBBox(minX, minY, maxX, maxY) {
199
+ // Validate all parameters exist and are Decimal-convertible (WHY: prevent null/undefined crashes)
200
+ if (minX == null || minY == null || maxX == null || maxY == null) {
201
+ throw new Error(
202
+ "createBBox: all parameters (minX, minY, maxX, maxY) must be provided",
203
+ );
204
+ }
205
+
206
+ // Convert to Decimal (WHY: ensure consistent type)
207
+ const dMinX = D(minX);
208
+ const dMinY = D(minY);
209
+ const dMaxX = D(maxX);
210
+ const dMaxY = D(maxY);
211
+
212
+ // Validate bounds are not inverted (WHY: catch logic errors early)
213
+ if (dMaxX.lessThan(dMinX)) {
214
+ throw new Error(
215
+ `createBBox: maxX (${dMaxX}) must be >= minX (${dMinX})`,
216
+ );
217
+ }
218
+ if (dMaxY.lessThan(dMinY)) {
219
+ throw new Error(
220
+ `createBBox: maxY (${dMaxY}) must be >= minY (${dMinY})`,
221
+ );
222
+ }
223
+
198
224
  return {
199
- minX,
200
- minY,
201
- maxX,
202
- maxY,
203
- width: maxX.minus(minX),
204
- height: maxY.minus(minY),
225
+ minX: dMinX,
226
+ minY: dMinY,
227
+ maxX: dMaxX,
228
+ maxY: dMaxY,
229
+ width: dMaxX.minus(dMinX),
230
+ height: dMaxY.minus(dMinY),
205
231
  };
206
232
  }
207
233
 
@@ -212,13 +238,34 @@ function createBBox(minX, minY, maxX, maxY) {
212
238
  * @param {Decimal} x - Point X coordinate
213
239
  * @param {Decimal} y - Point Y coordinate
214
240
  * @returns {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}}
241
+ * @throws {Error} If bbox or coordinates are invalid
215
242
  */
216
243
  function _expandBBox(bbox, x, y) {
244
+ // Validate bbox parameter (WHY: prevent null dereference)
245
+ if (!bbox || typeof bbox !== "object") {
246
+ throw new Error("_expandBBox: bbox must be a bounding box object");
247
+ }
248
+ if (
249
+ bbox.minX == null ||
250
+ bbox.minY == null ||
251
+ bbox.maxX == null ||
252
+ bbox.maxY == null
253
+ ) {
254
+ throw new Error(
255
+ "_expandBBox: bbox must have minX, minY, maxX, maxY properties",
256
+ );
257
+ }
258
+
259
+ // Validate coordinates (WHY: prevent null/undefined crashes)
260
+ if (x == null || y == null) {
261
+ throw new Error("_expandBBox: x and y coordinates must be provided");
262
+ }
263
+
217
264
  return {
218
- minX: Decimal.min(bbox.minX, x),
219
- minY: Decimal.min(bbox.minY, y),
220
- maxX: Decimal.max(bbox.maxX, x),
221
- maxY: Decimal.max(bbox.maxY, y),
265
+ minX: Decimal.min(bbox.minX, D(x)),
266
+ minY: Decimal.min(bbox.minY, D(y)),
267
+ maxX: Decimal.max(bbox.maxX, D(x)),
268
+ maxY: Decimal.max(bbox.maxY, D(y)),
222
269
  };
223
270
  }
224
271
 
@@ -235,8 +282,30 @@ function _expandBBox(bbox, x, y) {
235
282
  * @param {Decimal} y3 - End Y
236
283
  * @param {number} samples - Number of samples
237
284
  * @returns {Array<{x: Decimal, y: Decimal}>} Sample points
285
+ * @throws {Error} If parameters are invalid or samples is zero/negative
238
286
  */
239
287
  function sampleCubicBezier(x0, y0, x1, y1, x2, y2, x3, y3, samples = 20) {
288
+ // Validate all parameters exist (WHY: prevent null/undefined crashes)
289
+ if (
290
+ x0 == null ||
291
+ y0 == null ||
292
+ x1 == null ||
293
+ y1 == null ||
294
+ x2 == null ||
295
+ y2 == null ||
296
+ x3 == null ||
297
+ y3 == null
298
+ ) {
299
+ throw new Error(
300
+ "sampleCubicBezier: all coordinate parameters must be provided",
301
+ );
302
+ }
303
+
304
+ // Validate samples parameter (WHY: prevent division by zero)
305
+ if (samples == null || samples <= 0) {
306
+ throw new Error("sampleCubicBezier: samples must be a positive number");
307
+ }
308
+
240
309
  const points = [];
241
310
  for (let i = 0; i <= samples; i++) {
242
311
  const t = D(i).div(samples);
@@ -276,8 +345,28 @@ function sampleCubicBezier(x0, y0, x1, y1, x2, y2, x3, y3, samples = 20) {
276
345
  * @param {Decimal} y2 - End Y
277
346
  * @param {number} samples - Number of samples
278
347
  * @returns {Array<{x: Decimal, y: Decimal}>} Sample points
348
+ * @throws {Error} If parameters are invalid or samples is zero/negative
279
349
  */
280
350
  function sampleQuadraticBezier(x0, y0, x1, y1, x2, y2, samples = 20) {
351
+ // Validate all parameters exist (WHY: prevent null/undefined crashes)
352
+ if (
353
+ x0 == null ||
354
+ y0 == null ||
355
+ x1 == null ||
356
+ y1 == null ||
357
+ x2 == null ||
358
+ y2 == null
359
+ ) {
360
+ throw new Error(
361
+ "sampleQuadraticBezier: all coordinate parameters must be provided",
362
+ );
363
+ }
364
+
365
+ // Validate samples parameter (WHY: prevent division by zero)
366
+ if (samples == null || samples <= 0) {
367
+ throw new Error("sampleQuadraticBezier: samples must be a positive number");
368
+ }
369
+
281
370
  const points = [];
282
371
  for (let i = 0; i <= samples; i++) {
283
372
  const t = D(i).div(samples);
@@ -309,8 +398,29 @@ function sampleQuadraticBezier(x0, y0, x1, y1, x2, y2, samples = 20) {
309
398
  * @param {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}} bbox - Bounding box
310
399
  * @param {Decimal} tolerance - Tolerance for boundary checks
311
400
  * @returns {boolean} True if point is inside or on boundary
401
+ * @throws {Error} If pt or bbox are invalid
312
402
  */
313
403
  function pointInBBox(pt, bbox, tolerance = DEFAULT_TOLERANCE) {
404
+ // Validate point parameter (WHY: prevent null dereference)
405
+ if (!pt || typeof pt !== "object" || pt.x == null || pt.y == null) {
406
+ throw new Error("pointInBBox: pt must be an object with x and y properties");
407
+ }
408
+
409
+ // Validate bbox parameter (WHY: prevent null dereference)
410
+ if (!bbox || typeof bbox !== "object") {
411
+ throw new Error("pointInBBox: bbox must be a bounding box object");
412
+ }
413
+ if (
414
+ bbox.minX == null ||
415
+ bbox.minY == null ||
416
+ bbox.maxX == null ||
417
+ bbox.maxY == null
418
+ ) {
419
+ throw new Error(
420
+ "pointInBBox: bbox must have minX, minY, maxX, maxY properties",
421
+ );
422
+ }
423
+
314
424
  return (
315
425
  pt.x.greaterThanOrEqualTo(bbox.minX.minus(tolerance)) &&
316
426
  pt.x.lessThanOrEqualTo(bbox.maxX.plus(tolerance)) &&
@@ -358,6 +468,13 @@ export function pathBoundingBox(pathCommands) {
358
468
  const samplePoints = [];
359
469
 
360
470
  for (const cmd of pathCommands) {
471
+ // Validate command object and type property (WHY: prevent null dereference and type errors)
472
+ if (!cmd || typeof cmd !== "object" || !cmd.type || typeof cmd.type !== "string") {
473
+ throw new Error(
474
+ "pathBoundingBox: each command must be an object with a string type property",
475
+ );
476
+ }
477
+
361
478
  const type = cmd.type.toUpperCase();
362
479
  // BUG 1 FIX: Check if command is relative (lowercase) or absolute (uppercase)
363
480
  const isRelative = cmd.type === cmd.type.toLowerCase();
@@ -365,6 +482,11 @@ export function pathBoundingBox(pathCommands) {
365
482
  switch (type) {
366
483
  case "M": // Move
367
484
  {
485
+ // Validate required properties (WHY: prevent accessing undefined properties)
486
+ if (cmd.x == null || cmd.y == null) {
487
+ throw new Error("pathBoundingBox: M command requires x and y properties");
488
+ }
489
+
368
490
  // BUG 1 FIX: Handle relative coordinates
369
491
  const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
370
492
  const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
@@ -387,6 +509,11 @@ export function pathBoundingBox(pathCommands) {
387
509
 
388
510
  case "L": // Line to
389
511
  {
512
+ // Validate required properties (WHY: prevent accessing undefined properties)
513
+ if (cmd.x == null || cmd.y == null) {
514
+ throw new Error("pathBoundingBox: L command requires x and y properties");
515
+ }
516
+
390
517
  // BUG 1 FIX: Handle relative coordinates
391
518
  const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
392
519
  const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
@@ -407,6 +534,11 @@ export function pathBoundingBox(pathCommands) {
407
534
 
408
535
  case "H": // Horizontal line
409
536
  {
537
+ // Validate required properties (WHY: prevent accessing undefined properties)
538
+ if (cmd.x == null) {
539
+ throw new Error("pathBoundingBox: H command requires x property");
540
+ }
541
+
410
542
  // BUG 1 FIX: Handle relative coordinates
411
543
  const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
412
544
 
@@ -423,6 +555,11 @@ export function pathBoundingBox(pathCommands) {
423
555
 
424
556
  case "V": // Vertical line
425
557
  {
558
+ // Validate required properties (WHY: prevent accessing undefined properties)
559
+ if (cmd.y == null) {
560
+ throw new Error("pathBoundingBox: V command requires y property");
561
+ }
562
+
426
563
  // BUG 1 FIX: Handle relative coordinates
427
564
  const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
428
565
 
@@ -439,6 +576,20 @@ export function pathBoundingBox(pathCommands) {
439
576
 
440
577
  case "C": // Cubic Bezier
441
578
  {
579
+ // Validate required properties (WHY: prevent accessing undefined properties)
580
+ if (
581
+ cmd.x1 == null ||
582
+ cmd.y1 == null ||
583
+ cmd.x2 == null ||
584
+ cmd.y2 == null ||
585
+ cmd.x == null ||
586
+ cmd.y == null
587
+ ) {
588
+ throw new Error(
589
+ "pathBoundingBox: C command requires x1, y1, x2, y2, x, y properties",
590
+ );
591
+ }
592
+
442
593
  // BUG 1 FIX: Handle relative coordinates
443
594
  const x1 = isRelative ? currentX.plus(D(cmd.x1)) : D(cmd.x1);
444
595
  const y1 = isRelative ? currentY.plus(D(cmd.y1)) : D(cmd.y1);
@@ -477,6 +628,13 @@ export function pathBoundingBox(pathCommands) {
477
628
 
478
629
  case "S": // Smooth cubic Bezier
479
630
  {
631
+ // Validate required properties (WHY: prevent accessing undefined properties)
632
+ if (cmd.x2 == null || cmd.y2 == null || cmd.x == null || cmd.y == null) {
633
+ throw new Error(
634
+ "pathBoundingBox: S command requires x2, y2, x, y properties",
635
+ );
636
+ }
637
+
480
638
  // BUG 1 FIX: Handle relative coordinates
481
639
  const x2 = isRelative ? currentX.plus(D(cmd.x2)) : D(cmd.x2);
482
640
  const y2 = isRelative ? currentY.plus(D(cmd.y2)) : D(cmd.y2);
@@ -523,6 +681,13 @@ export function pathBoundingBox(pathCommands) {
523
681
 
524
682
  case "Q": // Quadratic Bezier
525
683
  {
684
+ // Validate required properties (WHY: prevent accessing undefined properties)
685
+ if (cmd.x1 == null || cmd.y1 == null || cmd.x == null || cmd.y == null) {
686
+ throw new Error(
687
+ "pathBoundingBox: Q command requires x1, y1, x, y properties",
688
+ );
689
+ }
690
+
526
691
  // BUG 1 FIX: Handle relative coordinates
527
692
  const x1 = isRelative ? currentX.plus(D(cmd.x1)) : D(cmd.x1);
528
693
  const y1 = isRelative ? currentY.plus(D(cmd.y1)) : D(cmd.y1);
@@ -556,6 +721,11 @@ export function pathBoundingBox(pathCommands) {
556
721
 
557
722
  case "T": // Smooth quadratic Bezier
558
723
  {
724
+ // Validate required properties (WHY: prevent accessing undefined properties)
725
+ if (cmd.x == null || cmd.y == null) {
726
+ throw new Error("pathBoundingBox: T command requires x and y properties");
727
+ }
728
+
559
729
  // BUG 1 FIX: Handle relative coordinates
560
730
  const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
561
731
  const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
@@ -598,6 +768,11 @@ export function pathBoundingBox(pathCommands) {
598
768
 
599
769
  case "A": // Arc (approximate with samples)
600
770
  {
771
+ // Validate required properties (WHY: prevent accessing undefined properties)
772
+ if (cmd.x == null || cmd.y == null) {
773
+ throw new Error("pathBoundingBox: A command requires x and y properties");
774
+ }
775
+
601
776
  // BUG 4: Arc bounding box ignores actual arc geometry
602
777
  // TODO: Implement proper arc-to-bezier conversion or calculate arc extrema
603
778
  // Current implementation only samples linearly between endpoints, which
@@ -698,6 +873,18 @@ export function shapeBoundingBox(shape) {
698
873
  switch (type) {
699
874
  case "rect":
700
875
  {
876
+ // Validate required properties (WHY: prevent accessing undefined properties)
877
+ if (
878
+ shape.x == null ||
879
+ shape.y == null ||
880
+ shape.width == null ||
881
+ shape.height == null
882
+ ) {
883
+ throw new Error(
884
+ "shapeBoundingBox: rect requires x, y, width, height properties",
885
+ );
886
+ }
887
+
701
888
  const x = D(shape.x);
702
889
  const y = D(shape.y);
703
890
  const width = D(shape.width);
@@ -717,6 +904,13 @@ export function shapeBoundingBox(shape) {
717
904
 
718
905
  case "circle":
719
906
  {
907
+ // Validate required properties (WHY: prevent accessing undefined properties)
908
+ if (shape.cx == null || shape.cy == null || shape.r == null) {
909
+ throw new Error(
910
+ "shapeBoundingBox: circle requires cx, cy, r properties",
911
+ );
912
+ }
913
+
720
914
  const cx = D(shape.cx);
721
915
  const cy = D(shape.cy);
722
916
  const r = D(shape.r);
@@ -737,6 +931,18 @@ export function shapeBoundingBox(shape) {
737
931
 
738
932
  case "ellipse":
739
933
  {
934
+ // Validate required properties (WHY: prevent accessing undefined properties)
935
+ if (
936
+ shape.cx == null ||
937
+ shape.cy == null ||
938
+ shape.rx == null ||
939
+ shape.ry == null
940
+ ) {
941
+ throw new Error(
942
+ "shapeBoundingBox: ellipse requires cx, cy, rx, ry properties",
943
+ );
944
+ }
945
+
740
946
  const cx = D(shape.cx);
741
947
  const cy = D(shape.cy);
742
948
  const rx = D(shape.rx);
@@ -758,6 +964,18 @@ export function shapeBoundingBox(shape) {
758
964
 
759
965
  case "line":
760
966
  {
967
+ // Validate required properties (WHY: prevent accessing undefined properties)
968
+ if (
969
+ shape.x1 == null ||
970
+ shape.y1 == null ||
971
+ shape.x2 == null ||
972
+ shape.y2 == null
973
+ ) {
974
+ throw new Error(
975
+ "shapeBoundingBox: line requires x1, y1, x2, y2 properties",
976
+ );
977
+ }
978
+
761
979
  const x1 = D(shape.x1);
762
980
  const y1 = D(shape.y1);
763
981
  const x2 = D(shape.x2);
@@ -789,6 +1007,13 @@ export function shapeBoundingBox(shape) {
789
1007
  let maxY = D(-Infinity);
790
1008
 
791
1009
  for (const pt of shape.points) {
1010
+ // Validate point has x and y properties (WHY: prevent accessing undefined properties)
1011
+ if (!pt || typeof pt !== "object" || pt.x == null || pt.y == null) {
1012
+ throw new Error(
1013
+ `shapeBoundingBox: ${type} points must have x and y properties`,
1014
+ );
1015
+ }
1016
+
792
1017
  const x = D(pt.x);
793
1018
  const y = D(pt.y);
794
1019
  minX = Decimal.min(minX, x);
@@ -827,8 +1052,24 @@ export function shapeBoundingBox(shape) {
827
1052
  *
828
1053
  * @param {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}} bbox - Bounding box
829
1054
  * @returns {Array<{x: Decimal, y: Decimal}>} Polygon vertices (counter-clockwise)
1055
+ * @throws {Error} If bbox is invalid
830
1056
  */
831
1057
  function bboxToPolygon(bbox) {
1058
+ // Validate bbox parameter (WHY: prevent null dereference)
1059
+ if (!bbox || typeof bbox !== "object") {
1060
+ throw new Error("bboxToPolygon: bbox must be a bounding box object");
1061
+ }
1062
+ if (
1063
+ bbox.minX == null ||
1064
+ bbox.minY == null ||
1065
+ bbox.maxX == null ||
1066
+ bbox.maxY == null
1067
+ ) {
1068
+ throw new Error(
1069
+ "bboxToPolygon: bbox must have minX, minY, maxX, maxY properties",
1070
+ );
1071
+ }
1072
+
832
1073
  return [
833
1074
  point(bbox.minX, bbox.minY),
834
1075
  point(bbox.maxX, bbox.minY),
@@ -848,9 +1089,25 @@ function bboxToPolygon(bbox) {
848
1089
  * @param {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}} bbox - Bounding box to test
849
1090
  * @param {{x: Decimal, y: Decimal, width: Decimal, height: Decimal}} viewBox - ViewBox object
850
1091
  * @returns {{intersects: boolean, verified: boolean}}
1092
+ * @throws {Error} If bbox or viewBox are invalid
851
1093
  */
852
1094
  export function bboxIntersectsViewBox(bbox, viewBox) {
853
- // Convert both to polygons
1095
+ // Validate viewBox parameter (WHY: prevent null dereference)
1096
+ if (!viewBox || typeof viewBox !== "object") {
1097
+ throw new Error("bboxIntersectsViewBox: viewBox must be an object");
1098
+ }
1099
+ if (
1100
+ viewBox.x == null ||
1101
+ viewBox.y == null ||
1102
+ viewBox.width == null ||
1103
+ viewBox.height == null
1104
+ ) {
1105
+ throw new Error(
1106
+ "bboxIntersectsViewBox: viewBox must have x, y, width, height properties",
1107
+ );
1108
+ }
1109
+
1110
+ // Convert both to polygons (bboxToPolygon validates bbox)
854
1111
  const bboxPoly = bboxToPolygon(bbox);
855
1112
  const viewBoxPoly = bboxToPolygon({
856
1113
  minX: viewBox.x,
@@ -882,9 +1139,25 @@ export function bboxIntersectsViewBox(bbox, viewBox) {
882
1139
  * @param {Array<Object>} pathCommands - Array of path command objects
883
1140
  * @param {{x: Decimal, y: Decimal, width: Decimal, height: Decimal}} viewBox - ViewBox object
884
1141
  * @returns {{offCanvas: boolean, bbox: Object, verified: boolean}}
1142
+ * @throws {Error} If parameters are invalid
885
1143
  */
886
1144
  export function isPathOffCanvas(pathCommands, viewBox) {
887
- // Calculate path bounding box
1145
+ // Validate viewBox parameter (WHY: prevent null dereference)
1146
+ if (!viewBox || typeof viewBox !== "object") {
1147
+ throw new Error("isPathOffCanvas: viewBox must be an object");
1148
+ }
1149
+ if (
1150
+ viewBox.x == null ||
1151
+ viewBox.y == null ||
1152
+ viewBox.width == null ||
1153
+ viewBox.height == null
1154
+ ) {
1155
+ throw new Error(
1156
+ "isPathOffCanvas: viewBox must have x, y, width, height properties",
1157
+ );
1158
+ }
1159
+
1160
+ // Calculate path bounding box (pathBoundingBox validates pathCommands)
888
1161
  const bbox = pathBoundingBox(pathCommands);
889
1162
 
890
1163
  // Check intersection
@@ -918,8 +1191,14 @@ export function isPathOffCanvas(pathCommands, viewBox) {
918
1191
  for (const cmd of pathCommands) {
919
1192
  if (sampleCount >= maxSamples) break;
920
1193
 
1194
+ // Validate command has type (WHY: prevent null dereference)
1195
+ if (!cmd || !cmd.type) continue;
1196
+
921
1197
  const type = cmd.type.toUpperCase();
922
1198
  if (type === "M" || type === "L") {
1199
+ // Skip if properties missing (WHY: avoid crashes during verification)
1200
+ if (cmd.x == null || cmd.y == null) continue;
1201
+
923
1202
  const x = D(cmd.x);
924
1203
  const y = D(cmd.y);
925
1204
  if (pointInBBox({ x, y }, viewBoxBBox)) {
@@ -949,9 +1228,25 @@ export function isPathOffCanvas(pathCommands, viewBox) {
949
1228
  * @param {Object} shape - Shape object with type and properties
950
1229
  * @param {{x: Decimal, y: Decimal, width: Decimal, height: Decimal}} viewBox - ViewBox object
951
1230
  * @returns {{offCanvas: boolean, bbox: Object, verified: boolean}}
1231
+ * @throws {Error} If parameters are invalid
952
1232
  */
953
1233
  export function isShapeOffCanvas(shape, viewBox) {
954
- // Calculate shape bounding box
1234
+ // Validate viewBox parameter (WHY: prevent null dereference)
1235
+ if (!viewBox || typeof viewBox !== "object") {
1236
+ throw new Error("isShapeOffCanvas: viewBox must be an object");
1237
+ }
1238
+ if (
1239
+ viewBox.x == null ||
1240
+ viewBox.y == null ||
1241
+ viewBox.width == null ||
1242
+ viewBox.height == null
1243
+ ) {
1244
+ throw new Error(
1245
+ "isShapeOffCanvas: viewBox must have x, y, width, height properties",
1246
+ );
1247
+ }
1248
+
1249
+ // Calculate shape bounding box (shapeBoundingBox validates shape)
955
1250
  const bbox = shapeBoundingBox(shape);
956
1251
 
957
1252
  // Check intersection
@@ -985,8 +1280,31 @@ export function isShapeOffCanvas(shape, viewBox) {
985
1280
  * @param {{x: Decimal, y: Decimal}} p2 - Line segment end
986
1281
  * @param {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}} bounds - Clipping bounds
987
1282
  * @returns {Array<{x: Decimal, y: Decimal}>} Clipped segment endpoints (empty if completely outside)
1283
+ * @throws {Error} If parameters are invalid
988
1284
  */
989
1285
  function clipLine(p1, p2, bounds) {
1286
+ // Validate points (WHY: prevent null dereference)
1287
+ if (!p1 || typeof p1 !== "object" || p1.x == null || p1.y == null) {
1288
+ throw new Error("clipLine: p1 must be an object with x and y properties");
1289
+ }
1290
+ if (!p2 || typeof p2 !== "object" || p2.x == null || p2.y == null) {
1291
+ throw new Error("clipLine: p2 must be an object with x and y properties");
1292
+ }
1293
+
1294
+ // Validate bounds (WHY: prevent null dereference)
1295
+ if (!bounds || typeof bounds !== "object") {
1296
+ throw new Error("clipLine: bounds must be a bounding box object");
1297
+ }
1298
+ if (
1299
+ bounds.minX == null ||
1300
+ bounds.minY == null ||
1301
+ bounds.maxX == null ||
1302
+ bounds.maxY == null
1303
+ ) {
1304
+ throw new Error(
1305
+ "clipLine: bounds must have minX, minY, maxX, maxY properties",
1306
+ );
1307
+ }
990
1308
  // Cohen-Sutherland outcodes
991
1309
  const INSIDE = 0; // 0000
992
1310
  const LEFT = 1; // 0001
@@ -1101,8 +1419,29 @@ function clipLine(p1, p2, bounds) {
1101
1419
  * @param {Array<Object>} pathCommands - Array of path command objects
1102
1420
  * @param {{x: Decimal, y: Decimal, width: Decimal, height: Decimal}} viewBox - ViewBox object
1103
1421
  * @returns {{commands: Array<Object>, verified: boolean}}
1422
+ * @throws {Error} If parameters are invalid
1104
1423
  */
1105
1424
  export function clipPathToViewBox(pathCommands, viewBox) {
1425
+ // Validate pathCommands (WHY: prevent null dereference)
1426
+ if (!Array.isArray(pathCommands)) {
1427
+ throw new Error("clipPathToViewBox: pathCommands must be an array");
1428
+ }
1429
+
1430
+ // Validate viewBox parameter (WHY: prevent null dereference)
1431
+ if (!viewBox || typeof viewBox !== "object") {
1432
+ throw new Error("clipPathToViewBox: viewBox must be an object");
1433
+ }
1434
+ if (
1435
+ viewBox.x == null ||
1436
+ viewBox.y == null ||
1437
+ viewBox.width == null ||
1438
+ viewBox.height == null
1439
+ ) {
1440
+ throw new Error(
1441
+ "clipPathToViewBox: viewBox must have x, y, width, height properties",
1442
+ );
1443
+ }
1444
+
1106
1445
  const bounds = {
1107
1446
  minX: viewBox.x,
1108
1447
  minY: viewBox.y,
@@ -1116,11 +1455,21 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1116
1455
  let pathStarted = false;
1117
1456
 
1118
1457
  for (const cmd of pathCommands) {
1458
+ // Validate command object and type (WHY: prevent null dereference)
1459
+ if (!cmd || typeof cmd !== "object" || !cmd.type || typeof cmd.type !== "string") {
1460
+ continue; // Skip invalid commands during clipping (WHY: graceful degradation)
1461
+ }
1462
+
1119
1463
  const type = cmd.type.toUpperCase();
1120
1464
 
1121
1465
  switch (type) {
1122
1466
  case "M": // Move
1123
1467
  {
1468
+ // Skip if properties missing (WHY: graceful degradation during clipping)
1469
+ if (cmd.x == null || cmd.y == null) {
1470
+ continue;
1471
+ }
1472
+
1124
1473
  const x = D(cmd.x);
1125
1474
  const y = D(cmd.y);
1126
1475
 
@@ -1139,6 +1488,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1139
1488
 
1140
1489
  case "L": // Line to
1141
1490
  {
1491
+ // Skip if properties missing (WHY: graceful degradation during clipping)
1492
+ if (cmd.x == null || cmd.y == null) {
1493
+ continue;
1494
+ }
1495
+
1142
1496
  const x = D(cmd.x);
1143
1497
  const y = D(cmd.y);
1144
1498
 
@@ -1176,6 +1530,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1176
1530
 
1177
1531
  case "H": // Horizontal line
1178
1532
  {
1533
+ // Skip if properties missing (WHY: graceful degradation during clipping)
1534
+ if (cmd.x == null) {
1535
+ continue;
1536
+ }
1537
+
1179
1538
  const x = D(cmd.x);
1180
1539
  const clipped = clipLine(
1181
1540
  { x: currentX, y: currentY },
@@ -1207,6 +1566,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1207
1566
 
1208
1567
  case "V": // Vertical line
1209
1568
  {
1569
+ // Skip if properties missing (WHY: graceful degradation during clipping)
1570
+ if (cmd.y == null) {
1571
+ continue;
1572
+ }
1573
+
1210
1574
  const y = D(cmd.y);
1211
1575
  const clipped = clipLine(
1212
1576
  { x: currentX, y: currentY },
@@ -1242,6 +1606,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
1242
1606
  case "T": // Smooth quadratic - sample as polyline
1243
1607
  case "A": // Arc - sample as polyline
1244
1608
  {
1609
+ // Skip if properties missing (WHY: graceful degradation during clipping)
1610
+ if (cmd.x == null || cmd.y == null) {
1611
+ continue;
1612
+ }
1613
+
1245
1614
  // For simplicity, just include the endpoint
1246
1615
  // A full implementation would sample the curve
1247
1616
  const x = D(cmd.x);