@emasoft/svg-matrix 1.0.5 → 1.0.6
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 +317 -396
- package/package.json +19 -1
- package/src/browser-verify.js +463 -0
- package/src/clip-path-resolver.js +759 -0
- package/src/geometry-to-path.js +348 -0
- package/src/index.js +413 -6
- package/src/marker-resolver.js +1006 -0
- package/src/mask-resolver.js +1407 -0
- package/src/mesh-gradient.js +1215 -0
- package/src/pattern-resolver.js +844 -0
- package/src/polygon-clip.js +1491 -0
- package/src/svg-flatten.js +1264 -105
- package/src/text-to-path.js +820 -0
- package/src/transforms2d.js +493 -37
- package/src/transforms3d.js +418 -47
- package/src/use-symbol-resolver.js +1126 -0
- package/samples/preserveAspectRatio_SVG.svg +0 -63
- package/samples/test.svg +0 -39
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClipPath Resolver - Flatten clipPath operations with arbitrary precision
|
|
3
|
+
*
|
|
4
|
+
* This module resolves SVG clipPath elements by performing boolean intersection
|
|
5
|
+
* operations using Decimal.js-backed polygon clipping.
|
|
6
|
+
*
|
|
7
|
+
* ## SVG clipPath Coordinate Systems
|
|
8
|
+
*
|
|
9
|
+
* SVG clipPath elements support two coordinate systems via the `clipPathUnits` attribute:
|
|
10
|
+
*
|
|
11
|
+
* - **userSpaceOnUse** (default): The clipPath coordinates are in the same coordinate
|
|
12
|
+
* system as the element being clipped. This is the user coordinate system established
|
|
13
|
+
* by the viewport.
|
|
14
|
+
*
|
|
15
|
+
* - **objectBoundingBox**: The clipPath coordinates are expressed as fractions (0-1)
|
|
16
|
+
* of the bounding box of the element being clipped. A point (0.5, 0.5) would be
|
|
17
|
+
* the center of the target element's bounding box.
|
|
18
|
+
*
|
|
19
|
+
* ## Clipping Process
|
|
20
|
+
*
|
|
21
|
+
* 1. Convert clipPath shapes to polygons by sampling curves (Bezier, arcs)
|
|
22
|
+
* 2. Apply transforms (element transform, CTM, and coordinate system transforms)
|
|
23
|
+
* 3. Perform boolean union of multiple clipPath children into unified clip polygon
|
|
24
|
+
* 4. Intersect target element polygon with clip polygon to get final clipped geometry
|
|
25
|
+
* 5. Handle nested clipPaths by recursively resolving and intersecting
|
|
26
|
+
*
|
|
27
|
+
* @module clip-path-resolver
|
|
28
|
+
*/
|
|
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';
|
|
34
|
+
import {
|
|
35
|
+
circleToPath,
|
|
36
|
+
ellipseToPath,
|
|
37
|
+
rectToPath,
|
|
38
|
+
lineToPath,
|
|
39
|
+
polygonToPath,
|
|
40
|
+
polylineToPath,
|
|
41
|
+
parseTransformAttribute,
|
|
42
|
+
transformPathData
|
|
43
|
+
} from './svg-flatten.js';
|
|
44
|
+
|
|
45
|
+
// Alias for cleaner code
|
|
46
|
+
const parseTransform = parseTransformAttribute;
|
|
47
|
+
|
|
48
|
+
Decimal.set({ precision: 80 });
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Helper function to convert a value to Decimal.
|
|
52
|
+
* @private
|
|
53
|
+
* @param {number|string|Decimal} x - Value to convert
|
|
54
|
+
* @returns {Decimal} Decimal instance
|
|
55
|
+
*/
|
|
56
|
+
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Default number of sample points per curve segment (for Bezier curves and arcs).
|
|
60
|
+
* Higher values produce smoother polygons but increase computation time.
|
|
61
|
+
* @constant {number}
|
|
62
|
+
*/
|
|
63
|
+
const DEFAULT_CURVE_SAMPLES = 20;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert SVG path data to a polygon (array of points) by sampling curves.
|
|
67
|
+
*
|
|
68
|
+
* This function parses SVG path commands and converts curves (cubic Bezier, quadratic
|
|
69
|
+
* Bezier, and elliptical arcs) into polyline segments by sampling points along the curve.
|
|
70
|
+
* Straight line commands (M, L, H, V) are converted directly to polygon vertices.
|
|
71
|
+
*
|
|
72
|
+
* All numeric values are converted to Decimal for arbitrary precision.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} pathData - SVG path d attribute string (e.g., "M 0 0 L 100 0 L 100 100 Z")
|
|
75
|
+
* @param {number} [samplesPerCurve=20] - Number of sample points to generate per curve segment.
|
|
76
|
+
* Higher values produce smoother approximations of curves but increase vertex count.
|
|
77
|
+
* @returns {Array<{x: Decimal, y: Decimal}>} Array of polygon vertices with Decimal coordinates.
|
|
78
|
+
* Consecutive duplicate points are removed.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // Convert a simple path to polygon
|
|
82
|
+
* const polygon = pathToPolygon("M 0 0 L 100 0 L 100 100 L 0 100 Z");
|
|
83
|
+
* // Returns: [{x: Decimal(0), y: Decimal(0)}, {x: Decimal(100), y: Decimal(0)}, ...]
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // Convert a path with curves using custom sampling
|
|
87
|
+
* const smoothPolygon = pathToPolygon("M 0 0 C 50 0 50 100 100 100", 50);
|
|
88
|
+
* // Higher sampling (50 points) creates smoother curve approximation
|
|
89
|
+
*/
|
|
90
|
+
export function pathToPolygon(pathData, samplesPerCurve = DEFAULT_CURVE_SAMPLES) {
|
|
91
|
+
const points = [];
|
|
92
|
+
let currentX = D(0), currentY = D(0);
|
|
93
|
+
let startX = D(0), startY = D(0);
|
|
94
|
+
|
|
95
|
+
const commands = parsePathCommands(pathData);
|
|
96
|
+
|
|
97
|
+
for (const cmd of commands) {
|
|
98
|
+
const { type, args } = cmd;
|
|
99
|
+
|
|
100
|
+
switch (type) {
|
|
101
|
+
case 'M':
|
|
102
|
+
currentX = D(args[0]); currentY = D(args[1]);
|
|
103
|
+
startX = currentX; startY = currentY;
|
|
104
|
+
points.push(PolygonClip.point(currentX, currentY));
|
|
105
|
+
break;
|
|
106
|
+
case 'm':
|
|
107
|
+
currentX = currentX.plus(args[0]); currentY = currentY.plus(args[1]);
|
|
108
|
+
startX = currentX; startY = currentY;
|
|
109
|
+
points.push(PolygonClip.point(currentX, currentY));
|
|
110
|
+
break;
|
|
111
|
+
case 'L':
|
|
112
|
+
currentX = D(args[0]); currentY = D(args[1]);
|
|
113
|
+
points.push(PolygonClip.point(currentX, currentY));
|
|
114
|
+
break;
|
|
115
|
+
case 'l':
|
|
116
|
+
currentX = currentX.plus(args[0]); currentY = currentY.plus(args[1]);
|
|
117
|
+
points.push(PolygonClip.point(currentX, currentY));
|
|
118
|
+
break;
|
|
119
|
+
case 'H':
|
|
120
|
+
currentX = D(args[0]);
|
|
121
|
+
points.push(PolygonClip.point(currentX, currentY));
|
|
122
|
+
break;
|
|
123
|
+
case 'h':
|
|
124
|
+
currentX = currentX.plus(args[0]);
|
|
125
|
+
points.push(PolygonClip.point(currentX, currentY));
|
|
126
|
+
break;
|
|
127
|
+
case 'V':
|
|
128
|
+
currentY = D(args[0]);
|
|
129
|
+
points.push(PolygonClip.point(currentX, currentY));
|
|
130
|
+
break;
|
|
131
|
+
case 'v':
|
|
132
|
+
currentY = currentY.plus(args[0]);
|
|
133
|
+
points.push(PolygonClip.point(currentX, currentY));
|
|
134
|
+
break;
|
|
135
|
+
case 'C':
|
|
136
|
+
sampleCubicBezier(points, currentX, currentY,
|
|
137
|
+
D(args[0]), D(args[1]), D(args[2]), D(args[3]), D(args[4]), D(args[5]),
|
|
138
|
+
samplesPerCurve);
|
|
139
|
+
currentX = D(args[4]); currentY = D(args[5]);
|
|
140
|
+
break;
|
|
141
|
+
case 'c':
|
|
142
|
+
sampleCubicBezier(points, currentX, currentY,
|
|
143
|
+
currentX.plus(args[0]), currentY.plus(args[1]),
|
|
144
|
+
currentX.plus(args[2]), currentY.plus(args[3]),
|
|
145
|
+
currentX.plus(args[4]), currentY.plus(args[5]),
|
|
146
|
+
samplesPerCurve);
|
|
147
|
+
currentX = currentX.plus(args[4]); currentY = currentY.plus(args[5]);
|
|
148
|
+
break;
|
|
149
|
+
case 'Q':
|
|
150
|
+
sampleQuadraticBezier(points, currentX, currentY,
|
|
151
|
+
D(args[0]), D(args[1]), D(args[2]), D(args[3]),
|
|
152
|
+
samplesPerCurve);
|
|
153
|
+
currentX = D(args[2]); currentY = D(args[3]);
|
|
154
|
+
break;
|
|
155
|
+
case 'q':
|
|
156
|
+
sampleQuadraticBezier(points, currentX, currentY,
|
|
157
|
+
currentX.plus(args[0]), currentY.plus(args[1]),
|
|
158
|
+
currentX.plus(args[2]), currentY.plus(args[3]),
|
|
159
|
+
samplesPerCurve);
|
|
160
|
+
currentX = currentX.plus(args[2]); currentY = currentY.plus(args[3]);
|
|
161
|
+
break;
|
|
162
|
+
case 'A':
|
|
163
|
+
sampleArc(points, currentX, currentY,
|
|
164
|
+
D(args[0]), D(args[1]), D(args[2]), args[3], args[4],
|
|
165
|
+
D(args[5]), D(args[6]), samplesPerCurve);
|
|
166
|
+
currentX = D(args[5]); currentY = D(args[6]);
|
|
167
|
+
break;
|
|
168
|
+
case 'a':
|
|
169
|
+
sampleArc(points, currentX, currentY,
|
|
170
|
+
D(args[0]), D(args[1]), D(args[2]), args[3], args[4],
|
|
171
|
+
currentX.plus(args[5]), currentY.plus(args[6]), samplesPerCurve);
|
|
172
|
+
currentX = currentX.plus(args[5]); currentY = currentY.plus(args[6]);
|
|
173
|
+
break;
|
|
174
|
+
case 'Z': case 'z':
|
|
175
|
+
currentX = startX; currentY = startY;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return removeDuplicateConsecutive(points);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Parse SVG path data into individual commands with arguments.
|
|
185
|
+
*
|
|
186
|
+
* Extracts path commands (M, L, C, Q, A, Z, etc.) and their numeric arguments from
|
|
187
|
+
* an SVG path data string.
|
|
188
|
+
*
|
|
189
|
+
* @private
|
|
190
|
+
* @param {string} pathData - SVG path d attribute string
|
|
191
|
+
* @returns {Array<{type: string, args: Array<number>}>} Array of command objects,
|
|
192
|
+
* each containing a command type (single letter) and array of numeric arguments
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* parsePathCommands("M 10 20 L 30 40")
|
|
196
|
+
* // Returns: [{type: 'M', args: [10, 20]}, {type: 'L', args: [30, 40]}]
|
|
197
|
+
*/
|
|
198
|
+
function parsePathCommands(pathData) {
|
|
199
|
+
const commands = [];
|
|
200
|
+
const regex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
|
|
201
|
+
let match;
|
|
202
|
+
while ((match = regex.exec(pathData)) !== null) {
|
|
203
|
+
const type = match[1];
|
|
204
|
+
const argsStr = match[2].trim();
|
|
205
|
+
const args = argsStr.length > 0
|
|
206
|
+
? argsStr.split(/[\s,]+/).filter(s => s.length > 0).map(Number)
|
|
207
|
+
: [];
|
|
208
|
+
commands.push({ type, args });
|
|
209
|
+
}
|
|
210
|
+
return commands;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Sample points along a cubic Bezier curve and append them to the points array.
|
|
215
|
+
*
|
|
216
|
+
* Uses the cubic Bezier formula: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
|
|
217
|
+
* where t ranges from 0 to 1.
|
|
218
|
+
*
|
|
219
|
+
* @private
|
|
220
|
+
* @param {Array<{x: Decimal, y: Decimal}>} points - Array to append sampled points to
|
|
221
|
+
* @param {Decimal} x0 - Start point x coordinate
|
|
222
|
+
* @param {Decimal} y0 - Start point y coordinate
|
|
223
|
+
* @param {Decimal} x1 - First control point x coordinate
|
|
224
|
+
* @param {Decimal} y1 - First control point y coordinate
|
|
225
|
+
* @param {Decimal} x2 - Second control point x coordinate
|
|
226
|
+
* @param {Decimal} y2 - Second control point y coordinate
|
|
227
|
+
* @param {Decimal} x3 - End point x coordinate
|
|
228
|
+
* @param {Decimal} y3 - End point y coordinate
|
|
229
|
+
* @param {number} samples - Number of points to sample along the curve
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* const points = [];
|
|
233
|
+
* sampleCubicBezier(points, D(0), D(0), D(50), D(0), D(50), D(100), D(100), D(100), 20);
|
|
234
|
+
* // points now contains 20 sampled points along the cubic Bezier curve
|
|
235
|
+
*/
|
|
236
|
+
function sampleCubicBezier(points, x0, y0, x1, y1, x2, y2, x3, y3, samples) {
|
|
237
|
+
for (let i = 1; i <= samples; i++) {
|
|
238
|
+
const t = D(i).div(samples);
|
|
239
|
+
const mt = D(1).minus(t);
|
|
240
|
+
const mt2 = mt.mul(mt), mt3 = mt2.mul(mt);
|
|
241
|
+
const t2 = t.mul(t), t3 = t2.mul(t);
|
|
242
|
+
const x = mt3.mul(x0).plus(D(3).mul(mt2).mul(t).mul(x1))
|
|
243
|
+
.plus(D(3).mul(mt).mul(t2).mul(x2)).plus(t3.mul(x3));
|
|
244
|
+
const y = mt3.mul(y0).plus(D(3).mul(mt2).mul(t).mul(y1))
|
|
245
|
+
.plus(D(3).mul(mt).mul(t2).mul(y2)).plus(t3.mul(y3));
|
|
246
|
+
points.push(PolygonClip.point(x, y));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Sample points along a quadratic Bezier curve and append them to the points array.
|
|
252
|
+
*
|
|
253
|
+
* Uses the quadratic Bezier formula: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂
|
|
254
|
+
* where t ranges from 0 to 1.
|
|
255
|
+
*
|
|
256
|
+
* @private
|
|
257
|
+
* @param {Array<{x: Decimal, y: Decimal}>} points - Array to append sampled points to
|
|
258
|
+
* @param {Decimal} x0 - Start point x coordinate
|
|
259
|
+
* @param {Decimal} y0 - Start point y coordinate
|
|
260
|
+
* @param {Decimal} x1 - Control point x coordinate
|
|
261
|
+
* @param {Decimal} y1 - Control point y coordinate
|
|
262
|
+
* @param {Decimal} x2 - End point x coordinate
|
|
263
|
+
* @param {Decimal} y2 - End point y coordinate
|
|
264
|
+
* @param {number} samples - Number of points to sample along the curve
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* const points = [];
|
|
268
|
+
* sampleQuadraticBezier(points, D(0), D(0), D(50), D(50), D(100), D(0), 20);
|
|
269
|
+
* // points now contains 20 sampled points along the quadratic Bezier curve
|
|
270
|
+
*/
|
|
271
|
+
function sampleQuadraticBezier(points, x0, y0, x1, y1, x2, y2, samples) {
|
|
272
|
+
for (let i = 1; i <= samples; i++) {
|
|
273
|
+
const t = D(i).div(samples);
|
|
274
|
+
const mt = D(1).minus(t);
|
|
275
|
+
const mt2 = mt.mul(mt), t2 = t.mul(t);
|
|
276
|
+
const x = mt2.mul(x0).plus(D(2).mul(mt).mul(t).mul(x1)).plus(t2.mul(x2));
|
|
277
|
+
const y = mt2.mul(y0).plus(D(2).mul(mt).mul(t).mul(y1)).plus(t2.mul(y2));
|
|
278
|
+
points.push(PolygonClip.point(x, y));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Sample points along an elliptical arc and append them to the points array.
|
|
284
|
+
*
|
|
285
|
+
* Converts SVG arc parameters to center parameterization and samples points along
|
|
286
|
+
* the arc. Implements the SVG arc conversion algorithm from the SVG specification.
|
|
287
|
+
*
|
|
288
|
+
* @private
|
|
289
|
+
* @param {Array<{x: Decimal, y: Decimal}>} points - Array to append sampled points to
|
|
290
|
+
* @param {Decimal} x0 - Start point x coordinate
|
|
291
|
+
* @param {Decimal} y0 - Start point y coordinate
|
|
292
|
+
* @param {Decimal} rx - X-axis radius of the ellipse
|
|
293
|
+
* @param {Decimal} ry - Y-axis radius of the ellipse
|
|
294
|
+
* @param {Decimal} xAxisRotation - Rotation angle of the ellipse x-axis (in degrees)
|
|
295
|
+
* @param {number} largeArc - Large arc flag (0 or 1): whether to take the larger arc
|
|
296
|
+
* @param {number} sweep - Sweep flag (0 or 1): direction of the arc (0=counterclockwise, 1=clockwise)
|
|
297
|
+
* @param {Decimal} x1 - End point x coordinate
|
|
298
|
+
* @param {Decimal} y1 - End point y coordinate
|
|
299
|
+
* @param {number} samples - Number of points to sample along the arc
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* const points = [];
|
|
303
|
+
* // Sample an arc from (0,0) to (100,100) with radii 50,50
|
|
304
|
+
* sampleArc(points, D(0), D(0), D(50), D(50), D(0), 0, 1, D(100), D(100), 20);
|
|
305
|
+
*/
|
|
306
|
+
function sampleArc(points, x0, y0, rx, ry, xAxisRotation, largeArc, sweep, x1, y1, samples) {
|
|
307
|
+
if (rx.eq(0) || ry.eq(0)) { points.push(PolygonClip.point(x1, y1)); return; }
|
|
308
|
+
rx = rx.abs(); ry = ry.abs();
|
|
309
|
+
|
|
310
|
+
const phi = xAxisRotation.mul(Math.PI).div(180);
|
|
311
|
+
const cosPhi = phi.cos(), sinPhi = phi.sin();
|
|
312
|
+
const dx = x0.minus(x1).div(2), dy = y0.minus(y1).div(2);
|
|
313
|
+
const x1p = cosPhi.mul(dx).plus(sinPhi.mul(dy));
|
|
314
|
+
const y1p = cosPhi.mul(dy).minus(sinPhi.mul(dx));
|
|
315
|
+
|
|
316
|
+
let rx2 = rx.mul(rx), ry2 = ry.mul(ry);
|
|
317
|
+
const x1p2 = x1p.mul(x1p), y1p2 = y1p.mul(y1p);
|
|
318
|
+
const lambda = x1p2.div(rx2).plus(y1p2.div(ry2));
|
|
319
|
+
if (lambda.gt(1)) {
|
|
320
|
+
const sqrtLambda = lambda.sqrt();
|
|
321
|
+
rx = rx.mul(sqrtLambda); ry = ry.mul(sqrtLambda);
|
|
322
|
+
rx2 = rx.mul(rx); ry2 = ry.mul(ry);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let sq = rx2.mul(ry2).minus(rx2.mul(y1p2)).minus(ry2.mul(x1p2));
|
|
326
|
+
sq = sq.div(rx2.mul(y1p2).plus(ry2.mul(x1p2)));
|
|
327
|
+
if (sq.lt(0)) sq = D(0);
|
|
328
|
+
sq = sq.sqrt();
|
|
329
|
+
if (largeArc === sweep) sq = sq.neg();
|
|
330
|
+
|
|
331
|
+
const cxp = sq.mul(rx).mul(y1p).div(ry);
|
|
332
|
+
const cyp = sq.neg().mul(ry).mul(x1p).div(rx);
|
|
333
|
+
const midX = x0.plus(x1).div(2), midY = y0.plus(y1).div(2);
|
|
334
|
+
const cx = cosPhi.mul(cxp).minus(sinPhi.mul(cyp)).plus(midX);
|
|
335
|
+
const cy = sinPhi.mul(cxp).plus(cosPhi.mul(cyp)).plus(midY);
|
|
336
|
+
|
|
337
|
+
const ux = x1p.minus(cxp).div(rx), uy = y1p.minus(cyp).div(ry);
|
|
338
|
+
const vx = x1p.neg().minus(cxp).div(rx), vy = y1p.neg().minus(cyp).div(ry);
|
|
339
|
+
const n1 = ux.mul(ux).plus(uy.mul(uy)).sqrt();
|
|
340
|
+
let theta1 = Decimal.acos(ux.div(n1));
|
|
341
|
+
if (uy.lt(0)) theta1 = theta1.neg();
|
|
342
|
+
|
|
343
|
+
const n2 = n1.mul(vx.mul(vx).plus(vy.mul(vy)).sqrt());
|
|
344
|
+
let dtheta = Decimal.acos(ux.mul(vx).plus(uy.mul(vy)).div(n2));
|
|
345
|
+
if (ux.mul(vy).minus(uy.mul(vx)).lt(0)) dtheta = dtheta.neg();
|
|
346
|
+
|
|
347
|
+
const PI = D(Math.PI), TWO_PI = PI.mul(2);
|
|
348
|
+
if (sweep && dtheta.lt(0)) dtheta = dtheta.plus(TWO_PI);
|
|
349
|
+
else if (!sweep && dtheta.gt(0)) dtheta = dtheta.minus(TWO_PI);
|
|
350
|
+
|
|
351
|
+
for (let i = 1; i <= samples; i++) {
|
|
352
|
+
const t = D(i).div(samples);
|
|
353
|
+
const theta = theta1.plus(dtheta.mul(t));
|
|
354
|
+
const cosTheta = theta.cos(), sinTheta = theta.sin();
|
|
355
|
+
const x = cosPhi.mul(rx.mul(cosTheta)).minus(sinPhi.mul(ry.mul(sinTheta))).plus(cx);
|
|
356
|
+
const y = sinPhi.mul(rx.mul(cosTheta)).plus(cosPhi.mul(ry.mul(sinTheta))).plus(cy);
|
|
357
|
+
points.push(PolygonClip.point(x, y));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Remove consecutive duplicate points from a polygon.
|
|
363
|
+
*
|
|
364
|
+
* Compares each point with the previous point and removes duplicates that appear
|
|
365
|
+
* consecutively. This is necessary because curve sampling may produce identical
|
|
366
|
+
* consecutive points at curve endpoints.
|
|
367
|
+
*
|
|
368
|
+
* @private
|
|
369
|
+
* @param {Array<{x: Decimal, y: Decimal}>} points - Array of polygon vertices
|
|
370
|
+
* @returns {Array<{x: Decimal, y: Decimal}>} Array with consecutive duplicates removed
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* const points = [{x: D(0), y: D(0)}, {x: D(0), y: D(0)}, {x: D(1), y: D(1)}];
|
|
374
|
+
* const clean = removeDuplicateConsecutive(points);
|
|
375
|
+
* // Returns: [{x: D(0), y: D(0)}, {x: D(1), y: D(1)}]
|
|
376
|
+
*/
|
|
377
|
+
function removeDuplicateConsecutive(points) {
|
|
378
|
+
if (points.length < 2) return points;
|
|
379
|
+
const result = [points[0]];
|
|
380
|
+
for (let i = 1; i < points.length; i++) {
|
|
381
|
+
if (!PolygonClip.pointsEqual(points[i], result[result.length - 1])) {
|
|
382
|
+
result.push(points[i]);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Convert an SVG shape element to a polygon by sampling its geometry.
|
|
390
|
+
*
|
|
391
|
+
* Supports various SVG shape elements (circle, ellipse, rect, line, polygon, polyline, path)
|
|
392
|
+
* and converts them to polygons with Decimal-precision coordinates. Applies element transforms
|
|
393
|
+
* and optional current transformation matrix (CTM).
|
|
394
|
+
*
|
|
395
|
+
* Shape elements are first converted to path data, then transforms are applied, and finally
|
|
396
|
+
* the path is sampled into a polygon.
|
|
397
|
+
*
|
|
398
|
+
* @param {Object} element - SVG element object with properties like type, cx, cy, r, d, etc.
|
|
399
|
+
* @param {Object} element.type - Shape type: 'circle', 'ellipse', 'rect', 'line', 'polygon', 'polyline', or 'path'
|
|
400
|
+
* @param {Matrix|null} [ctm=null] - Current transformation matrix (3x3) to apply to the shape.
|
|
401
|
+
* If null, no additional transform is applied beyond the element's own transform.
|
|
402
|
+
* @param {number} [samples=20] - Number of sample points per curve segment for path sampling
|
|
403
|
+
* @returns {Array<{x: Decimal, y: Decimal}>} Polygon vertices representing the shape
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* // Convert a circle to polygon
|
|
407
|
+
* const circle = { type: 'circle', cx: 50, cy: 50, r: 25 };
|
|
408
|
+
* const polygon = shapeToPolygon(circle);
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* // Convert a rect with transform and CTM
|
|
412
|
+
* const rect = { type: 'rect', x: 0, y: 0, width: 100, height: 50, transform: 'rotate(45)' };
|
|
413
|
+
* const ctm = Transforms2D.translation(10, 20);
|
|
414
|
+
* const polygon = shapeToPolygon(rect, ctm, 30);
|
|
415
|
+
*/
|
|
416
|
+
export function shapeToPolygon(element, ctm = null, samples = DEFAULT_CURVE_SAMPLES) {
|
|
417
|
+
let pathData;
|
|
418
|
+
switch (element.type) {
|
|
419
|
+
case 'circle':
|
|
420
|
+
pathData = circleToPath(D(element.cx || 0), D(element.cy || 0), D(element.r || 0));
|
|
421
|
+
break;
|
|
422
|
+
case 'ellipse':
|
|
423
|
+
pathData = ellipseToPath(D(element.cx || 0), D(element.cy || 0), D(element.rx || 0), D(element.ry || 0));
|
|
424
|
+
break;
|
|
425
|
+
case 'rect':
|
|
426
|
+
pathData = rectToPath(D(element.x || 0), D(element.y || 0), D(element.width || 0), D(element.height || 0),
|
|
427
|
+
D(element.rx || 0), element.ry !== undefined ? D(element.ry) : null);
|
|
428
|
+
break;
|
|
429
|
+
case 'line':
|
|
430
|
+
pathData = lineToPath(D(element.x1 || 0), D(element.y1 || 0), D(element.x2 || 0), D(element.y2 || 0));
|
|
431
|
+
break;
|
|
432
|
+
case 'polygon':
|
|
433
|
+
pathData = polygonToPath(element.points || '');
|
|
434
|
+
break;
|
|
435
|
+
case 'polyline':
|
|
436
|
+
pathData = polylineToPath(element.points || '');
|
|
437
|
+
break;
|
|
438
|
+
case 'path':
|
|
439
|
+
pathData = element.d || '';
|
|
440
|
+
break;
|
|
441
|
+
default:
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (element.transform) {
|
|
446
|
+
const elementTransform = parseTransform(element.transform);
|
|
447
|
+
pathData = transformPathData(pathData, elementTransform);
|
|
448
|
+
}
|
|
449
|
+
if (ctm) {
|
|
450
|
+
pathData = transformPathData(pathData, ctm);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return pathToPolygon(pathData, samples);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Resolve a clipPath element into a unified clipping polygon.
|
|
458
|
+
*
|
|
459
|
+
* Takes a clipPath definition and converts all its child shapes into a single polygon
|
|
460
|
+
* by performing boolean union operations. Handles both coordinate systems (userSpaceOnUse
|
|
461
|
+
* and objectBoundingBox) and applies appropriate transforms.
|
|
462
|
+
*
|
|
463
|
+
* ## Coordinate System Handling
|
|
464
|
+
*
|
|
465
|
+
* - **userSpaceOnUse** (default): clipPath coordinates are in the same coordinate system
|
|
466
|
+
* as the element being clipped. The CTM and clipPath transform are applied directly.
|
|
467
|
+
*
|
|
468
|
+
* - **objectBoundingBox**: clipPath coordinates are fractions (0-1) of the target element's
|
|
469
|
+
* bounding box. A bounding box transform is computed and applied to scale/translate the
|
|
470
|
+
* clipPath into the target element's space.
|
|
471
|
+
*
|
|
472
|
+
* ## Transform Application Order
|
|
473
|
+
*
|
|
474
|
+
* 1. Start with CTM (current transformation matrix)
|
|
475
|
+
* 2. Apply clipPath's own transform attribute
|
|
476
|
+
* 3. If objectBoundingBox, apply bounding box scaling/translation
|
|
477
|
+
* 4. Apply to each child shape
|
|
478
|
+
*
|
|
479
|
+
* @param {Object} clipPathDef - clipPath definition object
|
|
480
|
+
* @param {string} [clipPathDef.clipPathUnits='userSpaceOnUse'] - Coordinate system: 'userSpaceOnUse' or 'objectBoundingBox'
|
|
481
|
+
* @param {string} [clipPathDef.transform] - Optional transform attribute for the clipPath
|
|
482
|
+
* @param {Array<Object>} [clipPathDef.children=[]] - Child shape elements to clip with
|
|
483
|
+
* @param {Object|null} targetElement - The element being clipped (needed for objectBoundingBox)
|
|
484
|
+
* @param {Matrix|null} [ctm=null] - Current transformation matrix (3x3)
|
|
485
|
+
* @param {Object} [options={}] - Additional options
|
|
486
|
+
* @param {number} [options.samples=20] - Number of sample points per curve segment
|
|
487
|
+
* @returns {Array<{x: Decimal, y: Decimal}>} Unified clipping polygon (union of all child shapes)
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* // Resolve clipPath with userSpaceOnUse (default)
|
|
491
|
+
* const clipDef = {
|
|
492
|
+
* clipPathUnits: 'userSpaceOnUse',
|
|
493
|
+
* children: [
|
|
494
|
+
* { type: 'circle', cx: 50, cy: 50, r: 40 },
|
|
495
|
+
* { type: 'rect', x: 40, y: 40, width: 20, height: 20 }
|
|
496
|
+
* ]
|
|
497
|
+
* };
|
|
498
|
+
* const clipPolygon = resolveClipPath(clipDef, null);
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* // Resolve clipPath with objectBoundingBox
|
|
502
|
+
* const clipDef = {
|
|
503
|
+
* clipPathUnits: 'objectBoundingBox',
|
|
504
|
+
* children: [{ type: 'circle', cx: 0.5, cy: 0.5, r: 0.4 }]
|
|
505
|
+
* };
|
|
506
|
+
* const target = { type: 'rect', x: 0, y: 0, width: 200, height: 100 };
|
|
507
|
+
* const clipPolygon = resolveClipPath(clipDef, target);
|
|
508
|
+
* // Circle will be scaled to bbox: center at (100,50), radius 40 (0.4 * min(200,100))
|
|
509
|
+
*/
|
|
510
|
+
export function resolveClipPath(clipPathDef, targetElement, ctm = null, options = {}) {
|
|
511
|
+
const { samples = DEFAULT_CURVE_SAMPLES } = options;
|
|
512
|
+
const clipPathUnits = clipPathDef.clipPathUnits || 'userSpaceOnUse';
|
|
513
|
+
let clipTransform = ctm ? ctm.clone() : Matrix.identity(3);
|
|
514
|
+
|
|
515
|
+
if (clipPathDef.transform) {
|
|
516
|
+
const clipPathTransformMatrix = parseTransform(clipPathDef.transform);
|
|
517
|
+
clipTransform = clipTransform.mul(clipPathTransformMatrix);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (clipPathUnits === 'objectBoundingBox' && targetElement) {
|
|
521
|
+
const bbox = getElementBoundingBox(targetElement);
|
|
522
|
+
if (bbox) {
|
|
523
|
+
const bboxTransform = Transforms2D.translation(bbox.x, bbox.y)
|
|
524
|
+
.mul(Transforms2D.scale(bbox.width, bbox.height));
|
|
525
|
+
clipTransform = clipTransform.mul(bboxTransform);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const clipPolygons = [];
|
|
530
|
+
for (const child of (clipPathDef.children || [])) {
|
|
531
|
+
const polygon = shapeToPolygon(child, clipTransform, samples);
|
|
532
|
+
if (polygon.length >= 3) clipPolygons.push(polygon);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (clipPolygons.length === 0) return [];
|
|
536
|
+
if (clipPolygons.length === 1) return clipPolygons[0];
|
|
537
|
+
|
|
538
|
+
let unified = clipPolygons[0];
|
|
539
|
+
for (let i = 1; i < clipPolygons.length; i++) {
|
|
540
|
+
const unionResult = PolygonClip.polygonUnion(unified, clipPolygons[i]);
|
|
541
|
+
if (unionResult.length > 0) unified = unionResult[0];
|
|
542
|
+
}
|
|
543
|
+
return unified;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Apply a clipPath to an element, returning the clipped geometry.
|
|
548
|
+
*
|
|
549
|
+
* Performs the complete clipping operation by:
|
|
550
|
+
* 1. Resolving the clipPath definition into a clipping polygon
|
|
551
|
+
* 2. Converting the target element to a polygon
|
|
552
|
+
* 3. Computing the intersection of the two polygons
|
|
553
|
+
*
|
|
554
|
+
* This is the main function for applying clip paths to elements. The result is a
|
|
555
|
+
* polygon representing the visible portion of the element after clipping.
|
|
556
|
+
*
|
|
557
|
+
* @param {Object} element - SVG element to be clipped (rect, circle, path, etc.)
|
|
558
|
+
* @param {Object} clipPathDef - clipPath definition object (see resolveClipPath)
|
|
559
|
+
* @param {Matrix|null} [ctm=null] - Current transformation matrix (3x3) to apply
|
|
560
|
+
* @param {Object} [options={}] - Additional options
|
|
561
|
+
* @param {number} [options.samples=20] - Number of sample points per curve segment
|
|
562
|
+
* @returns {Array<{x: Decimal, y: Decimal}>} Clipped polygon representing the intersection
|
|
563
|
+
* of the element and clipPath. Empty array if no intersection or invalid input.
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* // Clip a rectangle with a circular clipPath
|
|
567
|
+
* const rect = { type: 'rect', x: 0, y: 0, width: 100, height: 100 };
|
|
568
|
+
* const clipDef = {
|
|
569
|
+
* children: [{ type: 'circle', cx: 50, cy: 50, r: 40 }]
|
|
570
|
+
* };
|
|
571
|
+
* const clipped = applyClipPath(rect, clipDef);
|
|
572
|
+
* // Returns polygon approximating the intersection (rounded corners of rect)
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
575
|
+
* // Clip with objectBoundingBox coordinate system
|
|
576
|
+
* const ellipse = { type: 'ellipse', cx: 100, cy: 100, rx: 80, ry: 60 };
|
|
577
|
+
* const clipDef = {
|
|
578
|
+
* clipPathUnits: 'objectBoundingBox',
|
|
579
|
+
* children: [{ type: 'rect', x: 0.25, y: 0.25, width: 0.5, height: 0.5 }]
|
|
580
|
+
* };
|
|
581
|
+
* const clipped = applyClipPath(ellipse, clipDef, null, { samples: 50 });
|
|
582
|
+
*/
|
|
583
|
+
export function applyClipPath(element, clipPathDef, ctm = null, options = {}) {
|
|
584
|
+
const { samples = DEFAULT_CURVE_SAMPLES } = options;
|
|
585
|
+
const clipPolygon = resolveClipPath(clipPathDef, element, ctm, options);
|
|
586
|
+
if (clipPolygon.length < 3) return [];
|
|
587
|
+
|
|
588
|
+
const elementPolygon = shapeToPolygon(element, ctm, samples);
|
|
589
|
+
if (elementPolygon.length < 3) return [];
|
|
590
|
+
|
|
591
|
+
return PolygonClip.polygonIntersection(elementPolygon, clipPolygon);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Compute the bounding box of an SVG element.
|
|
596
|
+
*
|
|
597
|
+
* For simple shapes (rect, circle, ellipse), the bounding box is computed analytically.
|
|
598
|
+
* For complex shapes (path, polygon, etc.), the element is converted to a polygon and
|
|
599
|
+
* the bounding box is computed from the polygon vertices.
|
|
600
|
+
*
|
|
601
|
+
* This is used when clipPathUnits='objectBoundingBox' to determine the scaling and
|
|
602
|
+
* translation needed to map fractional coordinates to the element's actual bounds.
|
|
603
|
+
*
|
|
604
|
+
* @private
|
|
605
|
+
* @param {Object} element - SVG element object
|
|
606
|
+
* @returns {Object|null} Bounding box {x, y, width, height} with Decimal values, or null if invalid
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* const rect = { type: 'rect', x: 10, y: 20, width: 100, height: 50 };
|
|
610
|
+
* const bbox = getElementBoundingBox(rect);
|
|
611
|
+
* // Returns: {x: Decimal(10), y: Decimal(20), width: Decimal(100), height: Decimal(50)}
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* const circle = { type: 'circle', cx: 50, cy: 50, r: 25 };
|
|
615
|
+
* const bbox = getElementBoundingBox(circle);
|
|
616
|
+
* // Returns: {x: Decimal(25), y: Decimal(25), width: Decimal(50), height: Decimal(50)}
|
|
617
|
+
*/
|
|
618
|
+
function getElementBoundingBox(element) {
|
|
619
|
+
switch (element.type) {
|
|
620
|
+
case 'rect':
|
|
621
|
+
return { x: D(element.x || 0), y: D(element.y || 0),
|
|
622
|
+
width: D(element.width || 0), height: D(element.height || 0) };
|
|
623
|
+
case 'circle': {
|
|
624
|
+
const cx = D(element.cx || 0), cy = D(element.cy || 0), r = D(element.r || 0);
|
|
625
|
+
return { x: cx.minus(r), y: cy.minus(r), width: r.mul(2), height: r.mul(2) };
|
|
626
|
+
}
|
|
627
|
+
case 'ellipse': {
|
|
628
|
+
const cx = D(element.cx || 0), cy = D(element.cy || 0);
|
|
629
|
+
const rx = D(element.rx || 0), ry = D(element.ry || 0);
|
|
630
|
+
return { x: cx.minus(rx), y: cy.minus(ry), width: rx.mul(2), height: ry.mul(2) };
|
|
631
|
+
}
|
|
632
|
+
default: {
|
|
633
|
+
const polygon = shapeToPolygon(element, null, 10);
|
|
634
|
+
if (polygon.length > 0) {
|
|
635
|
+
const bbox = PolygonClip.boundingBox(polygon);
|
|
636
|
+
return { x: bbox.minX, y: bbox.minY,
|
|
637
|
+
width: bbox.maxX.minus(bbox.minX), height: bbox.maxY.minus(bbox.minY) };
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Convert a polygon back to SVG path data string.
|
|
646
|
+
*
|
|
647
|
+
* Takes an array of polygon vertices with Decimal coordinates and generates an SVG
|
|
648
|
+
* path data string (d attribute) representing the closed polygon. Coordinates are
|
|
649
|
+
* formatted with the specified precision and trailing zeros are removed.
|
|
650
|
+
*
|
|
651
|
+
* The generated path starts with M (moveto) to the first point, followed by L (lineto)
|
|
652
|
+
* commands for each subsequent point, and ends with Z (closepath).
|
|
653
|
+
*
|
|
654
|
+
* @param {Array<{x: Decimal, y: Decimal}>} polygon - Array of polygon vertices
|
|
655
|
+
* @param {number} [precision=6] - Number of decimal places for coordinate formatting
|
|
656
|
+
* @returns {string} SVG path data string (e.g., "M 0 0 L 100 0 L 100 100 L 0 100 Z")
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* const polygon = [
|
|
660
|
+
* {x: D(0), y: D(0)},
|
|
661
|
+
* {x: D(100), y: D(0)},
|
|
662
|
+
* {x: D(100), y: D(100)},
|
|
663
|
+
* {x: D(0), y: D(100)}
|
|
664
|
+
* ];
|
|
665
|
+
* const pathData = polygonToPathData(polygon);
|
|
666
|
+
* // Returns: "M 0 0 L 100 0 L 100 100 L 0 100 Z"
|
|
667
|
+
*
|
|
668
|
+
* @example
|
|
669
|
+
* // Higher precision for decimal coordinates
|
|
670
|
+
* const polygon = [{x: D('0.123456789'), y: D('0.987654321')}];
|
|
671
|
+
* const pathData = polygonToPathData(polygon, 8);
|
|
672
|
+
* // Returns: "M 0.12345679 0.98765432 Z"
|
|
673
|
+
*/
|
|
674
|
+
export function polygonToPathData(polygon, precision = 6) {
|
|
675
|
+
if (polygon.length < 2) return '';
|
|
676
|
+
const fmt = n => (n instanceof Decimal ? n : D(n)).toFixed(precision).replace(/\.?0+$/, '');
|
|
677
|
+
let d = `M ${fmt(polygon[0].x)} ${fmt(polygon[0].y)}`;
|
|
678
|
+
for (let i = 1; i < polygon.length; i++) {
|
|
679
|
+
d += ` L ${fmt(polygon[i].x)} ${fmt(polygon[i].y)}`;
|
|
680
|
+
}
|
|
681
|
+
return d + ' Z';
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Resolve nested clipPaths recursively with cycle detection.
|
|
686
|
+
*
|
|
687
|
+
* SVG allows clipPath elements to reference other clipPaths via the clip-path attribute.
|
|
688
|
+
* This function resolves such nested references by:
|
|
689
|
+
* 1. Resolving the current clipPath into a polygon
|
|
690
|
+
* 2. Checking if this clipPath has a clip-path reference
|
|
691
|
+
* 3. If yes, recursively resolving the referenced clipPath
|
|
692
|
+
* 4. Intersecting the two polygons to get the final clip region
|
|
693
|
+
*
|
|
694
|
+
* Cycle detection prevents infinite recursion if clipPaths reference each other circularly.
|
|
695
|
+
*
|
|
696
|
+
* @param {Object} clipPathDef - clipPath definition object
|
|
697
|
+
* @param {string} [clipPathDef.id] - ID of this clipPath (for cycle detection)
|
|
698
|
+
* @param {string} [clipPathDef['clip-path']] - Reference to another clipPath (e.g., "url(#otherClip)")
|
|
699
|
+
* @param {Map<string, Object>} defsMap - Map of clipPath IDs to their definitions
|
|
700
|
+
* @param {Object|null} targetElement - The element being clipped
|
|
701
|
+
* @param {Matrix|null} [ctm=null] - Current transformation matrix (3x3)
|
|
702
|
+
* @param {Set<string>} [visited=new Set()] - Set of visited clipPath IDs for cycle detection
|
|
703
|
+
* @param {Object} [options={}] - Additional options (see resolveClipPath)
|
|
704
|
+
* @returns {Array<{x: Decimal, y: Decimal}>} Resolved clipping polygon (intersection of nested clips)
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* // Define two clipPaths where one references the other
|
|
708
|
+
* const defsMap = new Map([
|
|
709
|
+
* ['clip1', {
|
|
710
|
+
* id: 'clip1',
|
|
711
|
+
* children: [{ type: 'circle', cx: 50, cy: 50, r: 40 }],
|
|
712
|
+
* 'clip-path': 'url(#clip2)'
|
|
713
|
+
* }],
|
|
714
|
+
* ['clip2', {
|
|
715
|
+
* id: 'clip2',
|
|
716
|
+
* children: [{ type: 'rect', x: 0, y: 0, width: 100, height: 100 }]
|
|
717
|
+
* }]
|
|
718
|
+
* ]);
|
|
719
|
+
* const target = { type: 'rect', x: 0, y: 0, width: 100, height: 100 };
|
|
720
|
+
* const polygon = resolveNestedClipPath(defsMap.get('clip1'), defsMap, target);
|
|
721
|
+
* // Returns intersection of circle and rect
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* // Circular reference detection
|
|
725
|
+
* const defsMap = new Map([
|
|
726
|
+
* ['clip1', { id: 'clip1', children: [...], 'clip-path': 'url(#clip2)' }],
|
|
727
|
+
* ['clip2', { id: 'clip2', children: [...], 'clip-path': 'url(#clip1)' }]
|
|
728
|
+
* ]);
|
|
729
|
+
* const polygon = resolveNestedClipPath(defsMap.get('clip1'), defsMap, target);
|
|
730
|
+
* // Logs warning about circular reference and returns clip1 polygon only
|
|
731
|
+
*/
|
|
732
|
+
export function resolveNestedClipPath(clipPathDef, defsMap, targetElement, ctm = null, visited = new Set(), options = {}) {
|
|
733
|
+
const clipId = clipPathDef.id;
|
|
734
|
+
if (clipId && visited.has(clipId)) {
|
|
735
|
+
console.warn(`Circular clipPath reference detected: ${clipId}`);
|
|
736
|
+
return [];
|
|
737
|
+
}
|
|
738
|
+
if (clipId) visited.add(clipId);
|
|
739
|
+
|
|
740
|
+
let clipPolygon = resolveClipPath(clipPathDef, targetElement, ctm, options);
|
|
741
|
+
|
|
742
|
+
if (clipPathDef['clip-path'] && clipPolygon.length >= 3) {
|
|
743
|
+
const nestedRef = clipPathDef['clip-path'].replace(/^url\(#?|[)'"]/g, '');
|
|
744
|
+
const nestedClipDef = defsMap.get(nestedRef);
|
|
745
|
+
if (nestedClipDef) {
|
|
746
|
+
const nestedClip = resolveNestedClipPath(nestedClipDef, defsMap, targetElement, ctm, visited, options);
|
|
747
|
+
if (nestedClip.length >= 3) {
|
|
748
|
+
const intersection = PolygonClip.polygonIntersection(clipPolygon, nestedClip);
|
|
749
|
+
clipPolygon = intersection.length > 0 ? intersection[0] : [];
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return clipPolygon;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export default {
|
|
757
|
+
pathToPolygon, shapeToPolygon, resolveClipPath, applyClipPath,
|
|
758
|
+
polygonToPathData, resolveNestedClipPath, DEFAULT_CURVE_SAMPLES
|
|
759
|
+
};
|