@emasoft/svg-matrix 1.0.28 → 1.0.30

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 (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +985 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +723 -180
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +18 -7
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +22 -18
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16381 -3370
  34. package/src/svg2-polyfills.js +93 -224
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
@@ -27,10 +27,10 @@
27
27
  * @module clip-path-resolver
28
28
  */
29
29
 
30
- import Decimal from 'decimal.js';
31
- import { Matrix } from './matrix.js';
32
- import * as Transforms2D from './transforms2d.js';
33
- import * as PolygonClip from './polygon-clip.js';
30
+ import Decimal from "decimal.js";
31
+ import { Matrix } from "./matrix.js";
32
+ import * as Transforms2D from "./transforms2d.js";
33
+ import * as PolygonClip from "./polygon-clip.js";
34
34
  import {
35
35
  circleToPath,
36
36
  ellipseToPath,
@@ -39,14 +39,11 @@ import {
39
39
  polygonToPath,
40
40
  polylineToPath,
41
41
  parseTransformAttribute,
42
- transformPathData
43
- } from './svg-flatten.js';
44
- import {
45
- circleToPathDataHP,
46
- ellipseToPathDataHP
47
- } from './geometry-to-path.js';
48
- import { Logger } from './logger.js';
49
- import { FillRule, pointInPolygonWithRule } from './svg-boolean-ops.js';
42
+ transformPathData,
43
+ } from "./svg-flatten.js";
44
+ import { circleToPathDataHP, ellipseToPathDataHP } from "./geometry-to-path.js";
45
+ import { Logger } from "./logger.js";
46
+ import { FillRule, pointInPolygonWithRule } from "./svg-boolean-ops.js";
50
47
 
51
48
  // Alias for cleaner code
52
49
  const parseTransform = parseTransformAttribute;
@@ -59,7 +56,7 @@ Decimal.set({ precision: 80 });
59
56
  * @param {number|string|Decimal} x - Value to convert
60
57
  * @returns {Decimal} Decimal instance
61
58
  */
62
- const D = x => (x instanceof Decimal ? x : new Decimal(x));
59
+ const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
63
60
 
64
61
  /**
65
62
  * Default number of sample points per curve segment (for Bezier curves and arcs).
@@ -93,10 +90,15 @@ const DEFAULT_CURVE_SAMPLES = 20;
93
90
  * const smoothPolygon = pathToPolygon("M 0 0 C 50 0 50 100 100 100", 50);
94
91
  * // Higher sampling (50 points) creates smoother curve approximation
95
92
  */
96
- export function pathToPolygon(pathData, samplesPerCurve = DEFAULT_CURVE_SAMPLES) {
93
+ export function pathToPolygon(
94
+ pathData,
95
+ samplesPerCurve = DEFAULT_CURVE_SAMPLES,
96
+ ) {
97
97
  const points = [];
98
- let currentX = D(0), currentY = D(0);
99
- let startX = D(0), startY = D(0);
98
+ let currentX = D(0),
99
+ currentY = D(0);
100
+ let startX = D(0),
101
+ startY = D(0);
100
102
 
101
103
  const commands = parsePathCommands(pathData);
102
104
 
@@ -104,81 +106,144 @@ export function pathToPolygon(pathData, samplesPerCurve = DEFAULT_CURVE_SAMPLES)
104
106
  const { type, args } = cmd;
105
107
 
106
108
  switch (type) {
107
- case 'M':
108
- currentX = D(args[0]); currentY = D(args[1]);
109
- startX = currentX; startY = currentY;
109
+ case "M":
110
+ currentX = D(args[0]);
111
+ currentY = D(args[1]);
112
+ startX = currentX;
113
+ startY = currentY;
110
114
  points.push(PolygonClip.point(currentX, currentY));
111
115
  break;
112
- case 'm':
113
- currentX = currentX.plus(args[0]); currentY = currentY.plus(args[1]);
114
- startX = currentX; startY = currentY;
116
+ case "m":
117
+ currentX = currentX.plus(args[0]);
118
+ currentY = currentY.plus(args[1]);
119
+ startX = currentX;
120
+ startY = currentY;
115
121
  points.push(PolygonClip.point(currentX, currentY));
116
122
  break;
117
- case 'L':
118
- currentX = D(args[0]); currentY = D(args[1]);
123
+ case "L":
124
+ currentX = D(args[0]);
125
+ currentY = D(args[1]);
119
126
  points.push(PolygonClip.point(currentX, currentY));
120
127
  break;
121
- case 'l':
122
- currentX = currentX.plus(args[0]); currentY = currentY.plus(args[1]);
128
+ case "l":
129
+ currentX = currentX.plus(args[0]);
130
+ currentY = currentY.plus(args[1]);
123
131
  points.push(PolygonClip.point(currentX, currentY));
124
132
  break;
125
- case 'H':
133
+ case "H":
126
134
  currentX = D(args[0]);
127
135
  points.push(PolygonClip.point(currentX, currentY));
128
136
  break;
129
- case 'h':
137
+ case "h":
130
138
  currentX = currentX.plus(args[0]);
131
139
  points.push(PolygonClip.point(currentX, currentY));
132
140
  break;
133
- case 'V':
141
+ case "V":
134
142
  currentY = D(args[0]);
135
143
  points.push(PolygonClip.point(currentX, currentY));
136
144
  break;
137
- case 'v':
145
+ case "v":
138
146
  currentY = currentY.plus(args[0]);
139
147
  points.push(PolygonClip.point(currentX, currentY));
140
148
  break;
141
- case 'C':
142
- sampleCubicBezier(points, currentX, currentY,
143
- D(args[0]), D(args[1]), D(args[2]), D(args[3]), D(args[4]), D(args[5]),
144
- samplesPerCurve);
145
- currentX = D(args[4]); currentY = D(args[5]);
149
+ case "C":
150
+ sampleCubicBezier(
151
+ points,
152
+ currentX,
153
+ currentY,
154
+ D(args[0]),
155
+ D(args[1]),
156
+ D(args[2]),
157
+ D(args[3]),
158
+ D(args[4]),
159
+ D(args[5]),
160
+ samplesPerCurve,
161
+ );
162
+ currentX = D(args[4]);
163
+ currentY = D(args[5]);
146
164
  break;
147
- case 'c':
148
- sampleCubicBezier(points, currentX, currentY,
149
- currentX.plus(args[0]), currentY.plus(args[1]),
150
- currentX.plus(args[2]), currentY.plus(args[3]),
151
- currentX.plus(args[4]), currentY.plus(args[5]),
152
- samplesPerCurve);
153
- currentX = currentX.plus(args[4]); currentY = currentY.plus(args[5]);
165
+ case "c":
166
+ sampleCubicBezier(
167
+ points,
168
+ currentX,
169
+ currentY,
170
+ currentX.plus(args[0]),
171
+ currentY.plus(args[1]),
172
+ currentX.plus(args[2]),
173
+ currentY.plus(args[3]),
174
+ currentX.plus(args[4]),
175
+ currentY.plus(args[5]),
176
+ samplesPerCurve,
177
+ );
178
+ currentX = currentX.plus(args[4]);
179
+ currentY = currentY.plus(args[5]);
154
180
  break;
155
- case 'Q':
156
- sampleQuadraticBezier(points, currentX, currentY,
157
- D(args[0]), D(args[1]), D(args[2]), D(args[3]),
158
- samplesPerCurve);
159
- currentX = D(args[2]); currentY = D(args[3]);
181
+ case "Q":
182
+ sampleQuadraticBezier(
183
+ points,
184
+ currentX,
185
+ currentY,
186
+ D(args[0]),
187
+ D(args[1]),
188
+ D(args[2]),
189
+ D(args[3]),
190
+ samplesPerCurve,
191
+ );
192
+ currentX = D(args[2]);
193
+ currentY = D(args[3]);
160
194
  break;
161
- case 'q':
162
- sampleQuadraticBezier(points, currentX, currentY,
163
- currentX.plus(args[0]), currentY.plus(args[1]),
164
- currentX.plus(args[2]), currentY.plus(args[3]),
165
- samplesPerCurve);
166
- currentX = currentX.plus(args[2]); currentY = currentY.plus(args[3]);
195
+ case "q":
196
+ sampleQuadraticBezier(
197
+ points,
198
+ currentX,
199
+ currentY,
200
+ currentX.plus(args[0]),
201
+ currentY.plus(args[1]),
202
+ currentX.plus(args[2]),
203
+ currentY.plus(args[3]),
204
+ samplesPerCurve,
205
+ );
206
+ currentX = currentX.plus(args[2]);
207
+ currentY = currentY.plus(args[3]);
167
208
  break;
168
- case 'A':
169
- sampleArc(points, currentX, currentY,
170
- D(args[0]), D(args[1]), D(args[2]), args[3], args[4],
171
- D(args[5]), D(args[6]), samplesPerCurve);
172
- currentX = D(args[5]); currentY = D(args[6]);
209
+ case "A":
210
+ sampleArc(
211
+ points,
212
+ currentX,
213
+ currentY,
214
+ D(args[0]),
215
+ D(args[1]),
216
+ D(args[2]),
217
+ args[3],
218
+ args[4],
219
+ D(args[5]),
220
+ D(args[6]),
221
+ samplesPerCurve,
222
+ );
223
+ currentX = D(args[5]);
224
+ currentY = D(args[6]);
173
225
  break;
174
- case 'a':
175
- sampleArc(points, currentX, currentY,
176
- D(args[0]), D(args[1]), D(args[2]), args[3], args[4],
177
- currentX.plus(args[5]), currentY.plus(args[6]), samplesPerCurve);
178
- currentX = currentX.plus(args[5]); currentY = currentY.plus(args[6]);
226
+ case "a":
227
+ sampleArc(
228
+ points,
229
+ currentX,
230
+ currentY,
231
+ D(args[0]),
232
+ D(args[1]),
233
+ D(args[2]),
234
+ args[3],
235
+ args[4],
236
+ currentX.plus(args[5]),
237
+ currentY.plus(args[6]),
238
+ samplesPerCurve,
239
+ );
240
+ currentX = currentX.plus(args[5]);
241
+ currentY = currentY.plus(args[6]);
179
242
  break;
180
- case 'Z': case 'z':
181
- currentX = startX; currentY = startY;
243
+ case "Z":
244
+ case "z":
245
+ currentX = startX;
246
+ currentY = startY;
182
247
  break;
183
248
  }
184
249
  }
@@ -208,9 +273,14 @@ function parsePathCommands(pathData) {
208
273
  while ((match = regex.exec(pathData)) !== null) {
209
274
  const type = match[1];
210
275
  const argsStr = match[2].trim();
211
- const args = argsStr.length > 0
212
- ? argsStr.split(/[\s,]+/).filter(s => s.length > 0).map(Number)
213
- : [];
276
+
277
+ // FIX: Use regex to extract numbers, handles implicit negative separators (e.g., "0.8-2.9" -> ["0.8", "-2.9"])
278
+ // Per W3C SVG spec, negative signs can act as delimiters without spaces
279
+ const numRegex = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
280
+ const args =
281
+ argsStr.length > 0
282
+ ? Array.from(argsStr.matchAll(numRegex), (m) => Number(m[0]))
283
+ : [];
214
284
  commands.push({ type, args });
215
285
  }
216
286
  return commands;
@@ -243,12 +313,20 @@ function sampleCubicBezier(points, x0, y0, x1, y1, x2, y2, x3, y3, samples) {
243
313
  for (let i = 1; i <= samples; i++) {
244
314
  const t = D(i).div(samples);
245
315
  const mt = D(1).minus(t);
246
- const mt2 = mt.mul(mt), mt3 = mt2.mul(mt);
247
- const t2 = t.mul(t), t3 = t2.mul(t);
248
- const x = mt3.mul(x0).plus(D(3).mul(mt2).mul(t).mul(x1))
249
- .plus(D(3).mul(mt).mul(t2).mul(x2)).plus(t3.mul(x3));
250
- const y = mt3.mul(y0).plus(D(3).mul(mt2).mul(t).mul(y1))
251
- .plus(D(3).mul(mt).mul(t2).mul(y2)).plus(t3.mul(y3));
316
+ const mt2 = mt.mul(mt),
317
+ mt3 = mt2.mul(mt);
318
+ const t2 = t.mul(t),
319
+ t3 = t2.mul(t);
320
+ const x = mt3
321
+ .mul(x0)
322
+ .plus(D(3).mul(mt2).mul(t).mul(x1))
323
+ .plus(D(3).mul(mt).mul(t2).mul(x2))
324
+ .plus(t3.mul(x3));
325
+ const y = mt3
326
+ .mul(y0)
327
+ .plus(D(3).mul(mt2).mul(t).mul(y1))
328
+ .plus(D(3).mul(mt).mul(t2).mul(y2))
329
+ .plus(t3.mul(y3));
252
330
  points.push(PolygonClip.point(x, y));
253
331
  }
254
332
  }
@@ -278,7 +356,8 @@ function sampleQuadraticBezier(points, x0, y0, x1, y1, x2, y2, samples) {
278
356
  for (let i = 1; i <= samples; i++) {
279
357
  const t = D(i).div(samples);
280
358
  const mt = D(1).minus(t);
281
- const mt2 = mt.mul(mt), t2 = t.mul(t);
359
+ const mt2 = mt.mul(mt),
360
+ t2 = t.mul(t);
282
361
  const x = mt2.mul(x0).plus(D(2).mul(mt).mul(t).mul(x1)).plus(t2.mul(x2));
283
362
  const y = mt2.mul(y0).plus(D(2).mul(mt).mul(t).mul(y1)).plus(t2.mul(y2));
284
363
  points.push(PolygonClip.point(x, y));
@@ -309,23 +388,45 @@ function sampleQuadraticBezier(points, x0, y0, x1, y1, x2, y2, samples) {
309
388
  * // Sample an arc from (0,0) to (100,100) with radii 50,50
310
389
  * sampleArc(points, D(0), D(0), D(50), D(50), D(0), 0, 1, D(100), D(100), 20);
311
390
  */
312
- function sampleArc(points, x0, y0, rx, ry, xAxisRotation, largeArc, sweep, x1, y1, samples) {
313
- if (rx.eq(0) || ry.eq(0)) { points.push(PolygonClip.point(x1, y1)); return; }
314
- rx = rx.abs(); ry = ry.abs();
391
+ function sampleArc(
392
+ points,
393
+ x0,
394
+ y0,
395
+ rx,
396
+ ry,
397
+ xAxisRotation,
398
+ largeArc,
399
+ sweep,
400
+ x1,
401
+ y1,
402
+ samples,
403
+ ) {
404
+ if (rx.eq(0) || ry.eq(0)) {
405
+ points.push(PolygonClip.point(x1, y1));
406
+ return;
407
+ }
408
+ let rxLocal = rx.abs();
409
+ let ryLocal = ry.abs();
315
410
 
316
411
  const phi = xAxisRotation.mul(Math.PI).div(180);
317
- const cosPhi = phi.cos(), sinPhi = phi.sin();
318
- const dx = x0.minus(x1).div(2), dy = y0.minus(y1).div(2);
412
+ const cosPhi = phi.cos(),
413
+ sinPhi = phi.sin();
414
+ const dx = x0.minus(x1).div(2),
415
+ dy = y0.minus(y1).div(2);
319
416
  const x1p = cosPhi.mul(dx).plus(sinPhi.mul(dy));
320
417
  const y1p = cosPhi.mul(dy).minus(sinPhi.mul(dx));
321
418
 
322
- let rx2 = rx.mul(rx), ry2 = ry.mul(ry);
323
- const x1p2 = x1p.mul(x1p), y1p2 = y1p.mul(y1p);
419
+ let rx2 = rxLocal.mul(rxLocal),
420
+ ry2 = ryLocal.mul(ryLocal);
421
+ const x1p2 = x1p.mul(x1p),
422
+ y1p2 = y1p.mul(y1p);
324
423
  const lambda = x1p2.div(rx2).plus(y1p2.div(ry2));
325
424
  if (lambda.gt(1)) {
326
425
  const sqrtLambda = lambda.sqrt();
327
- rx = rx.mul(sqrtLambda); ry = ry.mul(sqrtLambda);
328
- rx2 = rx.mul(rx); ry2 = ry.mul(ry);
426
+ rxLocal = rxLocal.mul(sqrtLambda);
427
+ ryLocal = ryLocal.mul(sqrtLambda);
428
+ rx2 = rxLocal.mul(rxLocal);
429
+ ry2 = ryLocal.mul(ryLocal);
329
430
  }
330
431
 
331
432
  let sq = rx2.mul(ry2).minus(rx2.mul(y1p2)).minus(ry2.mul(x1p2));
@@ -336,12 +437,15 @@ function sampleArc(points, x0, y0, rx, ry, xAxisRotation, largeArc, sweep, x1, y
336
437
 
337
438
  const cxp = sq.mul(rx).mul(y1p).div(ry);
338
439
  const cyp = sq.neg().mul(ry).mul(x1p).div(rx);
339
- const midX = x0.plus(x1).div(2), midY = y0.plus(y1).div(2);
440
+ const midX = x0.plus(x1).div(2),
441
+ midY = y0.plus(y1).div(2);
340
442
  const cx = cosPhi.mul(cxp).minus(sinPhi.mul(cyp)).plus(midX);
341
443
  const cy = sinPhi.mul(cxp).plus(cosPhi.mul(cyp)).plus(midY);
342
444
 
343
- const ux = x1p.minus(cxp).div(rx), uy = y1p.minus(cyp).div(ry);
344
- const vx = x1p.neg().minus(cxp).div(rx), vy = y1p.neg().minus(cyp).div(ry);
445
+ const ux = x1p.minus(cxp).div(rx),
446
+ uy = y1p.minus(cyp).div(ry);
447
+ const vx = x1p.neg().minus(cxp).div(rx),
448
+ vy = y1p.neg().minus(cyp).div(ry);
345
449
  const n1 = ux.mul(ux).plus(uy.mul(uy)).sqrt();
346
450
  let theta1 = Decimal.acos(ux.div(n1));
347
451
  if (uy.lt(0)) theta1 = theta1.neg();
@@ -350,16 +454,24 @@ function sampleArc(points, x0, y0, rx, ry, xAxisRotation, largeArc, sweep, x1, y
350
454
  let dtheta = Decimal.acos(ux.mul(vx).plus(uy.mul(vy)).div(n2));
351
455
  if (ux.mul(vy).minus(uy.mul(vx)).lt(0)) dtheta = dtheta.neg();
352
456
 
353
- const PI = D(Math.PI), TWO_PI = PI.mul(2);
457
+ const PI = D(Math.PI),
458
+ TWO_PI = PI.mul(2);
354
459
  if (sweep && dtheta.lt(0)) dtheta = dtheta.plus(TWO_PI);
355
460
  else if (!sweep && dtheta.gt(0)) dtheta = dtheta.minus(TWO_PI);
356
461
 
357
462
  for (let i = 1; i <= samples; i++) {
358
463
  const t = D(i).div(samples);
359
464
  const theta = theta1.plus(dtheta.mul(t));
360
- const cosTheta = theta.cos(), sinTheta = theta.sin();
361
- const x = cosPhi.mul(rx.mul(cosTheta)).minus(sinPhi.mul(ry.mul(sinTheta))).plus(cx);
362
- const y = sinPhi.mul(rx.mul(cosTheta)).plus(cosPhi.mul(ry.mul(sinTheta))).plus(cy);
465
+ const cosTheta = theta.cos(),
466
+ sinTheta = theta.sin();
467
+ const x = cosPhi
468
+ .mul(rx.mul(cosTheta))
469
+ .minus(sinPhi.mul(ry.mul(sinTheta)))
470
+ .plus(cx);
471
+ const y = sinPhi
472
+ .mul(rx.mul(cosTheta))
473
+ .plus(cosPhi.mul(ry.mul(sinTheta)))
474
+ .plus(cy);
363
475
  points.push(PolygonClip.point(x, y));
364
476
  }
365
477
  }
@@ -427,40 +539,78 @@ function removeDuplicateConsecutive(points) {
427
539
  * @param {number} samples - Samples per curve for polygon conversion
428
540
  * @param {number} bezierArcs - Number of Bezier arcs for circles/ellipses (4=standard, 16 or 64=HP)
429
541
  */
430
- export function shapeToPolygon(element, ctm = null, samples = DEFAULT_CURVE_SAMPLES, bezierArcs = 4) {
542
+ export function shapeToPolygon(
543
+ element,
544
+ ctm = null,
545
+ samples = DEFAULT_CURVE_SAMPLES,
546
+ bezierArcs = 4,
547
+ ) {
431
548
  let pathData;
432
549
  switch (element.type) {
433
- case 'circle':
550
+ case "circle":
434
551
  // Use high-precision Bezier arcs for better curve approximation
435
552
  if (bezierArcs > 4) {
436
- pathData = circleToPathDataHP(element.cx || 0, element.cy || 0, element.r || 0, bezierArcs, 10);
553
+ pathData = circleToPathDataHP(
554
+ element.cx || 0,
555
+ element.cy || 0,
556
+ element.r || 0,
557
+ bezierArcs,
558
+ 10,
559
+ );
437
560
  } else {
438
- pathData = circleToPath(D(element.cx || 0), D(element.cy || 0), D(element.r || 0));
561
+ pathData = circleToPath(
562
+ D(element.cx || 0),
563
+ D(element.cy || 0),
564
+ D(element.r || 0),
565
+ );
439
566
  }
440
567
  break;
441
- case 'ellipse':
568
+ case "ellipse":
442
569
  // Use high-precision Bezier arcs for better curve approximation
443
570
  if (bezierArcs > 4) {
444
- pathData = ellipseToPathDataHP(element.cx || 0, element.cy || 0, element.rx || 0, element.ry || 0, bezierArcs, 10);
571
+ pathData = ellipseToPathDataHP(
572
+ element.cx || 0,
573
+ element.cy || 0,
574
+ element.rx || 0,
575
+ element.ry || 0,
576
+ bezierArcs,
577
+ 10,
578
+ );
445
579
  } else {
446
- pathData = ellipseToPath(D(element.cx || 0), D(element.cy || 0), D(element.rx || 0), D(element.ry || 0));
580
+ pathData = ellipseToPath(
581
+ D(element.cx || 0),
582
+ D(element.cy || 0),
583
+ D(element.rx || 0),
584
+ D(element.ry || 0),
585
+ );
447
586
  }
448
587
  break;
449
- case 'rect':
450
- pathData = rectToPath(D(element.x || 0), D(element.y || 0), D(element.width || 0), D(element.height || 0),
451
- D(element.rx || 0), element.ry !== undefined ? D(element.ry) : null);
588
+ case "rect":
589
+ pathData = rectToPath(
590
+ D(element.x || 0),
591
+ D(element.y || 0),
592
+ D(element.width || 0),
593
+ D(element.height || 0),
594
+ D(element.rx || 0),
595
+ element.ry !== undefined ? D(element.ry) : null,
596
+ );
452
597
  break;
453
- case 'line':
454
- pathData = lineToPath(D(element.x1 || 0), D(element.y1 || 0), D(element.x2 || 0), D(element.y2 || 0));
598
+ case "line":
599
+ pathData = lineToPath(
600
+ D(element.x1 || 0),
601
+ D(element.y1 || 0),
602
+ D(element.x2 || 0),
603
+ D(element.y2 || 0),
604
+ );
455
605
  break;
456
- case 'polygon':
457
- pathData = polygonToPath(element.points || '');
606
+ case "polygon":
607
+ pathData = polygonToPath(element.points || "");
458
608
  break;
459
- case 'polyline':
460
- pathData = polylineToPath(element.points || '');
609
+ case "polyline":
610
+ pathData = polylineToPath(element.points || "");
461
611
  break;
462
- case 'path':
463
- pathData = element.d || '';
612
+ case "path":
613
+ pathData = element.d || "";
464
614
  break;
465
615
  default:
466
616
  return [];
@@ -531,9 +681,14 @@ export function shapeToPolygon(element, ctm = null, samples = DEFAULT_CURVE_SAMP
531
681
  * const clipPolygon = resolveClipPath(clipDef, target);
532
682
  * // Circle will be scaled to bbox: center at (100,50), radius 40 (0.4 * min(200,100))
533
683
  */
534
- export function resolveClipPath(clipPathDef, targetElement, ctm = null, options = {}) {
684
+ export function resolveClipPath(
685
+ clipPathDef,
686
+ targetElement,
687
+ ctm = null,
688
+ options = {},
689
+ ) {
535
690
  const { samples = DEFAULT_CURVE_SAMPLES } = options;
536
- const clipPathUnits = clipPathDef.clipPathUnits || 'userSpaceOnUse';
691
+ const clipPathUnits = clipPathDef.clipPathUnits || "userSpaceOnUse";
537
692
  let clipTransform = ctm ? ctm.clone() : Matrix.identity(3);
538
693
 
539
694
  if (clipPathDef.transform) {
@@ -541,17 +696,18 @@ export function resolveClipPath(clipPathDef, targetElement, ctm = null, options
541
696
  clipTransform = clipTransform.mul(clipPathTransformMatrix);
542
697
  }
543
698
 
544
- if (clipPathUnits === 'objectBoundingBox' && targetElement) {
699
+ if (clipPathUnits === "objectBoundingBox" && targetElement) {
545
700
  const bbox = getElementBoundingBox(targetElement);
546
701
  if (bbox) {
547
- const bboxTransform = Transforms2D.translation(bbox.x, bbox.y)
548
- .mul(Transforms2D.scale(bbox.width, bbox.height));
702
+ const bboxTransform = Transforms2D.translation(bbox.x, bbox.y).mul(
703
+ Transforms2D.scale(bbox.width, bbox.height),
704
+ );
549
705
  clipTransform = clipTransform.mul(bboxTransform);
550
706
  }
551
707
  }
552
708
 
553
709
  const clipPolygons = [];
554
- for (const child of (clipPathDef.children || [])) {
710
+ for (const child of clipPathDef.children || []) {
555
711
  const polygon = shapeToPolygon(child, clipTransform, samples);
556
712
  if (polygon.length >= 3) clipPolygons.push(polygon);
557
713
  }
@@ -592,14 +748,17 @@ export function resolveClipPath(clipPathDef, targetElement, ctm = null, options
592
748
  function clipPolygonWithRule(elementPolygon, clipPolygon, clipRule) {
593
749
  // For nonzero rule, standard intersection works correctly
594
750
  // because polygonIntersection uses the winding number test internally
595
- if (clipRule === 'nonzero') {
751
+ if (clipRule === "nonzero") {
596
752
  return PolygonClip.polygonIntersection(elementPolygon, clipPolygon);
597
753
  }
598
754
 
599
755
  // For evenodd rule with self-intersecting clip paths, we need a different approach
600
756
  // The idea: filter vertices of the intersection result by the evenodd test
601
757
  // First get the standard intersection
602
- const intersection = PolygonClip.polygonIntersection(elementPolygon, clipPolygon);
758
+ const intersection = PolygonClip.polygonIntersection(
759
+ elementPolygon,
760
+ clipPolygon,
761
+ );
603
762
  if (intersection.length === 0) return [];
604
763
 
605
764
  // For each resulting polygon, check if its centroid is inside according to evenodd
@@ -612,7 +771,8 @@ function clipPolygonWithRule(elementPolygon, clipPolygon, clipRule) {
612
771
  const centroid = computeCentroid(poly);
613
772
 
614
773
  // Test if centroid is inside the clip polygon according to evenodd rule
615
- const fillRule = clipRule === 'evenodd' ? FillRule.EVENODD : FillRule.NONZERO;
774
+ const fillRule =
775
+ clipRule === "evenodd" ? FillRule.EVENODD : FillRule.NONZERO;
616
776
  const inside = pointInPolygonWithRule(centroid, clipPolygon, fillRule);
617
777
 
618
778
  // If centroid is inside (1) or on boundary (0), keep this polygon
@@ -651,7 +811,10 @@ function computeCentroid(polygon) {
651
811
  sumX = sumX.plus(p.x);
652
812
  sumY = sumY.plus(p.y);
653
813
  }
654
- return PolygonClip.point(sumX.div(polygon.length), sumY.div(polygon.length));
814
+ return PolygonClip.point(
815
+ sumX.div(polygon.length),
816
+ sumY.div(polygon.length),
817
+ );
655
818
  }
656
819
 
657
820
  const factor = new Decimal(1).div(area.times(6));
@@ -711,7 +874,7 @@ function computeCentroid(polygon) {
711
874
  * const clipped = applyClipPath(ellipse, clipDef, null, { samples: 50 });
712
875
  */
713
876
  export function applyClipPath(element, clipPathDef, ctm = null, options = {}) {
714
- const { samples = DEFAULT_CURVE_SAMPLES, clipRule = 'nonzero' } = options;
877
+ const { samples = DEFAULT_CURVE_SAMPLES, clipRule = "nonzero" } = options;
715
878
  const clipPolygon = resolveClipPath(clipPathDef, element, ctm, options);
716
879
  if (clipPolygon.length < 3) return [];
717
880
 
@@ -748,24 +911,46 @@ export function applyClipPath(element, clipPathDef, ctm = null, options = {}) {
748
911
  */
749
912
  function getElementBoundingBox(element) {
750
913
  switch (element.type) {
751
- case 'rect':
752
- return { x: D(element.x || 0), y: D(element.y || 0),
753
- width: D(element.width || 0), height: D(element.height || 0) };
754
- case 'circle': {
755
- const cx = D(element.cx || 0), cy = D(element.cy || 0), r = D(element.r || 0);
756
- return { x: cx.minus(r), y: cy.minus(r), width: r.mul(2), height: r.mul(2) };
914
+ case "rect":
915
+ return {
916
+ x: D(element.x || 0),
917
+ y: D(element.y || 0),
918
+ width: D(element.width || 0),
919
+ height: D(element.height || 0),
920
+ };
921
+ case "circle": {
922
+ const cx = D(element.cx || 0),
923
+ cy = D(element.cy || 0),
924
+ r = D(element.r || 0);
925
+ return {
926
+ x: cx.minus(r),
927
+ y: cy.minus(r),
928
+ width: r.mul(2),
929
+ height: r.mul(2),
930
+ };
757
931
  }
758
- case 'ellipse': {
759
- const cx = D(element.cx || 0), cy = D(element.cy || 0);
760
- const rx = D(element.rx || 0), ry = D(element.ry || 0);
761
- return { x: cx.minus(rx), y: cy.minus(ry), width: rx.mul(2), height: ry.mul(2) };
932
+ case "ellipse": {
933
+ const cx = D(element.cx || 0),
934
+ cy = D(element.cy || 0);
935
+ const rx = D(element.rx || 0),
936
+ ry = D(element.ry || 0);
937
+ return {
938
+ x: cx.minus(rx),
939
+ y: cy.minus(ry),
940
+ width: rx.mul(2),
941
+ height: ry.mul(2),
942
+ };
762
943
  }
763
944
  default: {
764
945
  const polygon = shapeToPolygon(element, null, 10);
765
946
  if (polygon.length > 0) {
766
947
  const bbox = PolygonClip.boundingBox(polygon);
767
- return { x: bbox.minX, y: bbox.minY,
768
- width: bbox.maxX.minus(bbox.minX), height: bbox.maxY.minus(bbox.minY) };
948
+ return {
949
+ x: bbox.minX,
950
+ y: bbox.minY,
951
+ width: bbox.maxX.minus(bbox.minX),
952
+ height: bbox.maxY.minus(bbox.minY),
953
+ };
769
954
  }
770
955
  return null;
771
956
  }
@@ -803,13 +988,14 @@ function getElementBoundingBox(element) {
803
988
  * // Returns: "M 0.12345679 0.98765432 Z"
804
989
  */
805
990
  export function polygonToPathData(polygon, precision = 6) {
806
- if (polygon.length < 2) return '';
807
- const fmt = n => (n instanceof Decimal ? n : D(n)).toFixed(precision).replace(/\.?0+$/, '');
991
+ if (polygon.length < 2) return "";
992
+ const fmt = (n) =>
993
+ (n instanceof Decimal ? n : D(n)).toFixed(precision).replace(/\.?0+$/, "");
808
994
  let d = `M ${fmt(polygon[0].x)} ${fmt(polygon[0].y)}`;
809
995
  for (let i = 1; i < polygon.length; i++) {
810
996
  d += ` L ${fmt(polygon[i].x)} ${fmt(polygon[i].y)}`;
811
997
  }
812
- return d + ' Z';
998
+ return d + " Z";
813
999
  }
814
1000
 
815
1001
  /**
@@ -860,7 +1046,14 @@ export function polygonToPathData(polygon, precision = 6) {
860
1046
  * const polygon = resolveNestedClipPath(defsMap.get('clip1'), defsMap, target);
861
1047
  * // Logs warning about circular reference and returns clip1 polygon only
862
1048
  */
863
- export function resolveNestedClipPath(clipPathDef, defsMap, targetElement, ctm = null, visited = new Set(), options = {}) {
1049
+ export function resolveNestedClipPath(
1050
+ clipPathDef,
1051
+ defsMap,
1052
+ targetElement,
1053
+ ctm = null,
1054
+ visited = new Set(),
1055
+ options = {},
1056
+ ) {
864
1057
  const clipId = clipPathDef.id;
865
1058
  if (clipId && visited.has(clipId)) {
866
1059
  Logger.warn(`Circular clipPath reference detected: ${clipId}`);
@@ -870,13 +1063,23 @@ export function resolveNestedClipPath(clipPathDef, defsMap, targetElement, ctm =
870
1063
 
871
1064
  let clipPolygon = resolveClipPath(clipPathDef, targetElement, ctm, options);
872
1065
 
873
- if (clipPathDef['clip-path'] && clipPolygon.length >= 3) {
874
- const nestedRef = clipPathDef['clip-path'].replace(/^url\(#?|[)'"]/g, '');
1066
+ if (clipPathDef["clip-path"] && clipPolygon.length >= 3) {
1067
+ const nestedRef = clipPathDef["clip-path"].replace(/^url\(#?|[)'"]/g, "");
875
1068
  const nestedClipDef = defsMap.get(nestedRef);
876
1069
  if (nestedClipDef) {
877
- const nestedClip = resolveNestedClipPath(nestedClipDef, defsMap, targetElement, ctm, visited, options);
1070
+ const nestedClip = resolveNestedClipPath(
1071
+ nestedClipDef,
1072
+ defsMap,
1073
+ targetElement,
1074
+ ctm,
1075
+ visited,
1076
+ options,
1077
+ );
878
1078
  if (nestedClip.length >= 3) {
879
- const intersection = PolygonClip.polygonIntersection(clipPolygon, nestedClip);
1079
+ const intersection = PolygonClip.polygonIntersection(
1080
+ clipPolygon,
1081
+ nestedClip,
1082
+ );
880
1083
  clipPolygon = intersection.length > 0 ? intersection[0] : [];
881
1084
  }
882
1085
  }
@@ -885,6 +1088,11 @@ export function resolveNestedClipPath(clipPathDef, defsMap, targetElement, ctm =
885
1088
  }
886
1089
 
887
1090
  export default {
888
- pathToPolygon, shapeToPolygon, resolveClipPath, applyClipPath,
889
- polygonToPathData, resolveNestedClipPath, DEFAULT_CURVE_SAMPLES
1091
+ pathToPolygon,
1092
+ shapeToPolygon,
1093
+ resolveClipPath,
1094
+ applyClipPath,
1095
+ polygonToPathData,
1096
+ resolveNestedClipPath,
1097
+ DEFAULT_CURVE_SAMPLES,
890
1098
  };