@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
@@ -94,6 +94,10 @@ export function pathToPolygon(
94
94
  pathData,
95
95
  samplesPerCurve = DEFAULT_CURVE_SAMPLES,
96
96
  ) {
97
+ if (typeof pathData !== 'string') throw new Error('pathToPolygon: pathData must be a string');
98
+ if (typeof samplesPerCurve !== 'number' || samplesPerCurve <= 0 || !Number.isFinite(samplesPerCurve)) {
99
+ throw new Error(`pathToPolygon: samplesPerCurve must be a positive finite number, got ${samplesPerCurve}`);
100
+ }
97
101
  const points = [];
98
102
  let currentX = D(0),
99
103
  currentY = D(0);
@@ -101,12 +105,19 @@ export function pathToPolygon(
101
105
  startY = D(0);
102
106
 
103
107
  const commands = parsePathCommands(pathData);
108
+ if (!Array.isArray(commands)) {
109
+ throw new Error('pathToPolygon: parsePathCommands must return an array');
110
+ }
104
111
 
105
112
  for (const cmd of commands) {
106
113
  const { type, args } = cmd;
114
+ if (!Array.isArray(args)) {
115
+ throw new Error(`pathToPolygon: command args must be an array for command ${type}`);
116
+ }
107
117
 
108
118
  switch (type) {
109
119
  case "M":
120
+ if (args.length < 2) throw new Error(`pathToPolygon: M command requires 2 arguments, got ${args.length}`);
110
121
  currentX = D(args[0]);
111
122
  currentY = D(args[1]);
112
123
  startX = currentX;
@@ -114,6 +125,7 @@ export function pathToPolygon(
114
125
  points.push(PolygonClip.point(currentX, currentY));
115
126
  break;
116
127
  case "m":
128
+ if (args.length < 2) throw new Error(`pathToPolygon: m command requires 2 arguments, got ${args.length}`);
117
129
  currentX = currentX.plus(args[0]);
118
130
  currentY = currentY.plus(args[1]);
119
131
  startX = currentX;
@@ -121,32 +133,39 @@ export function pathToPolygon(
121
133
  points.push(PolygonClip.point(currentX, currentY));
122
134
  break;
123
135
  case "L":
136
+ if (args.length < 2) throw new Error(`pathToPolygon: L command requires 2 arguments, got ${args.length}`);
124
137
  currentX = D(args[0]);
125
138
  currentY = D(args[1]);
126
139
  points.push(PolygonClip.point(currentX, currentY));
127
140
  break;
128
141
  case "l":
142
+ if (args.length < 2) throw new Error(`pathToPolygon: l command requires 2 arguments, got ${args.length}`);
129
143
  currentX = currentX.plus(args[0]);
130
144
  currentY = currentY.plus(args[1]);
131
145
  points.push(PolygonClip.point(currentX, currentY));
132
146
  break;
133
147
  case "H":
148
+ if (args.length < 1) throw new Error(`pathToPolygon: H command requires 1 argument, got ${args.length}`);
134
149
  currentX = D(args[0]);
135
150
  points.push(PolygonClip.point(currentX, currentY));
136
151
  break;
137
152
  case "h":
153
+ if (args.length < 1) throw new Error(`pathToPolygon: h command requires 1 argument, got ${args.length}`);
138
154
  currentX = currentX.plus(args[0]);
139
155
  points.push(PolygonClip.point(currentX, currentY));
140
156
  break;
141
157
  case "V":
158
+ if (args.length < 1) throw new Error(`pathToPolygon: V command requires 1 argument, got ${args.length}`);
142
159
  currentY = D(args[0]);
143
160
  points.push(PolygonClip.point(currentX, currentY));
144
161
  break;
145
162
  case "v":
163
+ if (args.length < 1) throw new Error(`pathToPolygon: v command requires 1 argument, got ${args.length}`);
146
164
  currentY = currentY.plus(args[0]);
147
165
  points.push(PolygonClip.point(currentX, currentY));
148
166
  break;
149
167
  case "C":
168
+ if (args.length < 6) throw new Error(`pathToPolygon: C command requires 6 arguments, got ${args.length}`);
150
169
  sampleCubicBezier(
151
170
  points,
152
171
  currentX,
@@ -163,6 +182,7 @@ export function pathToPolygon(
163
182
  currentY = D(args[5]);
164
183
  break;
165
184
  case "c":
185
+ if (args.length < 6) throw new Error(`pathToPolygon: c command requires 6 arguments, got ${args.length}`);
166
186
  sampleCubicBezier(
167
187
  points,
168
188
  currentX,
@@ -179,6 +199,7 @@ export function pathToPolygon(
179
199
  currentY = currentY.plus(args[5]);
180
200
  break;
181
201
  case "Q":
202
+ if (args.length < 4) throw new Error(`pathToPolygon: Q command requires 4 arguments, got ${args.length}`);
182
203
  sampleQuadraticBezier(
183
204
  points,
184
205
  currentX,
@@ -193,6 +214,7 @@ export function pathToPolygon(
193
214
  currentY = D(args[3]);
194
215
  break;
195
216
  case "q":
217
+ if (args.length < 4) throw new Error(`pathToPolygon: q command requires 4 arguments, got ${args.length}`);
196
218
  sampleQuadraticBezier(
197
219
  points,
198
220
  currentX,
@@ -207,6 +229,7 @@ export function pathToPolygon(
207
229
  currentY = currentY.plus(args[3]);
208
230
  break;
209
231
  case "A":
232
+ if (args.length < 7) throw new Error(`pathToPolygon: A command requires 7 arguments, got ${args.length}`);
210
233
  sampleArc(
211
234
  points,
212
235
  currentX,
@@ -224,6 +247,7 @@ export function pathToPolygon(
224
247
  currentY = D(args[6]);
225
248
  break;
226
249
  case "a":
250
+ if (args.length < 7) throw new Error(`pathToPolygon: a command requires 7 arguments, got ${args.length}`);
227
251
  sampleArc(
228
252
  points,
229
253
  currentX,
@@ -245,6 +269,8 @@ export function pathToPolygon(
245
269
  currentX = startX;
246
270
  currentY = startY;
247
271
  break;
272
+ default:
273
+ break;
248
274
  }
249
275
  }
250
276
 
@@ -267,6 +293,9 @@ export function pathToPolygon(
267
293
  * // Returns: [{type: 'M', args: [10, 20]}, {type: 'L', args: [30, 40]}]
268
294
  */
269
295
  function parsePathCommands(pathData) {
296
+ if (typeof pathData !== 'string') {
297
+ throw new Error(`parsePathCommands: pathData must be a string, got ${typeof pathData}`);
298
+ }
270
299
  const commands = [];
271
300
  const regex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
272
301
  let match;
@@ -310,6 +339,16 @@ function parsePathCommands(pathData) {
310
339
  * // points now contains 20 sampled points along the cubic Bezier curve
311
340
  */
312
341
  function sampleCubicBezier(points, x0, y0, x1, y1, x2, y2, x3, y3, samples) {
342
+ if (!Array.isArray(points)) {
343
+ throw new Error('sampleCubicBezier: points must be an array');
344
+ }
345
+ if (typeof samples !== 'number' || samples <= 0 || !Number.isFinite(samples)) {
346
+ throw new Error(`sampleCubicBezier: samples must be a positive finite number, got ${samples}`);
347
+ }
348
+ if (!(x0 instanceof Decimal) || !(y0 instanceof Decimal) || !(x1 instanceof Decimal) || !(y1 instanceof Decimal) ||
349
+ !(x2 instanceof Decimal) || !(y2 instanceof Decimal) || !(x3 instanceof Decimal) || !(y3 instanceof Decimal)) {
350
+ throw new Error('sampleCubicBezier: all coordinate parameters must be Decimal instances');
351
+ }
313
352
  for (let i = 1; i <= samples; i++) {
314
353
  const t = D(i).div(samples);
315
354
  const mt = D(1).minus(t);
@@ -353,6 +392,16 @@ function sampleCubicBezier(points, x0, y0, x1, y1, x2, y2, x3, y3, samples) {
353
392
  * // points now contains 20 sampled points along the quadratic Bezier curve
354
393
  */
355
394
  function sampleQuadraticBezier(points, x0, y0, x1, y1, x2, y2, samples) {
395
+ if (!Array.isArray(points)) {
396
+ throw new Error('sampleQuadraticBezier: points must be an array');
397
+ }
398
+ if (typeof samples !== 'number' || samples <= 0 || !Number.isFinite(samples)) {
399
+ throw new Error(`sampleQuadraticBezier: samples must be a positive finite number, got ${samples}`);
400
+ }
401
+ if (!(x0 instanceof Decimal) || !(y0 instanceof Decimal) || !(x1 instanceof Decimal) ||
402
+ !(y1 instanceof Decimal) || !(x2 instanceof Decimal) || !(y2 instanceof Decimal)) {
403
+ throw new Error('sampleQuadraticBezier: all coordinate parameters must be Decimal instances');
404
+ }
356
405
  for (let i = 1; i <= samples; i++) {
357
406
  const t = D(i).div(samples);
358
407
  const mt = D(1).minus(t);
@@ -401,6 +450,22 @@ function sampleArc(
401
450
  y1,
402
451
  samples,
403
452
  ) {
453
+ if (!Array.isArray(points)) {
454
+ throw new Error('sampleArc: points must be an array');
455
+ }
456
+ if (typeof samples !== 'number' || samples <= 0 || !Number.isFinite(samples)) {
457
+ throw new Error(`sampleArc: samples must be a positive finite number, got ${samples}`);
458
+ }
459
+ if (!(x0 instanceof Decimal) || !(y0 instanceof Decimal) || !(rx instanceof Decimal) || !(ry instanceof Decimal) ||
460
+ !(xAxisRotation instanceof Decimal) || !(x1 instanceof Decimal) || !(y1 instanceof Decimal)) {
461
+ throw new Error('sampleArc: all coordinate and angle parameters must be Decimal instances');
462
+ }
463
+ if (typeof largeArc !== 'number' || (largeArc !== 0 && largeArc !== 1)) {
464
+ throw new Error(`sampleArc: largeArc must be 0 or 1, got ${largeArc}`);
465
+ }
466
+ if (typeof sweep !== 'number' || (sweep !== 0 && sweep !== 1)) {
467
+ throw new Error(`sampleArc: sweep must be 0 or 1, got ${sweep}`);
468
+ }
404
469
  if (rx.eq(0) || ry.eq(0)) {
405
470
  points.push(PolygonClip.point(x1, y1));
406
471
  return;
@@ -447,10 +512,20 @@ function sampleArc(
447
512
  const vx = x1p.neg().minus(cxp).div(rx),
448
513
  vy = y1p.neg().minus(cyp).div(ry);
449
514
  const n1 = ux.mul(ux).plus(uy.mul(uy)).sqrt();
515
+ if (n1.eq(0)) {
516
+ // Degenerate arc: start and end points are the same after transformation
517
+ points.push(PolygonClip.point(x1, y1));
518
+ return;
519
+ }
450
520
  let theta1 = Decimal.acos(ux.div(n1));
451
521
  if (uy.lt(0)) theta1 = theta1.neg();
452
522
 
453
523
  const n2 = n1.mul(vx.mul(vx).plus(vy.mul(vy)).sqrt());
524
+ if (n2.eq(0)) {
525
+ // Degenerate arc: endpoints coincide
526
+ points.push(PolygonClip.point(x1, y1));
527
+ return;
528
+ }
454
529
  let dtheta = Decimal.acos(ux.mul(vx).plus(uy.mul(vy)).div(n2));
455
530
  if (ux.mul(vy).minus(uy.mul(vx)).lt(0)) dtheta = dtheta.neg();
456
531
 
@@ -493,6 +568,9 @@ function sampleArc(
493
568
  * // Returns: [{x: D(0), y: D(0)}, {x: D(1), y: D(1)}]
494
569
  */
495
570
  function removeDuplicateConsecutive(points) {
571
+ if (!Array.isArray(points)) {
572
+ throw new Error('removeDuplicateConsecutive: points must be an array');
573
+ }
496
574
  if (points.length < 2) return points;
497
575
  const result = [points[0]];
498
576
  for (let i = 1; i < points.length; i++) {
@@ -545,6 +623,21 @@ export function shapeToPolygon(
545
623
  samples = DEFAULT_CURVE_SAMPLES,
546
624
  bezierArcs = 4,
547
625
  ) {
626
+ if (!element || typeof element !== 'object') {
627
+ throw new Error('shapeToPolygon: element must be an object');
628
+ }
629
+ if (!element.type || typeof element.type !== 'string') {
630
+ throw new Error('shapeToPolygon: element.type must be a string');
631
+ }
632
+ if (typeof samples !== 'number' || samples <= 0 || !Number.isFinite(samples)) {
633
+ throw new Error(`shapeToPolygon: samples must be a positive finite number, got ${samples}`);
634
+ }
635
+ if (typeof bezierArcs !== 'number' || bezierArcs <= 0 || !Number.isFinite(bezierArcs)) {
636
+ throw new Error(`shapeToPolygon: bezierArcs must be a positive finite number, got ${bezierArcs}`);
637
+ }
638
+ if (ctm !== null && !(ctm instanceof Matrix)) {
639
+ throw new Error('shapeToPolygon: ctm must be null or a Matrix instance');
640
+ }
548
641
  let pathData;
549
642
  switch (element.type) {
550
643
  case "circle":
@@ -687,7 +780,19 @@ export function resolveClipPath(
687
780
  ctm = null,
688
781
  options = {},
689
782
  ) {
783
+ if (!clipPathDef || typeof clipPathDef !== 'object') {
784
+ throw new Error('resolveClipPath: clipPathDef must be an object');
785
+ }
786
+ if (ctm !== null && !(ctm instanceof Matrix)) {
787
+ throw new Error('resolveClipPath: ctm must be null or a Matrix instance');
788
+ }
789
+ if (typeof options !== 'object' || options === null) {
790
+ throw new Error('resolveClipPath: options must be an object');
791
+ }
690
792
  const { samples = DEFAULT_CURVE_SAMPLES } = options;
793
+ if (typeof samples !== 'number' || samples <= 0 || !Number.isFinite(samples)) {
794
+ throw new Error(`resolveClipPath: samples must be a positive finite number, got ${samples}`);
795
+ }
691
796
  const clipPathUnits = clipPathDef.clipPathUnits || "userSpaceOnUse";
692
797
  let clipTransform = ctm ? ctm.clone() : Matrix.identity(3);
693
798
 
@@ -746,6 +851,18 @@ export function resolveClipPath(
746
851
  * @returns {Array} Clipped polygon(s), or array of polygon arrays for multi-region results
747
852
  */
748
853
  function clipPolygonWithRule(elementPolygon, clipPolygon, clipRule) {
854
+ if (!Array.isArray(elementPolygon)) {
855
+ throw new Error('clipPolygonWithRule: elementPolygon must be an array');
856
+ }
857
+ if (!Array.isArray(clipPolygon)) {
858
+ throw new Error('clipPolygonWithRule: clipPolygon must be an array');
859
+ }
860
+ if (clipRule !== 'nonzero' && clipRule !== 'evenodd') {
861
+ throw new Error(`clipPolygonWithRule: clipRule must be 'nonzero' or 'evenodd', got ${clipRule}`);
862
+ }
863
+ if (elementPolygon.length < 3 || clipPolygon.length < 3) {
864
+ return [];
865
+ }
749
866
  // For nonzero rule, standard intersection works correctly
750
867
  // because polygonIntersection uses the winding number test internally
751
868
  if (clipRule === "nonzero") {
@@ -789,6 +906,12 @@ function clipPolygonWithRule(elementPolygon, clipPolygon, clipRule) {
789
906
  * @private
790
907
  */
791
908
  function computeCentroid(polygon) {
909
+ if (!Array.isArray(polygon)) {
910
+ throw new Error('computeCentroid: polygon must be an array');
911
+ }
912
+ if (polygon.length === 0) {
913
+ throw new Error('computeCentroid: polygon must not be empty');
914
+ }
792
915
  let cx = new Decimal(0);
793
916
  let cy = new Decimal(0);
794
917
  let area = new Decimal(0);
@@ -874,7 +997,25 @@ function computeCentroid(polygon) {
874
997
  * const clipped = applyClipPath(ellipse, clipDef, null, { samples: 50 });
875
998
  */
876
999
  export function applyClipPath(element, clipPathDef, ctm = null, options = {}) {
1000
+ if (!element || typeof element !== 'object') {
1001
+ throw new Error('applyClipPath: element must be an object');
1002
+ }
1003
+ if (!clipPathDef || typeof clipPathDef !== 'object') {
1004
+ throw new Error('applyClipPath: clipPathDef must be an object');
1005
+ }
1006
+ if (ctm !== null && !(ctm instanceof Matrix)) {
1007
+ throw new Error('applyClipPath: ctm must be null or a Matrix instance');
1008
+ }
1009
+ if (typeof options !== 'object' || options === null) {
1010
+ throw new Error('applyClipPath: options must be an object');
1011
+ }
877
1012
  const { samples = DEFAULT_CURVE_SAMPLES, clipRule = "nonzero" } = options;
1013
+ if (typeof samples !== 'number' || samples <= 0 || !Number.isFinite(samples)) {
1014
+ throw new Error(`applyClipPath: samples must be a positive finite number, got ${samples}`);
1015
+ }
1016
+ if (clipRule !== 'nonzero' && clipRule !== 'evenodd') {
1017
+ throw new Error(`applyClipPath: clipRule must be 'nonzero' or 'evenodd', got ${clipRule}`);
1018
+ }
878
1019
  const clipPolygon = resolveClipPath(clipPathDef, element, ctm, options);
879
1020
  if (clipPolygon.length < 3) return [];
880
1021
 
@@ -910,6 +1051,12 @@ export function applyClipPath(element, clipPathDef, ctm = null, options = {}) {
910
1051
  * // Returns: {x: Decimal(25), y: Decimal(25), width: Decimal(50), height: Decimal(50)}
911
1052
  */
912
1053
  function getElementBoundingBox(element) {
1054
+ if (!element || typeof element !== 'object') {
1055
+ throw new Error('getElementBoundingBox: element must be an object');
1056
+ }
1057
+ if (!element.type || typeof element.type !== 'string') {
1058
+ throw new Error('getElementBoundingBox: element.type must be a string');
1059
+ }
913
1060
  switch (element.type) {
914
1061
  case "rect":
915
1062
  return {
@@ -988,6 +1135,12 @@ function getElementBoundingBox(element) {
988
1135
  * // Returns: "M 0.12345679 0.98765432 Z"
989
1136
  */
990
1137
  export function polygonToPathData(polygon, precision = 6) {
1138
+ if (!Array.isArray(polygon)) {
1139
+ throw new Error('polygonToPathData: polygon must be an array');
1140
+ }
1141
+ if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
1142
+ throw new Error(`polygonToPathData: precision must be a non-negative finite number, got ${precision}`);
1143
+ }
991
1144
  if (polygon.length < 2) return "";
992
1145
  const fmt = (n) =>
993
1146
  (n instanceof Decimal ? n : D(n)).toFixed(precision).replace(/\.?0+$/, "");
@@ -1054,6 +1207,21 @@ export function resolveNestedClipPath(
1054
1207
  visited = new Set(),
1055
1208
  options = {},
1056
1209
  ) {
1210
+ if (!clipPathDef || typeof clipPathDef !== 'object') {
1211
+ throw new Error('resolveNestedClipPath: clipPathDef must be an object');
1212
+ }
1213
+ if (!(defsMap instanceof Map)) {
1214
+ throw new Error('resolveNestedClipPath: defsMap must be a Map');
1215
+ }
1216
+ if (ctm !== null && !(ctm instanceof Matrix)) {
1217
+ throw new Error('resolveNestedClipPath: ctm must be null or a Matrix instance');
1218
+ }
1219
+ if (!(visited instanceof Set)) {
1220
+ throw new Error('resolveNestedClipPath: visited must be a Set');
1221
+ }
1222
+ if (typeof options !== 'object' || options === null) {
1223
+ throw new Error('resolveNestedClipPath: options must be an object');
1224
+ }
1057
1225
  const clipId = clipPathDef.id;
1058
1226
  if (clipId && visited.has(clipId)) {
1059
1227
  Logger.warn(`Circular clipPath reference detected: ${clipId}`);