@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
package/src/geometry-to-path.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import Decimal from
|
|
2
|
-
import { Matrix } from
|
|
1
|
+
import Decimal from "decimal.js";
|
|
2
|
+
import { Matrix } from "./matrix.js";
|
|
3
3
|
|
|
4
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
4
|
+
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Standard kappa for 90° arcs (4 Bezier curves per circle).
|
|
8
8
|
* kappa = 4/3 * (sqrt(2) - 1) ≈ 0.5522847498
|
|
9
9
|
* Maximum radial error: ~0.027%
|
|
10
|
+
* @returns {Decimal} The kappa constant for 90-degree arc approximation
|
|
10
11
|
*/
|
|
11
12
|
export function getKappa() {
|
|
12
13
|
const two = new Decimal(2);
|
|
@@ -90,16 +91,20 @@ export function circleToPathDataHP(cx, cy, r, arcs = 8, precision = 6) {
|
|
|
90
91
|
*/
|
|
91
92
|
export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
|
|
92
93
|
// Enforce multiple of 4 for symmetry
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
let numArcs = arcs;
|
|
95
|
+
if (numArcs % 4 !== 0) {
|
|
96
|
+
numArcs = Math.ceil(numArcs / 4) * 4;
|
|
95
97
|
}
|
|
96
|
-
const cxD = D(cx),
|
|
97
|
-
|
|
98
|
+
const cxD = D(cx),
|
|
99
|
+
cyD = D(cy),
|
|
100
|
+
rxD = D(rx),
|
|
101
|
+
ryD = D(ry);
|
|
102
|
+
const f = (v) => formatNumber(v, precision);
|
|
98
103
|
|
|
99
104
|
// Angle per arc in radians
|
|
100
105
|
const PI = Decimal.acos(-1);
|
|
101
106
|
const TWO_PI = PI.mul(2);
|
|
102
|
-
const arcAngle = TWO_PI.div(
|
|
107
|
+
const arcAngle = TWO_PI.div(numArcs);
|
|
103
108
|
|
|
104
109
|
// Control point distance for this arc angle
|
|
105
110
|
const kappa = getKappaForArc(arcAngle);
|
|
@@ -107,7 +112,7 @@ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
|
|
|
107
112
|
// Generate path
|
|
108
113
|
const commands = [];
|
|
109
114
|
|
|
110
|
-
for (let i = 0; i <
|
|
115
|
+
for (let i = 0; i < numArcs; i++) {
|
|
111
116
|
const startAngle = arcAngle.mul(i);
|
|
112
117
|
const endAngle = arcAngle.mul(i + 1);
|
|
113
118
|
|
|
@@ -126,10 +131,10 @@ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
|
|
|
126
131
|
// Tangent at angle θ: (-sin(θ), cos(θ))
|
|
127
132
|
// Control point 1: start + kappa * tangent_at_start * radius
|
|
128
133
|
// Control point 2: end - kappa * tangent_at_end * radius
|
|
129
|
-
const tx0 = sinStart.neg();
|
|
130
|
-
const ty0 = cosStart;
|
|
131
|
-
const tx3 = sinEnd.neg();
|
|
132
|
-
const ty3 = cosEnd;
|
|
134
|
+
const tx0 = sinStart.neg(); // tangent x at start
|
|
135
|
+
const ty0 = cosStart; // tangent y at start
|
|
136
|
+
const tx3 = sinEnd.neg(); // tangent x at end
|
|
137
|
+
const ty3 = cosEnd; // tangent y at end
|
|
133
138
|
|
|
134
139
|
const x1 = x0.plus(kappa.mul(rxD).mul(tx0));
|
|
135
140
|
const y1 = y0.plus(kappa.mul(ryD).mul(ty0));
|
|
@@ -142,77 +147,212 @@ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
|
|
|
142
147
|
commands.push(`C${f(x1)} ${f(y1)} ${f(x2)} ${f(y2)} ${f(x3)} ${f(y3)}`);
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
commands.push(
|
|
146
|
-
return commands.join(
|
|
150
|
+
commands.push("Z");
|
|
151
|
+
return commands.join(" ");
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Format a number with specified precision, removing trailing zeros.
|
|
156
|
+
* @param {number|Decimal} value - Value to format
|
|
157
|
+
* @param {number} precision - Number of decimal places
|
|
158
|
+
* @returns {string} Formatted number string
|
|
159
|
+
*/
|
|
149
160
|
function formatNumber(value, precision = 6) {
|
|
150
161
|
// Format with precision then remove trailing zeros for smaller output
|
|
151
162
|
let str = value.toFixed(precision);
|
|
152
163
|
// Remove trailing zeros after decimal point
|
|
153
|
-
if (str.includes(
|
|
154
|
-
str = str.replace(/\.?0+$/,
|
|
164
|
+
if (str.includes(".")) {
|
|
165
|
+
str = str.replace(/\.?0+$/, "");
|
|
155
166
|
}
|
|
156
167
|
return str;
|
|
157
168
|
}
|
|
158
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Convert a circle to SVG path data using 4 Bezier arcs (standard kappa).
|
|
172
|
+
* @param {number|Decimal} cx - Center X coordinate
|
|
173
|
+
* @param {number|Decimal} cy - Center Y coordinate
|
|
174
|
+
* @param {number|Decimal} r - Radius
|
|
175
|
+
* @param {number} precision - Decimal precision for output coordinates
|
|
176
|
+
* @returns {string} SVG path data string
|
|
177
|
+
*/
|
|
159
178
|
export function circleToPathData(cx, cy, r, precision = 6) {
|
|
160
|
-
const cxD = D(cx),
|
|
179
|
+
const cxD = D(cx),
|
|
180
|
+
cyD = D(cy),
|
|
181
|
+
rD = D(r);
|
|
161
182
|
const k = getKappa().mul(rD);
|
|
162
|
-
const x0 = cxD.plus(rD),
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
const x0 = cxD.plus(rD),
|
|
184
|
+
y0 = cyD;
|
|
185
|
+
const c1x1 = x0,
|
|
186
|
+
c1y1 = y0.minus(k),
|
|
187
|
+
c1x2 = cxD.plus(k),
|
|
188
|
+
c1y2 = cyD.minus(rD),
|
|
189
|
+
x1 = cxD,
|
|
190
|
+
y1 = cyD.minus(rD);
|
|
191
|
+
const c2x1 = cxD.minus(k),
|
|
192
|
+
c2y1 = y1,
|
|
193
|
+
c2x2 = cxD.minus(rD),
|
|
194
|
+
c2y2 = cyD.minus(k),
|
|
195
|
+
x2 = cxD.minus(rD),
|
|
196
|
+
y2 = cyD;
|
|
197
|
+
const c3x1 = x2,
|
|
198
|
+
c3y1 = cyD.plus(k),
|
|
199
|
+
c3x2 = cxD.minus(k),
|
|
200
|
+
c3y2 = cyD.plus(rD),
|
|
201
|
+
x3 = cxD,
|
|
202
|
+
y3 = cyD.plus(rD);
|
|
203
|
+
const c4x1 = cxD.plus(k),
|
|
204
|
+
c4y1 = y3,
|
|
205
|
+
c4x2 = x0,
|
|
206
|
+
c4y2 = cyD.plus(k);
|
|
207
|
+
const f = (v) => formatNumber(v, precision);
|
|
168
208
|
return `M${f(x0)} ${f(y0)}C${f(c1x1)} ${f(c1y1)} ${f(c1x2)} ${f(c1y2)} ${f(x1)} ${f(y1)}C${f(c2x1)} ${f(c2y1)} ${f(c2x2)} ${f(c2y2)} ${f(x2)} ${f(y2)}C${f(c3x1)} ${f(c3y1)} ${f(c3x2)} ${f(c3y2)} ${f(x3)} ${f(y3)}C${f(c4x1)} ${f(c4y1)} ${f(c4x2)} ${f(c4y2)} ${f(x0)} ${f(y0)}Z`;
|
|
169
209
|
}
|
|
170
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Convert an ellipse to SVG path data using 4 Bezier arcs (standard kappa).
|
|
213
|
+
* @param {number|Decimal} cx - Center X coordinate
|
|
214
|
+
* @param {number|Decimal} cy - Center Y coordinate
|
|
215
|
+
* @param {number|Decimal} rx - X radius
|
|
216
|
+
* @param {number|Decimal} ry - Y radius
|
|
217
|
+
* @param {number} precision - Decimal precision for output coordinates
|
|
218
|
+
* @returns {string} SVG path data string
|
|
219
|
+
*/
|
|
171
220
|
export function ellipseToPathData(cx, cy, rx, ry, precision = 6) {
|
|
172
|
-
const cxD = D(cx),
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
221
|
+
const cxD = D(cx),
|
|
222
|
+
cyD = D(cy),
|
|
223
|
+
rxD = D(rx),
|
|
224
|
+
ryD = D(ry);
|
|
225
|
+
const kappa = getKappa(),
|
|
226
|
+
kx = kappa.mul(rxD),
|
|
227
|
+
ky = kappa.mul(ryD);
|
|
228
|
+
const x0 = cxD.plus(rxD),
|
|
229
|
+
y0 = cyD;
|
|
230
|
+
const c1x1 = x0,
|
|
231
|
+
c1y1 = y0.minus(ky),
|
|
232
|
+
c1x2 = cxD.plus(kx),
|
|
233
|
+
c1y2 = cyD.minus(ryD),
|
|
234
|
+
x1 = cxD,
|
|
235
|
+
y1 = cyD.minus(ryD);
|
|
236
|
+
const c2x1 = cxD.minus(kx),
|
|
237
|
+
c2y1 = y1,
|
|
238
|
+
c2x2 = cxD.minus(rxD),
|
|
239
|
+
c2y2 = cyD.minus(ky),
|
|
240
|
+
x2 = cxD.minus(rxD),
|
|
241
|
+
y2 = cyD;
|
|
242
|
+
const c3x1 = x2,
|
|
243
|
+
c3y1 = cyD.plus(ky),
|
|
244
|
+
c3x2 = cxD.minus(kx),
|
|
245
|
+
c3y2 = cyD.plus(ryD),
|
|
246
|
+
x3 = cxD,
|
|
247
|
+
y3 = cyD.plus(ryD);
|
|
248
|
+
const c4x1 = cxD.plus(kx),
|
|
249
|
+
c4y1 = y3,
|
|
250
|
+
c4x2 = x0,
|
|
251
|
+
c4y2 = cyD.plus(ky);
|
|
252
|
+
const f = (v) => formatNumber(v, precision);
|
|
180
253
|
return `M${f(x0)} ${f(y0)}C${f(c1x1)} ${f(c1y1)} ${f(c1x2)} ${f(c1y2)} ${f(x1)} ${f(y1)}C${f(c2x1)} ${f(c2y1)} ${f(c2x2)} ${f(c2y2)} ${f(x2)} ${f(y2)}C${f(c3x1)} ${f(c3y1)} ${f(c3x2)} ${f(c3y2)} ${f(x3)} ${f(y3)}C${f(c4x1)} ${f(c4y1)} ${f(c4x2)} ${f(c4y2)} ${f(x0)} ${f(y0)}Z`;
|
|
181
254
|
}
|
|
182
255
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Convert a rectangle to SVG path data, with optional rounded corners.
|
|
258
|
+
* @param {number|Decimal} x - Top-left X coordinate
|
|
259
|
+
* @param {number|Decimal} y - Top-left Y coordinate
|
|
260
|
+
* @param {number|Decimal} width - Rectangle width
|
|
261
|
+
* @param {number|Decimal} height - Rectangle height
|
|
262
|
+
* @param {number|Decimal} rx - X-axis corner radius (default 0)
|
|
263
|
+
* @param {number|Decimal|null} ry - Y-axis corner radius (defaults to rx if null)
|
|
264
|
+
* @param {boolean} useArcs - Use arc commands instead of Bezier curves for corners
|
|
265
|
+
* @param {number} precision - Decimal precision for output coordinates
|
|
266
|
+
* @returns {string} SVG path data string
|
|
267
|
+
*/
|
|
268
|
+
export function rectToPathData(
|
|
269
|
+
x,
|
|
270
|
+
y,
|
|
271
|
+
width,
|
|
272
|
+
height,
|
|
273
|
+
rx = 0,
|
|
274
|
+
ry = null,
|
|
275
|
+
useArcs = false,
|
|
276
|
+
precision = 6,
|
|
277
|
+
) {
|
|
278
|
+
const xD = D(x),
|
|
279
|
+
yD = D(y),
|
|
280
|
+
wD = D(width),
|
|
281
|
+
hD = D(height);
|
|
282
|
+
let rxD = D(rx || 0),
|
|
283
|
+
ryD = ry !== null ? D(ry) : rxD;
|
|
284
|
+
const halfW = wD.div(2),
|
|
285
|
+
halfH = hD.div(2);
|
|
187
286
|
if (rxD.gt(halfW)) rxD = halfW;
|
|
188
287
|
if (ryD.gt(halfH)) ryD = halfH;
|
|
189
|
-
const f = v => formatNumber(v, precision);
|
|
288
|
+
const f = (v) => formatNumber(v, precision);
|
|
190
289
|
if (rxD.isZero() || ryD.isZero()) {
|
|
191
|
-
const x1 = xD.plus(wD),
|
|
290
|
+
const x1 = xD.plus(wD),
|
|
291
|
+
y1 = yD.plus(hD);
|
|
192
292
|
// Use H (horizontal) and V (vertical) commands for smaller output
|
|
193
293
|
return `M${f(xD)} ${f(yD)}H${f(x1)}V${f(y1)}H${f(xD)}Z`;
|
|
194
294
|
}
|
|
195
|
-
const left = xD,
|
|
196
|
-
|
|
197
|
-
|
|
295
|
+
const left = xD,
|
|
296
|
+
right = xD.plus(wD),
|
|
297
|
+
top = yD,
|
|
298
|
+
bottom = yD.plus(hD);
|
|
299
|
+
const leftInner = left.plus(rxD),
|
|
300
|
+
rightInner = right.minus(rxD);
|
|
301
|
+
const topInner = top.plus(ryD),
|
|
302
|
+
bottomInner = bottom.minus(ryD);
|
|
198
303
|
if (useArcs) {
|
|
199
304
|
return `M${f(leftInner)} ${f(top)}L${f(rightInner)} ${f(top)}A${f(rxD)} ${f(ryD)} 0 0 1 ${f(right)} ${f(topInner)}L${f(right)} ${f(bottomInner)}A${f(rxD)} ${f(ryD)} 0 0 1 ${f(rightInner)} ${f(bottom)}L${f(leftInner)} ${f(bottom)}A${f(rxD)} ${f(ryD)} 0 0 1 ${f(left)} ${f(bottomInner)}L${f(left)} ${f(topInner)}A${f(rxD)} ${f(ryD)} 0 0 1 ${f(leftInner)} ${f(top)}Z`;
|
|
200
305
|
}
|
|
201
|
-
const kappa = getKappa(),
|
|
306
|
+
const kappa = getKappa(),
|
|
307
|
+
kx = kappa.mul(rxD),
|
|
308
|
+
ky = kappa.mul(ryD);
|
|
202
309
|
// Each corner has two Bezier control points:
|
|
203
310
|
// First control point: offset from start point along the edge tangent
|
|
204
311
|
// Second control point: offset from end point along the edge tangent
|
|
205
312
|
return `M${f(leftInner)} ${f(top)}L${f(rightInner)} ${f(top)}C${f(rightInner.plus(kx))} ${f(top)} ${f(right)} ${f(topInner.minus(ky))} ${f(right)} ${f(topInner)}L${f(right)} ${f(bottomInner)}C${f(right)} ${f(bottomInner.plus(ky))} ${f(rightInner.plus(kx))} ${f(bottom)} ${f(rightInner)} ${f(bottom)}L${f(leftInner)} ${f(bottom)}C${f(leftInner.minus(kx))} ${f(bottom)} ${f(left)} ${f(bottomInner.plus(ky))} ${f(left)} ${f(bottomInner)}L${f(left)} ${f(topInner)}C${f(left)} ${f(topInner.minus(ky))} ${f(leftInner.minus(kx))} ${f(top)} ${f(leftInner)} ${f(top)}Z`;
|
|
206
313
|
}
|
|
207
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Convert a line segment to SVG path data.
|
|
317
|
+
* @param {number|Decimal} x1 - Start point X coordinate
|
|
318
|
+
* @param {number|Decimal} y1 - Start point Y coordinate
|
|
319
|
+
* @param {number|Decimal} x2 - End point X coordinate
|
|
320
|
+
* @param {number|Decimal} y2 - End point Y coordinate
|
|
321
|
+
* @param {number} precision - Decimal precision for output coordinates
|
|
322
|
+
* @returns {string} SVG path data string
|
|
323
|
+
*/
|
|
208
324
|
export function lineToPathData(x1, y1, x2, y2, precision = 6) {
|
|
209
|
-
const f = v => formatNumber(D(v), precision);
|
|
325
|
+
const f = (v) => formatNumber(D(v), precision);
|
|
210
326
|
return `M${f(x1)} ${f(y1)}L${f(x2)} ${f(y2)}`;
|
|
211
327
|
}
|
|
212
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Parse SVG points attribute string into array of Decimal coordinate pairs.
|
|
331
|
+
* @param {string|Array|Object} points - Points string, array, or SVGAnimatedPoints object
|
|
332
|
+
* @returns {Array<Array<Decimal>>} Array of [x, y] Decimal pairs
|
|
333
|
+
*/
|
|
213
334
|
function parsePoints(points) {
|
|
335
|
+
// Handle null/undefined
|
|
336
|
+
if (points == null) return [];
|
|
337
|
+
// Handle arrays
|
|
214
338
|
if (Array.isArray(points)) return points.map(([x, y]) => [D(x), D(y)]);
|
|
215
|
-
|
|
339
|
+
// Handle SVGAnimatedPoints or objects with baseVal
|
|
340
|
+
let pointsValue = points;
|
|
341
|
+
if (typeof pointsValue === "object" && pointsValue.baseVal !== undefined) {
|
|
342
|
+
pointsValue = pointsValue.baseVal;
|
|
343
|
+
}
|
|
344
|
+
// Convert to string if not already a string
|
|
345
|
+
if (typeof pointsValue !== "string") {
|
|
346
|
+
if (typeof pointsValue.toString === "function") {
|
|
347
|
+
pointsValue = pointsValue.toString();
|
|
348
|
+
} else {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const nums = pointsValue
|
|
353
|
+
.split(/[\s,]+/)
|
|
354
|
+
.filter((s) => s.length > 0)
|
|
355
|
+
.map((s) => D(s));
|
|
216
356
|
const pairs = [];
|
|
217
357
|
for (let i = 0; i < nums.length; i += 2) {
|
|
218
358
|
if (i + 1 < nums.length) pairs.push([nums[i], nums[i + 1]]);
|
|
@@ -220,10 +360,16 @@ function parsePoints(points) {
|
|
|
220
360
|
return pairs;
|
|
221
361
|
}
|
|
222
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Convert polyline points to SVG path data.
|
|
365
|
+
* @param {string|Array} points - Points string or array of coordinate pairs
|
|
366
|
+
* @param {number} precision - Decimal precision for output coordinates
|
|
367
|
+
* @returns {string} SVG path data string
|
|
368
|
+
*/
|
|
223
369
|
export function polylineToPathData(points, precision = 6) {
|
|
224
370
|
const pairs = parsePoints(points);
|
|
225
|
-
if (pairs.length === 0) return
|
|
226
|
-
const f = v => formatNumber(v, precision);
|
|
371
|
+
if (pairs.length === 0) return "";
|
|
372
|
+
const f = (v) => formatNumber(v, precision);
|
|
227
373
|
const [x0, y0] = pairs[0];
|
|
228
374
|
let path = `M${f(x0)} ${f(y0)}`;
|
|
229
375
|
for (let i = 1; i < pairs.length; i++) {
|
|
@@ -233,18 +379,46 @@ export function polylineToPathData(points, precision = 6) {
|
|
|
233
379
|
return path;
|
|
234
380
|
}
|
|
235
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Convert polygon points to SVG path data (closed path).
|
|
384
|
+
* @param {string|Array} points - Points string or array of coordinate pairs
|
|
385
|
+
* @param {number} precision - Decimal precision for output coordinates
|
|
386
|
+
* @returns {string} SVG path data string with Z (closepath) command
|
|
387
|
+
*/
|
|
236
388
|
export function polygonToPathData(points, precision = 6) {
|
|
237
389
|
const path = polylineToPathData(points, precision);
|
|
238
|
-
return path ? path +
|
|
390
|
+
return path ? path + " Z" : "";
|
|
239
391
|
}
|
|
240
392
|
|
|
241
393
|
// Parameter count for each SVG path command
|
|
242
394
|
const COMMAND_PARAMS = {
|
|
243
|
-
M: 2,
|
|
244
|
-
|
|
245
|
-
|
|
395
|
+
M: 2,
|
|
396
|
+
m: 2,
|
|
397
|
+
L: 2,
|
|
398
|
+
l: 2,
|
|
399
|
+
H: 1,
|
|
400
|
+
h: 1,
|
|
401
|
+
V: 1,
|
|
402
|
+
v: 1,
|
|
403
|
+
C: 6,
|
|
404
|
+
c: 6,
|
|
405
|
+
S: 4,
|
|
406
|
+
s: 4,
|
|
407
|
+
Q: 4,
|
|
408
|
+
q: 4,
|
|
409
|
+
T: 2,
|
|
410
|
+
t: 2,
|
|
411
|
+
A: 7,
|
|
412
|
+
a: 7,
|
|
413
|
+
Z: 0,
|
|
414
|
+
z: 0,
|
|
246
415
|
};
|
|
247
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Parse SVG path data string into command objects.
|
|
419
|
+
* @param {string} pathData - SVG path data string
|
|
420
|
+
* @returns {Array<Object>} Array of {command, args} objects
|
|
421
|
+
*/
|
|
248
422
|
export function parsePathData(pathData) {
|
|
249
423
|
const commands = [];
|
|
250
424
|
const commandRegex = /([MmLlHhVvCcSsQqTtAaZz])\s*([^MmLlHhVvCcSsQqTtAaZz]*)/g;
|
|
@@ -252,7 +426,14 @@ export function parsePathData(pathData) {
|
|
|
252
426
|
while ((match = commandRegex.exec(pathData)) !== null) {
|
|
253
427
|
const command = match[1];
|
|
254
428
|
const argsStr = match[2].trim();
|
|
255
|
-
|
|
429
|
+
|
|
430
|
+
// FIX: Use regex to extract numbers, handles implicit negative separators (e.g., "0.8-2.9" -> ["0.8", "-2.9"])
|
|
431
|
+
// Per W3C SVG spec, negative signs can act as delimiters without spaces
|
|
432
|
+
const numRegex = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
433
|
+
const allArgs =
|
|
434
|
+
argsStr.length > 0
|
|
435
|
+
? Array.from(argsStr.matchAll(numRegex), (m) => D(m[0]))
|
|
436
|
+
: [];
|
|
256
437
|
|
|
257
438
|
const paramCount = COMMAND_PARAMS[command];
|
|
258
439
|
|
|
@@ -267,8 +448,8 @@ export function parsePathData(pathData) {
|
|
|
267
448
|
if (args.length === paramCount) {
|
|
268
449
|
// For M/m, first group is moveto, subsequent groups become implicit lineto (L/l)
|
|
269
450
|
let effectiveCmd = command;
|
|
270
|
-
if (i > 0 && (command ===
|
|
271
|
-
effectiveCmd = command ===
|
|
451
|
+
if (i > 0 && (command === "M" || command === "m")) {
|
|
452
|
+
effectiveCmd = command === "M" ? "L" : "l";
|
|
272
453
|
}
|
|
273
454
|
commands.push({ command: effectiveCmd, args });
|
|
274
455
|
}
|
|
@@ -279,62 +460,84 @@ export function parsePathData(pathData) {
|
|
|
279
460
|
return commands;
|
|
280
461
|
}
|
|
281
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Convert path command array back to SVG path data string.
|
|
465
|
+
* @param {Array<Object>} commands - Array of {command, args} objects
|
|
466
|
+
* @param {number} precision - Decimal precision for output coordinates
|
|
467
|
+
* @returns {string} SVG path data string
|
|
468
|
+
*/
|
|
282
469
|
export function pathArrayToString(commands, precision = 6) {
|
|
283
|
-
return commands
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
470
|
+
return commands
|
|
471
|
+
.map(({ command, args }) => {
|
|
472
|
+
const argsStr = args.map((a) => formatNumber(a, precision)).join(" ");
|
|
473
|
+
return argsStr.length > 0 ? `${command} ${argsStr}` : command;
|
|
474
|
+
})
|
|
475
|
+
.join(" ");
|
|
287
476
|
}
|
|
288
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Convert all path commands to absolute coordinates.
|
|
480
|
+
* @param {string} pathData - SVG path data string
|
|
481
|
+
* @returns {string} Path data with all absolute commands
|
|
482
|
+
*/
|
|
289
483
|
export function pathToAbsolute(pathData) {
|
|
290
484
|
const commands = parsePathData(pathData);
|
|
291
485
|
const result = [];
|
|
292
|
-
let currentX = new Decimal(0),
|
|
293
|
-
|
|
294
|
-
let
|
|
295
|
-
|
|
486
|
+
let currentX = new Decimal(0),
|
|
487
|
+
currentY = new Decimal(0);
|
|
488
|
+
let subpathStartX = new Decimal(0),
|
|
489
|
+
subpathStartY = new Decimal(0);
|
|
490
|
+
let lastControlX = new Decimal(0),
|
|
491
|
+
lastControlY = new Decimal(0);
|
|
492
|
+
let lastCommand = "";
|
|
296
493
|
|
|
297
494
|
for (const { command, args } of commands) {
|
|
298
495
|
const isRelative = command === command.toLowerCase();
|
|
299
496
|
const upperCmd = command.toUpperCase();
|
|
300
|
-
if (upperCmd ===
|
|
497
|
+
if (upperCmd === "M") {
|
|
301
498
|
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
302
499
|
const y = isRelative ? currentY.plus(args[1]) : args[1];
|
|
303
|
-
currentX = x;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
500
|
+
currentX = x;
|
|
501
|
+
currentY = y;
|
|
502
|
+
subpathStartX = x;
|
|
503
|
+
subpathStartY = y;
|
|
504
|
+
result.push({ command: "M", args: [x, y] });
|
|
505
|
+
lastCommand = "M";
|
|
506
|
+
} else if (upperCmd === "L") {
|
|
307
507
|
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
308
508
|
const y = isRelative ? currentY.plus(args[1]) : args[1];
|
|
309
|
-
currentX = x;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
509
|
+
currentX = x;
|
|
510
|
+
currentY = y;
|
|
511
|
+
result.push({ command: "L", args: [x, y] });
|
|
512
|
+
lastCommand = "L";
|
|
513
|
+
} else if (upperCmd === "H") {
|
|
313
514
|
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
314
515
|
currentX = x;
|
|
315
|
-
result.push({ command:
|
|
316
|
-
lastCommand =
|
|
317
|
-
} else if (upperCmd ===
|
|
516
|
+
result.push({ command: "L", args: [x, currentY] });
|
|
517
|
+
lastCommand = "H";
|
|
518
|
+
} else if (upperCmd === "V") {
|
|
318
519
|
const y = isRelative ? currentY.plus(args[0]) : args[0];
|
|
319
520
|
currentY = y;
|
|
320
|
-
result.push({ command:
|
|
321
|
-
lastCommand =
|
|
322
|
-
} else if (upperCmd ===
|
|
521
|
+
result.push({ command: "L", args: [currentX, y] });
|
|
522
|
+
lastCommand = "V";
|
|
523
|
+
} else if (upperCmd === "C") {
|
|
323
524
|
const x1 = isRelative ? currentX.plus(args[0]) : args[0];
|
|
324
525
|
const y1 = isRelative ? currentY.plus(args[1]) : args[1];
|
|
325
526
|
const x2 = isRelative ? currentX.plus(args[2]) : args[2];
|
|
326
527
|
const y2 = isRelative ? currentY.plus(args[3]) : args[3];
|
|
327
528
|
const x = isRelative ? currentX.plus(args[4]) : args[4];
|
|
328
529
|
const y = isRelative ? currentY.plus(args[5]) : args[5];
|
|
329
|
-
lastControlX = x2;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
530
|
+
lastControlX = x2;
|
|
531
|
+
lastControlY = y2;
|
|
532
|
+
currentX = x;
|
|
533
|
+
currentY = y;
|
|
534
|
+
result.push({ command: "C", args: [x1, y1, x2, y2, x, y] });
|
|
535
|
+
lastCommand = "C";
|
|
536
|
+
} else if (upperCmd === "S") {
|
|
334
537
|
// Smooth cubic Bezier: 4 args (x2, y2, x, y)
|
|
335
538
|
// First control point is reflection of previous second control point
|
|
336
539
|
let x1, y1;
|
|
337
|
-
if (lastCommand ===
|
|
540
|
+
if (lastCommand === "C" || lastCommand === "S") {
|
|
338
541
|
x1 = currentX.mul(2).minus(lastControlX);
|
|
339
542
|
y1 = currentY.mul(2).minus(lastControlY);
|
|
340
543
|
} else {
|
|
@@ -345,25 +548,29 @@ export function pathToAbsolute(pathData) {
|
|
|
345
548
|
const y2 = isRelative ? currentY.plus(args[1]) : args[1];
|
|
346
549
|
const x = isRelative ? currentX.plus(args[2]) : args[2];
|
|
347
550
|
const y = isRelative ? currentY.plus(args[3]) : args[3];
|
|
348
|
-
lastControlX = x2;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
551
|
+
lastControlX = x2;
|
|
552
|
+
lastControlY = y2;
|
|
553
|
+
currentX = x;
|
|
554
|
+
currentY = y;
|
|
555
|
+
result.push({ command: "C", args: [x1, y1, x2, y2, x, y] });
|
|
556
|
+
lastCommand = "S";
|
|
557
|
+
} else if (upperCmd === "Q") {
|
|
353
558
|
// Quadratic Bezier: 4 args (x1, y1, x, y)
|
|
354
559
|
const x1 = isRelative ? currentX.plus(args[0]) : args[0];
|
|
355
560
|
const y1 = isRelative ? currentY.plus(args[1]) : args[1];
|
|
356
561
|
const x = isRelative ? currentX.plus(args[2]) : args[2];
|
|
357
562
|
const y = isRelative ? currentY.plus(args[3]) : args[3];
|
|
358
|
-
lastControlX = x1;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
563
|
+
lastControlX = x1;
|
|
564
|
+
lastControlY = y1;
|
|
565
|
+
currentX = x;
|
|
566
|
+
currentY = y;
|
|
567
|
+
result.push({ command: "Q", args: [x1, y1, x, y] });
|
|
568
|
+
lastCommand = "Q";
|
|
569
|
+
} else if (upperCmd === "T") {
|
|
363
570
|
// Smooth quadratic Bezier: 2 args (x, y)
|
|
364
571
|
// Control point is reflection of previous control point
|
|
365
572
|
let x1, y1;
|
|
366
|
-
if (lastCommand ===
|
|
573
|
+
if (lastCommand === "Q" || lastCommand === "T") {
|
|
367
574
|
x1 = currentX.mul(2).minus(lastControlX);
|
|
368
575
|
y1 = currentY.mul(2).minus(lastControlY);
|
|
369
576
|
} else {
|
|
@@ -372,20 +579,27 @@ export function pathToAbsolute(pathData) {
|
|
|
372
579
|
}
|
|
373
580
|
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
374
581
|
const y = isRelative ? currentY.plus(args[1]) : args[1];
|
|
375
|
-
lastControlX = x1;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
582
|
+
lastControlX = x1;
|
|
583
|
+
lastControlY = y1;
|
|
584
|
+
currentX = x;
|
|
585
|
+
currentY = y;
|
|
586
|
+
result.push({ command: "Q", args: [x1, y1, x, y] });
|
|
587
|
+
lastCommand = "T";
|
|
588
|
+
} else if (upperCmd === "A") {
|
|
380
589
|
const x = isRelative ? currentX.plus(args[5]) : args[5];
|
|
381
590
|
const y = isRelative ? currentY.plus(args[6]) : args[6];
|
|
382
|
-
currentX = x;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
lastCommand =
|
|
591
|
+
currentX = x;
|
|
592
|
+
currentY = y;
|
|
593
|
+
result.push({
|
|
594
|
+
command: "A",
|
|
595
|
+
args: [args[0], args[1], args[2], args[3], args[4], x, y],
|
|
596
|
+
});
|
|
597
|
+
lastCommand = "A";
|
|
598
|
+
} else if (upperCmd === "Z") {
|
|
599
|
+
currentX = subpathStartX;
|
|
600
|
+
currentY = subpathStartY;
|
|
601
|
+
result.push({ command: "Z", args: [] });
|
|
602
|
+
lastCommand = "Z";
|
|
389
603
|
} else {
|
|
390
604
|
result.push({ command, args });
|
|
391
605
|
lastCommand = command;
|
|
@@ -394,9 +608,33 @@ export function pathToAbsolute(pathData) {
|
|
|
394
608
|
return pathArrayToString(result);
|
|
395
609
|
}
|
|
396
610
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
611
|
+
/**
|
|
612
|
+
* Transform arc command parameters by an affine transformation matrix.
|
|
613
|
+
* @param {number|Decimal} rx - X radius
|
|
614
|
+
* @param {number|Decimal} ry - Y radius
|
|
615
|
+
* @param {number|Decimal} xAxisRotation - Rotation angle in degrees
|
|
616
|
+
* @param {number} largeArc - Large arc flag (0 or 1)
|
|
617
|
+
* @param {number} sweep - Sweep flag (0 or 1)
|
|
618
|
+
* @param {number|Decimal} endX - Arc end point X
|
|
619
|
+
* @param {number|Decimal} endY - Arc end point Y
|
|
620
|
+
* @param {Matrix} matrix - 3x3 transformation matrix
|
|
621
|
+
* @returns {Array} Transformed arc parameters [rx, ry, rotation, largeArc, sweep, endX, endY]
|
|
622
|
+
*/
|
|
623
|
+
export function transformArcParams(
|
|
624
|
+
rx,
|
|
625
|
+
ry,
|
|
626
|
+
xAxisRotation,
|
|
627
|
+
largeArc,
|
|
628
|
+
sweep,
|
|
629
|
+
endX,
|
|
630
|
+
endY,
|
|
631
|
+
matrix,
|
|
632
|
+
) {
|
|
633
|
+
const rxD = D(rx),
|
|
634
|
+
ryD = D(ry),
|
|
635
|
+
rotD = D(xAxisRotation);
|
|
636
|
+
const endXD = D(endX),
|
|
637
|
+
endYD = D(endY);
|
|
400
638
|
|
|
401
639
|
// Transform the endpoint
|
|
402
640
|
const endPoint = Matrix.from([[endXD], [endYD], [new Decimal(1)]]);
|
|
@@ -405,8 +643,10 @@ export function transformArcParams(rx, ry, xAxisRotation, largeArc, sweep, endX,
|
|
|
405
643
|
const newEndY = transformedEnd.data[1][0].div(transformedEnd.data[2][0]);
|
|
406
644
|
|
|
407
645
|
// Extract the 2x2 linear part of the affine transformation
|
|
408
|
-
const a = matrix.data[0][0],
|
|
409
|
-
|
|
646
|
+
const a = matrix.data[0][0],
|
|
647
|
+
b = matrix.data[0][1];
|
|
648
|
+
const c = matrix.data[1][0],
|
|
649
|
+
d = matrix.data[1][1];
|
|
410
650
|
|
|
411
651
|
// Calculate determinant to check for reflection (flips sweep direction)
|
|
412
652
|
const det = a.mul(d).minus(b.mul(c));
|
|
@@ -416,7 +656,8 @@ export function transformArcParams(rx, ry, xAxisRotation, largeArc, sweep, endX,
|
|
|
416
656
|
// For an ellipse with rotation, we need to compute the transformed ellipse parameters
|
|
417
657
|
// Convert rotation to radians
|
|
418
658
|
const rotRad = rotD.mul(new Decimal(Math.PI)).div(180);
|
|
419
|
-
const cosRot = Decimal.cos(rotRad),
|
|
659
|
+
const cosRot = Decimal.cos(rotRad),
|
|
660
|
+
sinRot = Decimal.sin(rotRad);
|
|
420
661
|
|
|
421
662
|
// Unit ellipse basis vectors (before xAxisRotation)
|
|
422
663
|
// The ellipse can be parameterized as: [rx*cos(t)*cos(rot) - ry*sin(t)*sin(rot), rx*cos(t)*sin(rot) + ry*sin(t)*cos(rot)]
|
|
@@ -448,30 +689,53 @@ export function transformArcParams(rx, ry, xAxisRotation, largeArc, sweep, endX,
|
|
|
448
689
|
return [newRx, newRy, newRot, largeArc, newSweep, newEndX, newEndY];
|
|
449
690
|
}
|
|
450
691
|
|
|
692
|
+
/**
|
|
693
|
+
* Transform path data by an affine transformation matrix.
|
|
694
|
+
* @param {string} pathData - SVG path data string
|
|
695
|
+
* @param {Matrix} matrix - 3x3 transformation matrix
|
|
696
|
+
* @param {number} precision - Decimal precision for output coordinates
|
|
697
|
+
* @returns {string} Transformed SVG path data
|
|
698
|
+
*/
|
|
451
699
|
export function transformPathData(pathData, matrix, precision = 6) {
|
|
452
700
|
const absPath = pathToAbsolute(pathData);
|
|
453
701
|
const commands = parsePathData(absPath);
|
|
454
702
|
const result = [];
|
|
455
703
|
for (const { command, args } of commands) {
|
|
456
|
-
if (command ===
|
|
704
|
+
if (command === "M" || command === "L") {
|
|
457
705
|
const pt = Matrix.from([[args[0]], [args[1]], [new Decimal(1)]]);
|
|
458
706
|
const transformed = matrix.mul(pt);
|
|
459
707
|
const x = transformed.data[0][0].div(transformed.data[2][0]);
|
|
460
708
|
const y = transformed.data[1][0].div(transformed.data[2][0]);
|
|
461
709
|
result.push({ command, args: [x, y] });
|
|
462
|
-
} else if (command ===
|
|
710
|
+
} else if (command === "C") {
|
|
463
711
|
const transformedArgs = [];
|
|
464
712
|
for (let i = 0; i < 6; i += 2) {
|
|
465
713
|
const pt = Matrix.from([[args[i]], [args[i + 1]], [new Decimal(1)]]);
|
|
466
714
|
const transformed = matrix.mul(pt);
|
|
467
|
-
transformedArgs.push(
|
|
468
|
-
|
|
715
|
+
transformedArgs.push(
|
|
716
|
+
transformed.data[0][0].div(transformed.data[2][0]),
|
|
717
|
+
);
|
|
718
|
+
transformedArgs.push(
|
|
719
|
+
transformed.data[1][0].div(transformed.data[2][0]),
|
|
720
|
+
);
|
|
469
721
|
}
|
|
470
722
|
result.push({ command, args: transformedArgs });
|
|
471
|
-
} else if (command ===
|
|
723
|
+
} else if (command === "A") {
|
|
472
724
|
const [newRx, newRy, newRot, newLarge, newSweep, newEndX, newEndY] =
|
|
473
|
-
transformArcParams(
|
|
474
|
-
|
|
725
|
+
transformArcParams(
|
|
726
|
+
args[0],
|
|
727
|
+
args[1],
|
|
728
|
+
args[2],
|
|
729
|
+
args[3],
|
|
730
|
+
args[4],
|
|
731
|
+
args[5],
|
|
732
|
+
args[6],
|
|
733
|
+
matrix,
|
|
734
|
+
);
|
|
735
|
+
result.push({
|
|
736
|
+
command,
|
|
737
|
+
args: [newRx, newRy, newRot, newLarge, newSweep, newEndX, newEndY],
|
|
738
|
+
});
|
|
475
739
|
} else {
|
|
476
740
|
result.push({ command, args });
|
|
477
741
|
}
|
|
@@ -479,6 +743,16 @@ export function transformPathData(pathData, matrix, precision = 6) {
|
|
|
479
743
|
return pathArrayToString(result, precision);
|
|
480
744
|
}
|
|
481
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Convert quadratic Bezier to cubic Bezier control points.
|
|
748
|
+
* @param {Decimal} x0 - Start point X
|
|
749
|
+
* @param {Decimal} y0 - Start point Y
|
|
750
|
+
* @param {Decimal} x1 - Control point X
|
|
751
|
+
* @param {Decimal} y1 - Control point Y
|
|
752
|
+
* @param {Decimal} x2 - End point X
|
|
753
|
+
* @param {Decimal} y2 - End point Y
|
|
754
|
+
* @returns {Array<Decimal>} Cubic Bezier control points [cp1x, cp1y, cp2x, cp2y, x2, y2]
|
|
755
|
+
*/
|
|
482
756
|
function quadraticToCubic(x0, y0, x1, y1, x2, y2) {
|
|
483
757
|
const twoThirds = new Decimal(2).div(3);
|
|
484
758
|
const cp1x = x0.plus(twoThirds.mul(x1.minus(x0)));
|
|
@@ -488,66 +762,86 @@ function quadraticToCubic(x0, y0, x1, y1, x2, y2) {
|
|
|
488
762
|
return [cp1x, cp1y, cp2x, cp2y, x2, y2];
|
|
489
763
|
}
|
|
490
764
|
|
|
765
|
+
/**
|
|
766
|
+
* Convert all path commands to cubic Bezier curves.
|
|
767
|
+
* @param {string} pathData - SVG path data string
|
|
768
|
+
* @returns {string} Path data with only M, C, and Z commands
|
|
769
|
+
*/
|
|
491
770
|
export function pathToCubics(pathData) {
|
|
492
771
|
const absPath = pathToAbsolute(pathData);
|
|
493
772
|
const commands = parsePathData(absPath);
|
|
494
773
|
const result = [];
|
|
495
|
-
let currentX = new Decimal(0),
|
|
496
|
-
|
|
497
|
-
let
|
|
774
|
+
let currentX = new Decimal(0),
|
|
775
|
+
currentY = new Decimal(0);
|
|
776
|
+
let lastControlX = new Decimal(0),
|
|
777
|
+
lastControlY = new Decimal(0);
|
|
778
|
+
let lastCommand = "";
|
|
498
779
|
for (const { command, args } of commands) {
|
|
499
|
-
if (command ===
|
|
500
|
-
currentX = args[0];
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
780
|
+
if (command === "M") {
|
|
781
|
+
currentX = args[0];
|
|
782
|
+
currentY = args[1];
|
|
783
|
+
result.push({ command: "M", args: [currentX, currentY] });
|
|
784
|
+
lastCommand = "M";
|
|
785
|
+
} else if (command === "L") {
|
|
786
|
+
const x = args[0],
|
|
787
|
+
y = args[1];
|
|
788
|
+
result.push({ command: "C", args: [currentX, currentY, x, y, x, y] });
|
|
789
|
+
currentX = x;
|
|
790
|
+
currentY = y;
|
|
791
|
+
lastCommand = "L";
|
|
792
|
+
} else if (command === "C") {
|
|
509
793
|
const [x1, y1, x2, y2, x, y] = args;
|
|
510
|
-
result.push({ command:
|
|
511
|
-
lastControlX = x2;
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
794
|
+
result.push({ command: "C", args: [x1, y1, x2, y2, x, y] });
|
|
795
|
+
lastControlX = x2;
|
|
796
|
+
lastControlY = y2;
|
|
797
|
+
currentX = x;
|
|
798
|
+
currentY = y;
|
|
799
|
+
lastCommand = "C";
|
|
800
|
+
} else if (command === "S") {
|
|
515
801
|
let x1, y1;
|
|
516
|
-
if (lastCommand ===
|
|
802
|
+
if (lastCommand === "C" || lastCommand === "S") {
|
|
517
803
|
x1 = currentX.mul(2).minus(lastControlX);
|
|
518
804
|
y1 = currentY.mul(2).minus(lastControlY);
|
|
519
805
|
} else {
|
|
520
|
-
x1 = currentX;
|
|
806
|
+
x1 = currentX;
|
|
807
|
+
y1 = currentY;
|
|
521
808
|
}
|
|
522
809
|
const [x2, y2, x, y] = args;
|
|
523
|
-
result.push({ command:
|
|
524
|
-
lastControlX = x2;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
810
|
+
result.push({ command: "C", args: [x1, y1, x2, y2, x, y] });
|
|
811
|
+
lastControlX = x2;
|
|
812
|
+
lastControlY = y2;
|
|
813
|
+
currentX = x;
|
|
814
|
+
currentY = y;
|
|
815
|
+
lastCommand = "S";
|
|
816
|
+
} else if (command === "Q") {
|
|
528
817
|
const [x1, y1, x, y] = args;
|
|
529
818
|
const cubic = quadraticToCubic(currentX, currentY, x1, y1, x, y);
|
|
530
|
-
result.push({ command:
|
|
531
|
-
lastControlX = x1;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
819
|
+
result.push({ command: "C", args: cubic });
|
|
820
|
+
lastControlX = x1;
|
|
821
|
+
lastControlY = y1;
|
|
822
|
+
currentX = x;
|
|
823
|
+
currentY = y;
|
|
824
|
+
lastCommand = "Q";
|
|
825
|
+
} else if (command === "T") {
|
|
535
826
|
let x1, y1;
|
|
536
|
-
if (lastCommand ===
|
|
827
|
+
if (lastCommand === "Q" || lastCommand === "T") {
|
|
537
828
|
x1 = currentX.mul(2).minus(lastControlX);
|
|
538
829
|
y1 = currentY.mul(2).minus(lastControlY);
|
|
539
830
|
} else {
|
|
540
|
-
x1 = currentX;
|
|
831
|
+
x1 = currentX;
|
|
832
|
+
y1 = currentY;
|
|
541
833
|
}
|
|
542
834
|
const [x, y] = args;
|
|
543
835
|
const cubic = quadraticToCubic(currentX, currentY, x1, y1, x, y);
|
|
544
|
-
result.push({ command:
|
|
545
|
-
lastControlX = x1;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
836
|
+
result.push({ command: "C", args: cubic });
|
|
837
|
+
lastControlX = x1;
|
|
838
|
+
lastControlY = y1;
|
|
839
|
+
currentX = x;
|
|
840
|
+
currentY = y;
|
|
841
|
+
lastCommand = "T";
|
|
842
|
+
} else if (command === "Z") {
|
|
843
|
+
result.push({ command: "Z", args: [] });
|
|
844
|
+
lastCommand = "Z";
|
|
551
845
|
} else {
|
|
552
846
|
result.push({ command, args });
|
|
553
847
|
lastCommand = command;
|
|
@@ -556,6 +850,65 @@ export function pathToCubics(pathData) {
|
|
|
556
850
|
return pathArrayToString(result);
|
|
557
851
|
}
|
|
558
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Convert SVG shape element to path element with equivalent path data.
|
|
855
|
+
* @param {Object} element - SVG shape element (circle, ellipse, rect, line, polyline, polygon)
|
|
856
|
+
* @param {number} precision - Decimal precision for output coordinates
|
|
857
|
+
* @returns {string|null} SVG path data string, or null if element type not supported
|
|
858
|
+
*/
|
|
859
|
+
export function convertElementToPath(element, precision = 6) {
|
|
860
|
+
const getAttr = (name, defaultValue = 0) => {
|
|
861
|
+
const rawValue = element.getAttribute
|
|
862
|
+
? element.getAttribute(name)
|
|
863
|
+
: element[name];
|
|
864
|
+
const value =
|
|
865
|
+
rawValue !== undefined && rawValue !== null ? rawValue : defaultValue;
|
|
866
|
+
// Strip CSS units before returning (handles px, em, %, etc.)
|
|
867
|
+
return stripUnits(value);
|
|
868
|
+
};
|
|
869
|
+
const tagName = (element.tagName || element.type || "").toLowerCase();
|
|
870
|
+
if (tagName === "circle") {
|
|
871
|
+
return circleToPathData(
|
|
872
|
+
getAttr("cx", 0),
|
|
873
|
+
getAttr("cy", 0),
|
|
874
|
+
getAttr("r", 0),
|
|
875
|
+
precision,
|
|
876
|
+
);
|
|
877
|
+
} else if (tagName === "ellipse") {
|
|
878
|
+
return ellipseToPathData(
|
|
879
|
+
getAttr("cx", 0),
|
|
880
|
+
getAttr("cy", 0),
|
|
881
|
+
getAttr("rx", 0),
|
|
882
|
+
getAttr("ry", 0),
|
|
883
|
+
precision,
|
|
884
|
+
);
|
|
885
|
+
} else if (tagName === "rect") {
|
|
886
|
+
return rectToPathData(
|
|
887
|
+
getAttr("x", 0),
|
|
888
|
+
getAttr("y", 0),
|
|
889
|
+
getAttr("width", 0),
|
|
890
|
+
getAttr("height", 0),
|
|
891
|
+
getAttr("rx", 0),
|
|
892
|
+
getAttr("ry", null),
|
|
893
|
+
false,
|
|
894
|
+
precision,
|
|
895
|
+
);
|
|
896
|
+
} else if (tagName === "line") {
|
|
897
|
+
return lineToPathData(
|
|
898
|
+
getAttr("x1", 0),
|
|
899
|
+
getAttr("y1", 0),
|
|
900
|
+
getAttr("x2", 0),
|
|
901
|
+
getAttr("y2", 0),
|
|
902
|
+
precision,
|
|
903
|
+
);
|
|
904
|
+
} else if (tagName === "polyline") {
|
|
905
|
+
return polylineToPathData(getAttr("points", ""), precision);
|
|
906
|
+
} else if (tagName === "polygon") {
|
|
907
|
+
return polygonToPathData(getAttr("points", ""), precision);
|
|
908
|
+
}
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
|
|
559
912
|
/**
|
|
560
913
|
* Strip CSS units from a value string (e.g., "100px" -> 100, "50%" -> 50, "2em" -> 2)
|
|
561
914
|
* Returns the numeric value or 0 if parsing fails.
|
|
@@ -563,32 +916,8 @@ export function pathToCubics(pathData) {
|
|
|
563
916
|
* @returns {number} Numeric value without units
|
|
564
917
|
*/
|
|
565
918
|
function stripUnits(val) {
|
|
566
|
-
if (typeof val ===
|
|
919
|
+
if (typeof val === "string") {
|
|
567
920
|
return parseFloat(val) || 0;
|
|
568
921
|
}
|
|
569
922
|
return val;
|
|
570
923
|
}
|
|
571
|
-
|
|
572
|
-
export function convertElementToPath(element, precision = 6) {
|
|
573
|
-
const getAttr = (name, defaultValue = 0) => {
|
|
574
|
-
const rawValue = element.getAttribute ? element.getAttribute(name) : element[name];
|
|
575
|
-
const value = rawValue !== undefined && rawValue !== null ? rawValue : defaultValue;
|
|
576
|
-
// Strip CSS units before returning (handles px, em, %, etc.)
|
|
577
|
-
return stripUnits(value);
|
|
578
|
-
};
|
|
579
|
-
const tagName = (element.tagName || element.type || '').toLowerCase();
|
|
580
|
-
if (tagName === 'circle') {
|
|
581
|
-
return circleToPathData(getAttr('cx', 0), getAttr('cy', 0), getAttr('r', 0), precision);
|
|
582
|
-
} else if (tagName === 'ellipse') {
|
|
583
|
-
return ellipseToPathData(getAttr('cx', 0), getAttr('cy', 0), getAttr('rx', 0), getAttr('ry', 0), precision);
|
|
584
|
-
} else if (tagName === 'rect') {
|
|
585
|
-
return rectToPathData(getAttr('x', 0), getAttr('y', 0), getAttr('width', 0), getAttr('height', 0), getAttr('rx', 0), getAttr('ry', null), false, precision);
|
|
586
|
-
} else if (tagName === 'line') {
|
|
587
|
-
return lineToPathData(getAttr('x1', 0), getAttr('y1', 0), getAttr('x2', 0), getAttr('y2', 0), precision);
|
|
588
|
-
} else if (tagName === 'polyline') {
|
|
589
|
-
return polylineToPathData(getAttr('points', ''), precision);
|
|
590
|
-
} else if (tagName === 'polygon') {
|
|
591
|
-
return polygonToPathData(getAttr('points', ''), precision);
|
|
592
|
-
}
|
|
593
|
-
return null;
|
|
594
|
-
}
|