@emasoft/svg-matrix 1.0.27 → 1.0.29
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/README.md +325 -0
- package/bin/svg-matrix.js +994 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +744 -184
- package/package.json +16 -4
- package/src/animation-references.js +71 -52
- package/src/arc-length.js +160 -96
- package/src/bezier-analysis.js +257 -117
- package/src/bezier-intersections.js +411 -148
- package/src/browser-verify.js +240 -100
- package/src/clip-path-resolver.js +350 -142
- package/src/convert-path-data.js +279 -134
- package/src/css-specificity.js +78 -70
- package/src/flatten-pipeline.js +751 -263
- package/src/geometry-to-path.js +511 -182
- package/src/index.js +191 -46
- package/src/inkscape-support.js +404 -0
- package/src/marker-resolver.js +278 -164
- package/src/mask-resolver.js +209 -98
- package/src/matrix.js +147 -67
- package/src/mesh-gradient.js +187 -96
- package/src/off-canvas-detection.js +201 -104
- package/src/path-analysis.js +187 -107
- package/src/path-data-plugins.js +628 -167
- package/src/path-simplification.js +0 -1
- package/src/pattern-resolver.js +125 -88
- package/src/polygon-clip.js +111 -66
- package/src/svg-boolean-ops.js +194 -118
- package/src/svg-collections.js +48 -19
- package/src/svg-flatten.js +282 -164
- package/src/svg-parser.js +427 -200
- package/src/svg-rendering-context.js +147 -104
- package/src/svg-toolbox.js +16411 -3298
- package/src/svg2-polyfills.js +114 -245
- package/src/transform-decomposition.js +46 -41
- package/src/transform-optimization.js +89 -68
- package/src/transforms2d.js +49 -16
- package/src/transforms3d.js +58 -22
- package/src/use-symbol-resolver.js +150 -110
- package/src/vector.js +67 -15
- package/src/vendor/README.md +110 -0
- package/src/vendor/inkscape-hatch-polyfill.js +401 -0
- package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
- package/src/vendor/inkscape-mesh-polyfill.js +843 -0
- package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
- package/src/verification.js +288 -124
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
* @module clip-path-resolver
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import Decimal from
|
|
31
|
-
import { Matrix } from
|
|
32
|
-
import * as Transforms2D from
|
|
33
|
-
import * as PolygonClip from
|
|
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
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
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(
|
|
93
|
+
export function pathToPolygon(
|
|
94
|
+
pathData,
|
|
95
|
+
samplesPerCurve = DEFAULT_CURVE_SAMPLES,
|
|
96
|
+
) {
|
|
97
97
|
const points = [];
|
|
98
|
-
let currentX = D(0),
|
|
99
|
-
|
|
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
|
|
108
|
-
currentX = D(args[0]);
|
|
109
|
-
|
|
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
|
|
113
|
-
currentX = currentX.plus(args[0]);
|
|
114
|
-
|
|
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
|
|
118
|
-
currentX = D(args[0]);
|
|
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
|
|
122
|
-
currentX = currentX.plus(args[0]);
|
|
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
|
|
133
|
+
case "H":
|
|
126
134
|
currentX = D(args[0]);
|
|
127
135
|
points.push(PolygonClip.point(currentX, currentY));
|
|
128
136
|
break;
|
|
129
|
-
case
|
|
137
|
+
case "h":
|
|
130
138
|
currentX = currentX.plus(args[0]);
|
|
131
139
|
points.push(PolygonClip.point(currentX, currentY));
|
|
132
140
|
break;
|
|
133
|
-
case
|
|
141
|
+
case "V":
|
|
134
142
|
currentY = D(args[0]);
|
|
135
143
|
points.push(PolygonClip.point(currentX, currentY));
|
|
136
144
|
break;
|
|
137
|
-
case
|
|
145
|
+
case "v":
|
|
138
146
|
currentY = currentY.plus(args[0]);
|
|
139
147
|
points.push(PolygonClip.point(currentX, currentY));
|
|
140
148
|
break;
|
|
141
|
-
case
|
|
142
|
-
sampleCubicBezier(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
148
|
-
sampleCubicBezier(
|
|
149
|
-
|
|
150
|
-
currentX
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
156
|
-
sampleQuadraticBezier(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
162
|
-
sampleQuadraticBezier(
|
|
163
|
-
|
|
164
|
-
currentX
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
169
|
-
sampleArc(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
175
|
-
sampleArc(
|
|
176
|
-
|
|
177
|
-
currentX
|
|
178
|
-
|
|
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
|
|
181
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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),
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
.
|
|
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),
|
|
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(
|
|
313
|
-
|
|
314
|
-
|
|
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(),
|
|
318
|
-
|
|
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 =
|
|
323
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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),
|
|
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),
|
|
344
|
-
|
|
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),
|
|
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(),
|
|
361
|
-
|
|
362
|
-
const
|
|
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(
|
|
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
|
|
550
|
+
case "circle":
|
|
434
551
|
// Use high-precision Bezier arcs for better curve approximation
|
|
435
552
|
if (bezierArcs > 4) {
|
|
436
|
-
pathData = circleToPathDataHP(
|
|
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(
|
|
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
|
|
568
|
+
case "ellipse":
|
|
442
569
|
// Use high-precision Bezier arcs for better curve approximation
|
|
443
570
|
if (bezierArcs > 4) {
|
|
444
|
-
pathData = ellipseToPathDataHP(
|
|
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(
|
|
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
|
|
450
|
-
pathData = rectToPath(
|
|
451
|
-
D(element.
|
|
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
|
|
454
|
-
pathData = lineToPath(
|
|
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
|
|
457
|
-
pathData = polygonToPath(element.points ||
|
|
606
|
+
case "polygon":
|
|
607
|
+
pathData = polygonToPath(element.points || "");
|
|
458
608
|
break;
|
|
459
|
-
case
|
|
460
|
-
pathData = polylineToPath(element.points ||
|
|
609
|
+
case "polyline":
|
|
610
|
+
pathData = polylineToPath(element.points || "");
|
|
461
611
|
break;
|
|
462
|
-
case
|
|
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(
|
|
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 ||
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
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 ===
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
752
|
-
return {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
759
|
-
const cx = D(element.cx || 0),
|
|
760
|
-
|
|
761
|
-
|
|
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 {
|
|
768
|
-
|
|
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 =
|
|
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 +
|
|
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(
|
|
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[
|
|
874
|
-
const nestedRef = clipPathDef[
|
|
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(
|
|
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(
|
|
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,
|
|
889
|
-
|
|
1091
|
+
pathToPolygon,
|
|
1092
|
+
shapeToPolygon,
|
|
1093
|
+
resolveClipPath,
|
|
1094
|
+
applyClipPath,
|
|
1095
|
+
polygonToPathData,
|
|
1096
|
+
resolveNestedClipPath,
|
|
1097
|
+
DEFAULT_CURVE_SAMPLES,
|
|
890
1098
|
};
|