@emasoft/svg-matrix 1.1.0 → 1.2.0
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.
- package/bin/svg-matrix.js +7 -6
- package/bin/svgm.js +109 -40
- package/dist/svg-matrix.min.js +7 -7
- package/dist/svg-toolbox.min.js +148 -228
- package/dist/svgm.min.js +152 -232
- package/dist/version.json +5 -5
- package/package.json +1 -1
- package/scripts/postinstall.js +72 -41
- package/scripts/test-postinstall.js +18 -16
- package/scripts/version-sync.js +78 -60
- package/src/animation-optimization.js +190 -98
- package/src/animation-references.js +11 -3
- package/src/arc-length.js +23 -20
- package/src/bezier-analysis.js +9 -13
- package/src/bezier-intersections.js +18 -4
- package/src/browser-verify.js +35 -8
- package/src/clip-path-resolver.js +285 -114
- package/src/convert-path-data.js +20 -8
- package/src/css-specificity.js +33 -9
- package/src/douglas-peucker.js +272 -141
- package/src/geometry-to-path.js +79 -22
- package/src/gjk-collision.js +287 -126
- package/src/index.js +56 -21
- package/src/inkscape-support.js +122 -101
- package/src/logger.js +43 -27
- package/src/marker-resolver.js +201 -121
- package/src/mask-resolver.js +231 -98
- package/src/matrix.js +9 -5
- package/src/mesh-gradient.js +22 -14
- package/src/off-canvas-detection.js +53 -17
- package/src/path-optimization.js +356 -171
- package/src/path-simplification.js +671 -256
- package/src/pattern-resolver.js +1 -3
- package/src/polygon-clip.js +396 -78
- package/src/svg-boolean-ops.js +90 -23
- package/src/svg-collections.js +1546 -667
- package/src/svg-flatten.js +152 -38
- package/src/svg-matrix-lib.js +2 -2
- package/src/svg-parser.js +5 -1
- package/src/svg-rendering-context.js +3 -1
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svg-toolbox.js +99 -457
- package/src/svg-validation-data.js +513 -345
- package/src/svg2-polyfills.js +156 -93
- package/src/svgm-lib.js +8 -4
- package/src/transform-optimization.js +168 -51
- package/src/transforms2d.js +73 -40
- package/src/transforms3d.js +34 -27
- package/src/use-symbol-resolver.js +175 -76
- package/src/vector.js +80 -44
- package/src/vendor/inkscape-hatch-polyfill.js +143 -108
- package/src/vendor/inkscape-hatch-polyfill.min.js +291 -1
- package/src/vendor/inkscape-mesh-polyfill.js +953 -766
- package/src/vendor/inkscape-mesh-polyfill.min.js +896 -1
- package/src/verification.js +3 -4
|
@@ -94,9 +94,16 @@ export function pathToPolygon(
|
|
|
94
94
|
pathData,
|
|
95
95
|
samplesPerCurve = DEFAULT_CURVE_SAMPLES,
|
|
96
96
|
) {
|
|
97
|
-
if (typeof pathData !==
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
if (typeof pathData !== "string")
|
|
98
|
+
throw new Error("pathToPolygon: pathData must be a string");
|
|
99
|
+
if (
|
|
100
|
+
typeof samplesPerCurve !== "number" ||
|
|
101
|
+
samplesPerCurve <= 0 ||
|
|
102
|
+
!Number.isFinite(samplesPerCurve)
|
|
103
|
+
) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`pathToPolygon: samplesPerCurve must be a positive finite number, got ${samplesPerCurve}`,
|
|
106
|
+
);
|
|
100
107
|
}
|
|
101
108
|
const points = [];
|
|
102
109
|
let currentX = D(0),
|
|
@@ -106,18 +113,23 @@ export function pathToPolygon(
|
|
|
106
113
|
|
|
107
114
|
const commands = parsePathCommands(pathData);
|
|
108
115
|
if (!Array.isArray(commands)) {
|
|
109
|
-
throw new Error(
|
|
116
|
+
throw new Error("pathToPolygon: parsePathCommands must return an array");
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
for (const cmd of commands) {
|
|
113
120
|
const { type, args } = cmd;
|
|
114
121
|
if (!Array.isArray(args)) {
|
|
115
|
-
throw new Error(
|
|
122
|
+
throw new Error(
|
|
123
|
+
`pathToPolygon: command args must be an array for command ${type}`,
|
|
124
|
+
);
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
switch (type) {
|
|
119
128
|
case "M":
|
|
120
|
-
if (args.length < 2)
|
|
129
|
+
if (args.length < 2)
|
|
130
|
+
throw new Error(
|
|
131
|
+
`pathToPolygon: M command requires 2 arguments, got ${args.length}`,
|
|
132
|
+
);
|
|
121
133
|
currentX = D(args[0]);
|
|
122
134
|
currentY = D(args[1]);
|
|
123
135
|
startX = currentX;
|
|
@@ -125,7 +137,10 @@ export function pathToPolygon(
|
|
|
125
137
|
points.push(PolygonClip.point(currentX, currentY));
|
|
126
138
|
break;
|
|
127
139
|
case "m":
|
|
128
|
-
if (args.length < 2)
|
|
140
|
+
if (args.length < 2)
|
|
141
|
+
throw new Error(
|
|
142
|
+
`pathToPolygon: m command requires 2 arguments, got ${args.length}`,
|
|
143
|
+
);
|
|
129
144
|
currentX = currentX.plus(args[0]);
|
|
130
145
|
currentY = currentY.plus(args[1]);
|
|
131
146
|
startX = currentX;
|
|
@@ -133,39 +148,60 @@ export function pathToPolygon(
|
|
|
133
148
|
points.push(PolygonClip.point(currentX, currentY));
|
|
134
149
|
break;
|
|
135
150
|
case "L":
|
|
136
|
-
if (args.length < 2)
|
|
151
|
+
if (args.length < 2)
|
|
152
|
+
throw new Error(
|
|
153
|
+
`pathToPolygon: L command requires 2 arguments, got ${args.length}`,
|
|
154
|
+
);
|
|
137
155
|
currentX = D(args[0]);
|
|
138
156
|
currentY = D(args[1]);
|
|
139
157
|
points.push(PolygonClip.point(currentX, currentY));
|
|
140
158
|
break;
|
|
141
159
|
case "l":
|
|
142
|
-
if (args.length < 2)
|
|
160
|
+
if (args.length < 2)
|
|
161
|
+
throw new Error(
|
|
162
|
+
`pathToPolygon: l command requires 2 arguments, got ${args.length}`,
|
|
163
|
+
);
|
|
143
164
|
currentX = currentX.plus(args[0]);
|
|
144
165
|
currentY = currentY.plus(args[1]);
|
|
145
166
|
points.push(PolygonClip.point(currentX, currentY));
|
|
146
167
|
break;
|
|
147
168
|
case "H":
|
|
148
|
-
if (args.length < 1)
|
|
169
|
+
if (args.length < 1)
|
|
170
|
+
throw new Error(
|
|
171
|
+
`pathToPolygon: H command requires 1 argument, got ${args.length}`,
|
|
172
|
+
);
|
|
149
173
|
currentX = D(args[0]);
|
|
150
174
|
points.push(PolygonClip.point(currentX, currentY));
|
|
151
175
|
break;
|
|
152
176
|
case "h":
|
|
153
|
-
if (args.length < 1)
|
|
177
|
+
if (args.length < 1)
|
|
178
|
+
throw new Error(
|
|
179
|
+
`pathToPolygon: h command requires 1 argument, got ${args.length}`,
|
|
180
|
+
);
|
|
154
181
|
currentX = currentX.plus(args[0]);
|
|
155
182
|
points.push(PolygonClip.point(currentX, currentY));
|
|
156
183
|
break;
|
|
157
184
|
case "V":
|
|
158
|
-
if (args.length < 1)
|
|
185
|
+
if (args.length < 1)
|
|
186
|
+
throw new Error(
|
|
187
|
+
`pathToPolygon: V command requires 1 argument, got ${args.length}`,
|
|
188
|
+
);
|
|
159
189
|
currentY = D(args[0]);
|
|
160
190
|
points.push(PolygonClip.point(currentX, currentY));
|
|
161
191
|
break;
|
|
162
192
|
case "v":
|
|
163
|
-
if (args.length < 1)
|
|
193
|
+
if (args.length < 1)
|
|
194
|
+
throw new Error(
|
|
195
|
+
`pathToPolygon: v command requires 1 argument, got ${args.length}`,
|
|
196
|
+
);
|
|
164
197
|
currentY = currentY.plus(args[0]);
|
|
165
198
|
points.push(PolygonClip.point(currentX, currentY));
|
|
166
199
|
break;
|
|
167
200
|
case "C":
|
|
168
|
-
if (args.length < 6)
|
|
201
|
+
if (args.length < 6)
|
|
202
|
+
throw new Error(
|
|
203
|
+
`pathToPolygon: C command requires 6 arguments, got ${args.length}`,
|
|
204
|
+
);
|
|
169
205
|
sampleCubicBezier(
|
|
170
206
|
points,
|
|
171
207
|
currentX,
|
|
@@ -182,7 +218,10 @@ export function pathToPolygon(
|
|
|
182
218
|
currentY = D(args[5]);
|
|
183
219
|
break;
|
|
184
220
|
case "c":
|
|
185
|
-
if (args.length < 6)
|
|
221
|
+
if (args.length < 6)
|
|
222
|
+
throw new Error(
|
|
223
|
+
`pathToPolygon: c command requires 6 arguments, got ${args.length}`,
|
|
224
|
+
);
|
|
186
225
|
sampleCubicBezier(
|
|
187
226
|
points,
|
|
188
227
|
currentX,
|
|
@@ -199,7 +238,10 @@ export function pathToPolygon(
|
|
|
199
238
|
currentY = currentY.plus(args[5]);
|
|
200
239
|
break;
|
|
201
240
|
case "Q":
|
|
202
|
-
if (args.length < 4)
|
|
241
|
+
if (args.length < 4)
|
|
242
|
+
throw new Error(
|
|
243
|
+
`pathToPolygon: Q command requires 4 arguments, got ${args.length}`,
|
|
244
|
+
);
|
|
203
245
|
sampleQuadraticBezier(
|
|
204
246
|
points,
|
|
205
247
|
currentX,
|
|
@@ -214,7 +256,10 @@ export function pathToPolygon(
|
|
|
214
256
|
currentY = D(args[3]);
|
|
215
257
|
break;
|
|
216
258
|
case "q":
|
|
217
|
-
if (args.length < 4)
|
|
259
|
+
if (args.length < 4)
|
|
260
|
+
throw new Error(
|
|
261
|
+
`pathToPolygon: q command requires 4 arguments, got ${args.length}`,
|
|
262
|
+
);
|
|
218
263
|
sampleQuadraticBezier(
|
|
219
264
|
points,
|
|
220
265
|
currentX,
|
|
@@ -229,7 +274,10 @@ export function pathToPolygon(
|
|
|
229
274
|
currentY = currentY.plus(args[3]);
|
|
230
275
|
break;
|
|
231
276
|
case "A":
|
|
232
|
-
if (args.length < 7)
|
|
277
|
+
if (args.length < 7)
|
|
278
|
+
throw new Error(
|
|
279
|
+
`pathToPolygon: A command requires 7 arguments, got ${args.length}`,
|
|
280
|
+
);
|
|
233
281
|
sampleArc(
|
|
234
282
|
points,
|
|
235
283
|
currentX,
|
|
@@ -247,7 +295,10 @@ export function pathToPolygon(
|
|
|
247
295
|
currentY = D(args[6]);
|
|
248
296
|
break;
|
|
249
297
|
case "a":
|
|
250
|
-
if (args.length < 7)
|
|
298
|
+
if (args.length < 7)
|
|
299
|
+
throw new Error(
|
|
300
|
+
`pathToPolygon: a command requires 7 arguments, got ${args.length}`,
|
|
301
|
+
);
|
|
251
302
|
sampleArc(
|
|
252
303
|
points,
|
|
253
304
|
currentX,
|
|
@@ -293,8 +344,10 @@ export function pathToPolygon(
|
|
|
293
344
|
* // Returns: [{type: 'M', args: [10, 20]}, {type: 'L', args: [30, 40]}]
|
|
294
345
|
*/
|
|
295
346
|
function parsePathCommands(pathData) {
|
|
296
|
-
if (typeof pathData !==
|
|
297
|
-
throw new Error(
|
|
347
|
+
if (typeof pathData !== "string") {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`parsePathCommands: pathData must be a string, got ${typeof pathData}`,
|
|
350
|
+
);
|
|
298
351
|
}
|
|
299
352
|
const commands = [];
|
|
300
353
|
const regex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
|
|
@@ -340,14 +393,30 @@ function parsePathCommands(pathData) {
|
|
|
340
393
|
*/
|
|
341
394
|
function sampleCubicBezier(points, x0, y0, x1, y1, x2, y2, x3, y3, samples) {
|
|
342
395
|
if (!Array.isArray(points)) {
|
|
343
|
-
throw new Error(
|
|
344
|
-
}
|
|
345
|
-
if (
|
|
346
|
-
|
|
396
|
+
throw new Error("sampleCubicBezier: points must be an array");
|
|
397
|
+
}
|
|
398
|
+
if (
|
|
399
|
+
typeof samples !== "number" ||
|
|
400
|
+
samples <= 0 ||
|
|
401
|
+
!Number.isFinite(samples)
|
|
402
|
+
) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`sampleCubicBezier: samples must be a positive finite number, got ${samples}`,
|
|
405
|
+
);
|
|
347
406
|
}
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
407
|
+
if (
|
|
408
|
+
!(x0 instanceof Decimal) ||
|
|
409
|
+
!(y0 instanceof Decimal) ||
|
|
410
|
+
!(x1 instanceof Decimal) ||
|
|
411
|
+
!(y1 instanceof Decimal) ||
|
|
412
|
+
!(x2 instanceof Decimal) ||
|
|
413
|
+
!(y2 instanceof Decimal) ||
|
|
414
|
+
!(x3 instanceof Decimal) ||
|
|
415
|
+
!(y3 instanceof Decimal)
|
|
416
|
+
) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
"sampleCubicBezier: all coordinate parameters must be Decimal instances",
|
|
419
|
+
);
|
|
351
420
|
}
|
|
352
421
|
for (let i = 1; i <= samples; i++) {
|
|
353
422
|
const t = D(i).div(samples);
|
|
@@ -393,14 +462,28 @@ function sampleCubicBezier(points, x0, y0, x1, y1, x2, y2, x3, y3, samples) {
|
|
|
393
462
|
*/
|
|
394
463
|
function sampleQuadraticBezier(points, x0, y0, x1, y1, x2, y2, samples) {
|
|
395
464
|
if (!Array.isArray(points)) {
|
|
396
|
-
throw new Error(
|
|
397
|
-
}
|
|
398
|
-
if (
|
|
399
|
-
|
|
465
|
+
throw new Error("sampleQuadraticBezier: points must be an array");
|
|
466
|
+
}
|
|
467
|
+
if (
|
|
468
|
+
typeof samples !== "number" ||
|
|
469
|
+
samples <= 0 ||
|
|
470
|
+
!Number.isFinite(samples)
|
|
471
|
+
) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`sampleQuadraticBezier: samples must be a positive finite number, got ${samples}`,
|
|
474
|
+
);
|
|
400
475
|
}
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
|
|
476
|
+
if (
|
|
477
|
+
!(x0 instanceof Decimal) ||
|
|
478
|
+
!(y0 instanceof Decimal) ||
|
|
479
|
+
!(x1 instanceof Decimal) ||
|
|
480
|
+
!(y1 instanceof Decimal) ||
|
|
481
|
+
!(x2 instanceof Decimal) ||
|
|
482
|
+
!(y2 instanceof Decimal)
|
|
483
|
+
) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
"sampleQuadraticBezier: all coordinate parameters must be Decimal instances",
|
|
486
|
+
);
|
|
404
487
|
}
|
|
405
488
|
for (let i = 1; i <= samples; i++) {
|
|
406
489
|
const t = D(i).div(samples);
|
|
@@ -451,19 +534,34 @@ function sampleArc(
|
|
|
451
534
|
samples,
|
|
452
535
|
) {
|
|
453
536
|
if (!Array.isArray(points)) {
|
|
454
|
-
throw new Error(
|
|
455
|
-
}
|
|
456
|
-
if (
|
|
457
|
-
|
|
537
|
+
throw new Error("sampleArc: points must be an array");
|
|
538
|
+
}
|
|
539
|
+
if (
|
|
540
|
+
typeof samples !== "number" ||
|
|
541
|
+
samples <= 0 ||
|
|
542
|
+
!Number.isFinite(samples)
|
|
543
|
+
) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`sampleArc: samples must be a positive finite number, got ${samples}`,
|
|
546
|
+
);
|
|
458
547
|
}
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
548
|
+
if (
|
|
549
|
+
!(x0 instanceof Decimal) ||
|
|
550
|
+
!(y0 instanceof Decimal) ||
|
|
551
|
+
!(rx instanceof Decimal) ||
|
|
552
|
+
!(ry instanceof Decimal) ||
|
|
553
|
+
!(xAxisRotation instanceof Decimal) ||
|
|
554
|
+
!(x1 instanceof Decimal) ||
|
|
555
|
+
!(y1 instanceof Decimal)
|
|
556
|
+
) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
"sampleArc: all coordinate and angle parameters must be Decimal instances",
|
|
559
|
+
);
|
|
462
560
|
}
|
|
463
|
-
if (typeof largeArc !==
|
|
561
|
+
if (typeof largeArc !== "number" || (largeArc !== 0 && largeArc !== 1)) {
|
|
464
562
|
throw new Error(`sampleArc: largeArc must be 0 or 1, got ${largeArc}`);
|
|
465
563
|
}
|
|
466
|
-
if (typeof sweep !==
|
|
564
|
+
if (typeof sweep !== "number" || (sweep !== 0 && sweep !== 1)) {
|
|
467
565
|
throw new Error(`sampleArc: sweep must be 0 or 1, got ${sweep}`);
|
|
468
566
|
}
|
|
469
567
|
if (rx.eq(0) || ry.eq(0)) {
|
|
@@ -514,7 +612,9 @@ function sampleArc(
|
|
|
514
612
|
const n1 = ux.mul(ux).plus(uy.mul(uy)).sqrt();
|
|
515
613
|
if (n1.eq(0)) {
|
|
516
614
|
// Degenerate arc: center point coincides with start point
|
|
517
|
-
Logger.warn(
|
|
615
|
+
Logger.warn(
|
|
616
|
+
"sampleArc: degenerate arc - center coincides with start point, treating as line to end",
|
|
617
|
+
);
|
|
518
618
|
points.push(PolygonClip.point(x1, y1));
|
|
519
619
|
return;
|
|
520
620
|
}
|
|
@@ -524,7 +624,9 @@ function sampleArc(
|
|
|
524
624
|
const n2 = n1.mul(vx.mul(vx).plus(vy.mul(vy)).sqrt());
|
|
525
625
|
if (n2.eq(0)) {
|
|
526
626
|
// Degenerate arc: center coincides with both endpoints (zero-length arc)
|
|
527
|
-
Logger.warn(
|
|
627
|
+
Logger.warn(
|
|
628
|
+
"sampleArc: degenerate arc - zero length, treating as single point",
|
|
629
|
+
);
|
|
528
630
|
points.push(PolygonClip.point(x1, y1));
|
|
529
631
|
return;
|
|
530
632
|
}
|
|
@@ -571,7 +673,7 @@ function sampleArc(
|
|
|
571
673
|
*/
|
|
572
674
|
function removeDuplicateConsecutive(points) {
|
|
573
675
|
if (!Array.isArray(points)) {
|
|
574
|
-
throw new Error(
|
|
676
|
+
throw new Error("removeDuplicateConsecutive: points must be an array");
|
|
575
677
|
}
|
|
576
678
|
if (points.length < 2) return points;
|
|
577
679
|
const result = [points[0]];
|
|
@@ -625,20 +727,32 @@ export function shapeToPolygon(
|
|
|
625
727
|
samples = DEFAULT_CURVE_SAMPLES,
|
|
626
728
|
bezierArcs = 4,
|
|
627
729
|
) {
|
|
628
|
-
if (!element || typeof element !==
|
|
629
|
-
throw new Error(
|
|
630
|
-
}
|
|
631
|
-
if (!element.type || typeof element.type !==
|
|
632
|
-
throw new Error(
|
|
633
|
-
}
|
|
634
|
-
if (
|
|
635
|
-
|
|
730
|
+
if (!element || typeof element !== "object") {
|
|
731
|
+
throw new Error("shapeToPolygon: element must be an object");
|
|
732
|
+
}
|
|
733
|
+
if (!element.type || typeof element.type !== "string") {
|
|
734
|
+
throw new Error("shapeToPolygon: element.type must be a string");
|
|
735
|
+
}
|
|
736
|
+
if (
|
|
737
|
+
typeof samples !== "number" ||
|
|
738
|
+
samples <= 0 ||
|
|
739
|
+
!Number.isFinite(samples)
|
|
740
|
+
) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
`shapeToPolygon: samples must be a positive finite number, got ${samples}`,
|
|
743
|
+
);
|
|
636
744
|
}
|
|
637
|
-
if (
|
|
638
|
-
|
|
745
|
+
if (
|
|
746
|
+
typeof bezierArcs !== "number" ||
|
|
747
|
+
bezierArcs <= 0 ||
|
|
748
|
+
!Number.isFinite(bezierArcs)
|
|
749
|
+
) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
`shapeToPolygon: bezierArcs must be a positive finite number, got ${bezierArcs}`,
|
|
752
|
+
);
|
|
639
753
|
}
|
|
640
754
|
if (ctm !== null && !(ctm instanceof Matrix)) {
|
|
641
|
-
throw new Error(
|
|
755
|
+
throw new Error("shapeToPolygon: ctm must be null or a Matrix instance");
|
|
642
756
|
}
|
|
643
757
|
let pathData;
|
|
644
758
|
switch (element.type) {
|
|
@@ -714,7 +828,9 @@ export function shapeToPolygon(
|
|
|
714
828
|
if (element.transform) {
|
|
715
829
|
const elementTransform = parseTransform(element.transform);
|
|
716
830
|
if (!(elementTransform instanceof Matrix)) {
|
|
717
|
-
throw new Error(
|
|
831
|
+
throw new Error(
|
|
832
|
+
`shapeToPolygon: parseTransform must return a Matrix, got ${typeof elementTransform}`,
|
|
833
|
+
);
|
|
718
834
|
}
|
|
719
835
|
pathData = transformPathData(pathData, elementTransform);
|
|
720
836
|
}
|
|
@@ -785,24 +901,35 @@ export function resolveClipPath(
|
|
|
785
901
|
ctm = null,
|
|
786
902
|
options = {},
|
|
787
903
|
) {
|
|
788
|
-
if (!clipPathDef || typeof clipPathDef !==
|
|
789
|
-
throw new Error(
|
|
904
|
+
if (!clipPathDef || typeof clipPathDef !== "object") {
|
|
905
|
+
throw new Error("resolveClipPath: clipPathDef must be an object");
|
|
790
906
|
}
|
|
791
907
|
if (ctm !== null && !(ctm instanceof Matrix)) {
|
|
792
|
-
throw new Error(
|
|
908
|
+
throw new Error("resolveClipPath: ctm must be null or a Matrix instance");
|
|
793
909
|
}
|
|
794
|
-
if (typeof options !==
|
|
795
|
-
throw new Error(
|
|
910
|
+
if (typeof options !== "object" || options === null) {
|
|
911
|
+
throw new Error("resolveClipPath: options must be an object");
|
|
796
912
|
}
|
|
797
913
|
const { samples = DEFAULT_CURVE_SAMPLES } = options;
|
|
798
|
-
if (
|
|
799
|
-
|
|
914
|
+
if (
|
|
915
|
+
typeof samples !== "number" ||
|
|
916
|
+
samples <= 0 ||
|
|
917
|
+
!Number.isFinite(samples)
|
|
918
|
+
) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`resolveClipPath: samples must be a positive finite number, got ${samples}`,
|
|
921
|
+
);
|
|
800
922
|
}
|
|
801
923
|
const clipPathUnits = clipPathDef.clipPathUnits || "userSpaceOnUse";
|
|
802
924
|
|
|
803
925
|
// Validate clipPathUnits value (SVG spec: case-sensitive)
|
|
804
|
-
if (
|
|
805
|
-
|
|
926
|
+
if (
|
|
927
|
+
clipPathUnits !== "userSpaceOnUse" &&
|
|
928
|
+
clipPathUnits !== "objectBoundingBox"
|
|
929
|
+
) {
|
|
930
|
+
Logger.warn(
|
|
931
|
+
`resolveClipPath: invalid clipPathUnits '${clipPathUnits}', defaulting to 'userSpaceOnUse' (valid values: 'userSpaceOnUse', 'objectBoundingBox')`,
|
|
932
|
+
);
|
|
806
933
|
}
|
|
807
934
|
|
|
808
935
|
let clipTransform = ctm ? ctm.clone() : Matrix.identity(3);
|
|
@@ -810,23 +937,31 @@ export function resolveClipPath(
|
|
|
810
937
|
if (clipPathDef.transform) {
|
|
811
938
|
const clipPathTransformMatrix = parseTransform(clipPathDef.transform);
|
|
812
939
|
if (!(clipPathTransformMatrix instanceof Matrix)) {
|
|
813
|
-
throw new Error(
|
|
940
|
+
throw new Error(
|
|
941
|
+
`resolveClipPath: parseTransform must return a Matrix, got ${typeof clipPathTransformMatrix}`,
|
|
942
|
+
);
|
|
814
943
|
}
|
|
815
944
|
clipTransform = clipTransform.mul(clipPathTransformMatrix);
|
|
816
945
|
}
|
|
817
946
|
|
|
818
947
|
if (clipPathUnits === "objectBoundingBox") {
|
|
819
948
|
if (!targetElement) {
|
|
820
|
-
throw new Error(
|
|
949
|
+
throw new Error(
|
|
950
|
+
"resolveClipPath: targetElement required for objectBoundingBox clipPathUnits",
|
|
951
|
+
);
|
|
821
952
|
}
|
|
822
953
|
const bbox = getElementBoundingBox(targetElement);
|
|
823
954
|
if (!bbox) {
|
|
824
|
-
Logger.warn(
|
|
955
|
+
Logger.warn(
|
|
956
|
+
"resolveClipPath: failed to compute bounding box for objectBoundingBox clipPath",
|
|
957
|
+
);
|
|
825
958
|
return [];
|
|
826
959
|
}
|
|
827
960
|
// Validate bounding box dimensions - degenerate bbox clips everything
|
|
828
961
|
if (bbox.width.lte(0) || bbox.height.lte(0)) {
|
|
829
|
-
Logger.warn(
|
|
962
|
+
Logger.warn(
|
|
963
|
+
`resolveClipPath: degenerate bounding box (width=${bbox.width}, height=${bbox.height}), clipping entire element`,
|
|
964
|
+
);
|
|
830
965
|
return [];
|
|
831
966
|
}
|
|
832
967
|
const bboxTransform = Transforms2D.translation(bbox.x, bbox.y).mul(
|
|
@@ -838,7 +973,9 @@ export function resolveClipPath(
|
|
|
838
973
|
// Validate children array before processing
|
|
839
974
|
const children = clipPathDef.children || [];
|
|
840
975
|
if (!Array.isArray(children)) {
|
|
841
|
-
throw new Error(
|
|
976
|
+
throw new Error(
|
|
977
|
+
`resolveClipPath: clipPathDef.children must be an array, got ${typeof children}`,
|
|
978
|
+
);
|
|
842
979
|
}
|
|
843
980
|
|
|
844
981
|
const clipPolygons = [];
|
|
@@ -849,7 +986,9 @@ export function resolveClipPath(
|
|
|
849
986
|
|
|
850
987
|
// Empty clipPath returns empty polygon (clips everything)
|
|
851
988
|
if (clipPolygons.length === 0) {
|
|
852
|
-
Logger.warn(
|
|
989
|
+
Logger.warn(
|
|
990
|
+
"resolveClipPath: clipPath has no valid child shapes, clipping entire element",
|
|
991
|
+
);
|
|
853
992
|
return [];
|
|
854
993
|
}
|
|
855
994
|
if (clipPolygons.length === 1) return clipPolygons[0];
|
|
@@ -886,13 +1025,15 @@ export function resolveClipPath(
|
|
|
886
1025
|
*/
|
|
887
1026
|
function clipPolygonWithRule(elementPolygon, clipPolygon, clipRule) {
|
|
888
1027
|
if (!Array.isArray(elementPolygon)) {
|
|
889
|
-
throw new Error(
|
|
1028
|
+
throw new Error("clipPolygonWithRule: elementPolygon must be an array");
|
|
890
1029
|
}
|
|
891
1030
|
if (!Array.isArray(clipPolygon)) {
|
|
892
|
-
throw new Error(
|
|
1031
|
+
throw new Error("clipPolygonWithRule: clipPolygon must be an array");
|
|
893
1032
|
}
|
|
894
|
-
if (clipRule !==
|
|
895
|
-
throw new Error(
|
|
1033
|
+
if (clipRule !== "nonzero" && clipRule !== "evenodd") {
|
|
1034
|
+
throw new Error(
|
|
1035
|
+
`clipPolygonWithRule: clipRule must be 'nonzero' or 'evenodd', got '${clipRule}' (valid values: 'nonzero', 'evenodd')`,
|
|
1036
|
+
);
|
|
896
1037
|
}
|
|
897
1038
|
if (elementPolygon.length < 3 || clipPolygon.length < 3) {
|
|
898
1039
|
return [];
|
|
@@ -941,10 +1082,10 @@ function clipPolygonWithRule(elementPolygon, clipPolygon, clipRule) {
|
|
|
941
1082
|
*/
|
|
942
1083
|
function computeCentroid(polygon) {
|
|
943
1084
|
if (!Array.isArray(polygon)) {
|
|
944
|
-
throw new Error(
|
|
1085
|
+
throw new Error("computeCentroid: polygon must be an array");
|
|
945
1086
|
}
|
|
946
1087
|
if (polygon.length === 0) {
|
|
947
|
-
throw new Error(
|
|
1088
|
+
throw new Error("computeCentroid: polygon must not be empty");
|
|
948
1089
|
}
|
|
949
1090
|
let cx = new Decimal(0);
|
|
950
1091
|
let cy = new Decimal(0);
|
|
@@ -961,7 +1102,7 @@ function computeCentroid(polygon) {
|
|
|
961
1102
|
|
|
962
1103
|
area = area.div(2);
|
|
963
1104
|
// Use Decimal comparison for degenerate polygon detection
|
|
964
|
-
const epsilon = new Decimal(
|
|
1105
|
+
const epsilon = new Decimal("1e-10");
|
|
965
1106
|
if (area.abs().lt(epsilon)) {
|
|
966
1107
|
// Degenerate polygon (zero area) - return average of vertices
|
|
967
1108
|
let sumX = new Decimal(0);
|
|
@@ -1033,24 +1174,32 @@ function computeCentroid(polygon) {
|
|
|
1033
1174
|
* const clipped = applyClipPath(ellipse, clipDef, null, { samples: 50 });
|
|
1034
1175
|
*/
|
|
1035
1176
|
export function applyClipPath(element, clipPathDef, ctm = null, options = {}) {
|
|
1036
|
-
if (!element || typeof element !==
|
|
1037
|
-
throw new Error(
|
|
1177
|
+
if (!element || typeof element !== "object") {
|
|
1178
|
+
throw new Error("applyClipPath: element must be an object");
|
|
1038
1179
|
}
|
|
1039
|
-
if (!clipPathDef || typeof clipPathDef !==
|
|
1040
|
-
throw new Error(
|
|
1180
|
+
if (!clipPathDef || typeof clipPathDef !== "object") {
|
|
1181
|
+
throw new Error("applyClipPath: clipPathDef must be an object");
|
|
1041
1182
|
}
|
|
1042
1183
|
if (ctm !== null && !(ctm instanceof Matrix)) {
|
|
1043
|
-
throw new Error(
|
|
1184
|
+
throw new Error("applyClipPath: ctm must be null or a Matrix instance");
|
|
1044
1185
|
}
|
|
1045
|
-
if (typeof options !==
|
|
1046
|
-
throw new Error(
|
|
1186
|
+
if (typeof options !== "object" || options === null) {
|
|
1187
|
+
throw new Error("applyClipPath: options must be an object");
|
|
1047
1188
|
}
|
|
1048
1189
|
const { samples = DEFAULT_CURVE_SAMPLES, clipRule = "nonzero" } = options;
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1190
|
+
if (
|
|
1191
|
+
typeof samples !== "number" ||
|
|
1192
|
+
samples <= 0 ||
|
|
1193
|
+
!Number.isFinite(samples)
|
|
1194
|
+
) {
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
`applyClipPath: samples must be a positive finite number, got ${samples}`,
|
|
1197
|
+
);
|
|
1051
1198
|
}
|
|
1052
|
-
if (clipRule !==
|
|
1053
|
-
throw new Error(
|
|
1199
|
+
if (clipRule !== "nonzero" && clipRule !== "evenodd") {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`applyClipPath: clipRule must be 'nonzero' or 'evenodd', got '${clipRule}' (valid values: 'nonzero', 'evenodd')`,
|
|
1202
|
+
);
|
|
1054
1203
|
}
|
|
1055
1204
|
const clipPolygon = resolveClipPath(clipPathDef, element, ctm, options);
|
|
1056
1205
|
if (clipPolygon.length < 3) return [];
|
|
@@ -1087,11 +1236,11 @@ export function applyClipPath(element, clipPathDef, ctm = null, options = {}) {
|
|
|
1087
1236
|
* // Returns: {x: Decimal(25), y: Decimal(25), width: Decimal(50), height: Decimal(50)}
|
|
1088
1237
|
*/
|
|
1089
1238
|
function getElementBoundingBox(element) {
|
|
1090
|
-
if (!element || typeof element !==
|
|
1091
|
-
throw new Error(
|
|
1239
|
+
if (!element || typeof element !== "object") {
|
|
1240
|
+
throw new Error("getElementBoundingBox: element must be an object");
|
|
1092
1241
|
}
|
|
1093
|
-
if (!element.type || typeof element.type !==
|
|
1094
|
-
throw new Error(
|
|
1242
|
+
if (!element.type || typeof element.type !== "string") {
|
|
1243
|
+
throw new Error("getElementBoundingBox: element.type must be a string");
|
|
1095
1244
|
}
|
|
1096
1245
|
switch (element.type) {
|
|
1097
1246
|
case "rect": {
|
|
@@ -1099,7 +1248,9 @@ function getElementBoundingBox(element) {
|
|
|
1099
1248
|
const height = D(element.height || 0);
|
|
1100
1249
|
// Negative width/height are invalid in SVG
|
|
1101
1250
|
if (width.lt(0) || height.lt(0)) {
|
|
1102
|
-
Logger.warn(
|
|
1251
|
+
Logger.warn(
|
|
1252
|
+
`getElementBoundingBox: rect has negative dimensions (width=${width}, height=${height})`,
|
|
1253
|
+
);
|
|
1103
1254
|
return null;
|
|
1104
1255
|
}
|
|
1105
1256
|
return {
|
|
@@ -1115,7 +1266,9 @@ function getElementBoundingBox(element) {
|
|
|
1115
1266
|
r = D(element.r || 0);
|
|
1116
1267
|
// Negative radius is invalid in SVG
|
|
1117
1268
|
if (r.lt(0)) {
|
|
1118
|
-
Logger.warn(
|
|
1269
|
+
Logger.warn(
|
|
1270
|
+
`getElementBoundingBox: circle has negative radius (r=${r})`,
|
|
1271
|
+
);
|
|
1119
1272
|
return null;
|
|
1120
1273
|
}
|
|
1121
1274
|
return {
|
|
@@ -1132,7 +1285,9 @@ function getElementBoundingBox(element) {
|
|
|
1132
1285
|
ry = D(element.ry || 0);
|
|
1133
1286
|
// Negative radii are invalid in SVG
|
|
1134
1287
|
if (rx.lt(0) || ry.lt(0)) {
|
|
1135
|
-
Logger.warn(
|
|
1288
|
+
Logger.warn(
|
|
1289
|
+
`getElementBoundingBox: ellipse has negative radii (rx=${rx}, ry=${ry})`,
|
|
1290
|
+
);
|
|
1136
1291
|
return null;
|
|
1137
1292
|
}
|
|
1138
1293
|
return {
|
|
@@ -1147,14 +1302,18 @@ function getElementBoundingBox(element) {
|
|
|
1147
1302
|
if (polygon.length > 0) {
|
|
1148
1303
|
const bbox = PolygonClip.boundingBox(polygon);
|
|
1149
1304
|
if (!bbox || !bbox.minX || !bbox.minY || !bbox.maxX || !bbox.maxY) {
|
|
1150
|
-
Logger.warn(
|
|
1305
|
+
Logger.warn(
|
|
1306
|
+
"getElementBoundingBox: failed to compute bounding box from polygon",
|
|
1307
|
+
);
|
|
1151
1308
|
return null;
|
|
1152
1309
|
}
|
|
1153
1310
|
const width = bbox.maxX.minus(bbox.minX);
|
|
1154
1311
|
const height = bbox.maxY.minus(bbox.minY);
|
|
1155
1312
|
// Check for degenerate bounding box
|
|
1156
1313
|
if (width.lt(0) || height.lt(0)) {
|
|
1157
|
-
Logger.warn(
|
|
1314
|
+
Logger.warn(
|
|
1315
|
+
`getElementBoundingBox: invalid bounding box dimensions (width=${width}, height=${height})`,
|
|
1316
|
+
);
|
|
1158
1317
|
return null;
|
|
1159
1318
|
}
|
|
1160
1319
|
return {
|
|
@@ -1201,10 +1360,16 @@ function getElementBoundingBox(element) {
|
|
|
1201
1360
|
*/
|
|
1202
1361
|
export function polygonToPathData(polygon, precision = 6) {
|
|
1203
1362
|
if (!Array.isArray(polygon)) {
|
|
1204
|
-
throw new Error(
|
|
1205
|
-
}
|
|
1206
|
-
if (
|
|
1207
|
-
|
|
1363
|
+
throw new Error("polygonToPathData: polygon must be an array");
|
|
1364
|
+
}
|
|
1365
|
+
if (
|
|
1366
|
+
typeof precision !== "number" ||
|
|
1367
|
+
precision < 0 ||
|
|
1368
|
+
!Number.isFinite(precision)
|
|
1369
|
+
) {
|
|
1370
|
+
throw new Error(
|
|
1371
|
+
`polygonToPathData: precision must be a non-negative finite number, got ${precision}`,
|
|
1372
|
+
);
|
|
1208
1373
|
}
|
|
1209
1374
|
if (polygon.length < 2) return "";
|
|
1210
1375
|
const fmt = (n) =>
|
|
@@ -1272,20 +1437,22 @@ export function resolveNestedClipPath(
|
|
|
1272
1437
|
visited = new Set(),
|
|
1273
1438
|
options = {},
|
|
1274
1439
|
) {
|
|
1275
|
-
if (!clipPathDef || typeof clipPathDef !==
|
|
1276
|
-
throw new Error(
|
|
1440
|
+
if (!clipPathDef || typeof clipPathDef !== "object") {
|
|
1441
|
+
throw new Error("resolveNestedClipPath: clipPathDef must be an object");
|
|
1277
1442
|
}
|
|
1278
1443
|
if (!(defsMap instanceof Map)) {
|
|
1279
|
-
throw new Error(
|
|
1444
|
+
throw new Error("resolveNestedClipPath: defsMap must be a Map");
|
|
1280
1445
|
}
|
|
1281
1446
|
if (ctm !== null && !(ctm instanceof Matrix)) {
|
|
1282
|
-
throw new Error(
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
"resolveNestedClipPath: ctm must be null or a Matrix instance",
|
|
1449
|
+
);
|
|
1283
1450
|
}
|
|
1284
1451
|
if (!(visited instanceof Set)) {
|
|
1285
|
-
throw new Error(
|
|
1452
|
+
throw new Error("resolveNestedClipPath: visited must be a Set");
|
|
1286
1453
|
}
|
|
1287
|
-
if (typeof options !==
|
|
1288
|
-
throw new Error(
|
|
1454
|
+
if (typeof options !== "object" || options === null) {
|
|
1455
|
+
throw new Error("resolveNestedClipPath: options must be an object");
|
|
1289
1456
|
}
|
|
1290
1457
|
const clipId = clipPathDef.id;
|
|
1291
1458
|
if (clipId && visited.has(clipId)) {
|
|
@@ -1300,7 +1467,9 @@ export function resolveNestedClipPath(
|
|
|
1300
1467
|
const nestedRef = clipPathDef["clip-path"].replace(/^url\(#?|[)'"]/g, "");
|
|
1301
1468
|
const nestedClipDef = defsMap.get(nestedRef);
|
|
1302
1469
|
if (!nestedClipDef) {
|
|
1303
|
-
Logger.warn(
|
|
1470
|
+
Logger.warn(
|
|
1471
|
+
`resolveNestedClipPath: referenced clipPath not found: ${nestedRef}`,
|
|
1472
|
+
);
|
|
1304
1473
|
// Continue with current clipPolygon, ignoring missing reference
|
|
1305
1474
|
} else {
|
|
1306
1475
|
const nestedClip = resolveNestedClipPath(
|
|
@@ -1318,7 +1487,9 @@ export function resolveNestedClipPath(
|
|
|
1318
1487
|
);
|
|
1319
1488
|
clipPolygon = intersection.length > 0 ? intersection[0] : [];
|
|
1320
1489
|
} else {
|
|
1321
|
-
Logger.warn(
|
|
1490
|
+
Logger.warn(
|
|
1491
|
+
`resolveNestedClipPath: nested clipPath ${nestedRef} resolved to empty polygon`,
|
|
1492
|
+
);
|
|
1322
1493
|
clipPolygon = [];
|
|
1323
1494
|
}
|
|
1324
1495
|
}
|