@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/path-data-plugins.js
CHANGED
|
@@ -33,7 +33,13 @@
|
|
|
33
33
|
* @module path-data-plugins
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
parsePath,
|
|
38
|
+
serializePath,
|
|
39
|
+
toAbsolute,
|
|
40
|
+
toRelative,
|
|
41
|
+
formatNumber,
|
|
42
|
+
} from "./convert-path-data.js";
|
|
37
43
|
|
|
38
44
|
// ============================================================================
|
|
39
45
|
// PLUGIN: removeLeadingZero
|
|
@@ -52,13 +58,13 @@ export function removeLeadingZero(d, precision = 3) {
|
|
|
52
58
|
if (commands.length === 0) return d;
|
|
53
59
|
|
|
54
60
|
// Format numbers with leading zero removal
|
|
55
|
-
const formatted = commands.map(cmd => ({
|
|
61
|
+
const formatted = commands.map((cmd) => ({
|
|
56
62
|
command: cmd.command,
|
|
57
|
-
args: cmd.args.map(n => {
|
|
58
|
-
|
|
63
|
+
args: cmd.args.map((n) => {
|
|
64
|
+
const str = formatNumber(n, precision);
|
|
59
65
|
// formatNumber already handles leading zero removal
|
|
60
66
|
return parseFloat(str);
|
|
61
|
-
})
|
|
67
|
+
}),
|
|
62
68
|
}));
|
|
63
69
|
|
|
64
70
|
return serializePath(formatted, precision);
|
|
@@ -99,8 +105,10 @@ export function convertToRelative(d, precision = 3) {
|
|
|
99
105
|
const commands = parsePath(d);
|
|
100
106
|
if (commands.length === 0) return d;
|
|
101
107
|
|
|
102
|
-
let cx = 0,
|
|
103
|
-
|
|
108
|
+
let cx = 0,
|
|
109
|
+
cy = 0;
|
|
110
|
+
let startX = 0,
|
|
111
|
+
startY = 0;
|
|
104
112
|
const result = [];
|
|
105
113
|
|
|
106
114
|
for (const cmd of commands) {
|
|
@@ -111,14 +119,40 @@ export function convertToRelative(d, precision = 3) {
|
|
|
111
119
|
// Update position using absolute form
|
|
112
120
|
const abs = toAbsolute(cmd, cx, cy);
|
|
113
121
|
switch (abs.command) {
|
|
114
|
-
case
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
case
|
|
121
|
-
case
|
|
122
|
+
case "M":
|
|
123
|
+
cx = abs.args[0];
|
|
124
|
+
cy = abs.args[1];
|
|
125
|
+
startX = cx;
|
|
126
|
+
startY = cy;
|
|
127
|
+
break;
|
|
128
|
+
case "L":
|
|
129
|
+
case "T":
|
|
130
|
+
cx = abs.args[0];
|
|
131
|
+
cy = abs.args[1];
|
|
132
|
+
break;
|
|
133
|
+
case "H":
|
|
134
|
+
cx = abs.args[0];
|
|
135
|
+
break;
|
|
136
|
+
case "V":
|
|
137
|
+
cy = abs.args[0];
|
|
138
|
+
break;
|
|
139
|
+
case "C":
|
|
140
|
+
cx = abs.args[4];
|
|
141
|
+
cy = abs.args[5];
|
|
142
|
+
break;
|
|
143
|
+
case "S":
|
|
144
|
+
case "Q":
|
|
145
|
+
cx = abs.args[2];
|
|
146
|
+
cy = abs.args[3];
|
|
147
|
+
break;
|
|
148
|
+
case "A":
|
|
149
|
+
cx = abs.args[5];
|
|
150
|
+
cy = abs.args[6];
|
|
151
|
+
break;
|
|
152
|
+
case "Z":
|
|
153
|
+
cx = startX;
|
|
154
|
+
cy = startY;
|
|
155
|
+
break;
|
|
122
156
|
}
|
|
123
157
|
}
|
|
124
158
|
|
|
@@ -140,8 +174,10 @@ export function convertToAbsolute(d, precision = 3) {
|
|
|
140
174
|
const commands = parsePath(d);
|
|
141
175
|
if (commands.length === 0) return d;
|
|
142
176
|
|
|
143
|
-
let cx = 0,
|
|
144
|
-
|
|
177
|
+
let cx = 0,
|
|
178
|
+
cy = 0;
|
|
179
|
+
let startX = 0,
|
|
180
|
+
startY = 0;
|
|
145
181
|
const result = [];
|
|
146
182
|
|
|
147
183
|
for (const cmd of commands) {
|
|
@@ -151,14 +187,40 @@ export function convertToAbsolute(d, precision = 3) {
|
|
|
151
187
|
|
|
152
188
|
// Update position
|
|
153
189
|
switch (abs.command) {
|
|
154
|
-
case
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
case
|
|
161
|
-
case
|
|
190
|
+
case "M":
|
|
191
|
+
cx = abs.args[0];
|
|
192
|
+
cy = abs.args[1];
|
|
193
|
+
startX = cx;
|
|
194
|
+
startY = cy;
|
|
195
|
+
break;
|
|
196
|
+
case "L":
|
|
197
|
+
case "T":
|
|
198
|
+
cx = abs.args[0];
|
|
199
|
+
cy = abs.args[1];
|
|
200
|
+
break;
|
|
201
|
+
case "H":
|
|
202
|
+
cx = abs.args[0];
|
|
203
|
+
break;
|
|
204
|
+
case "V":
|
|
205
|
+
cy = abs.args[0];
|
|
206
|
+
break;
|
|
207
|
+
case "C":
|
|
208
|
+
cx = abs.args[4];
|
|
209
|
+
cy = abs.args[5];
|
|
210
|
+
break;
|
|
211
|
+
case "S":
|
|
212
|
+
case "Q":
|
|
213
|
+
cx = abs.args[2];
|
|
214
|
+
cy = abs.args[3];
|
|
215
|
+
break;
|
|
216
|
+
case "A":
|
|
217
|
+
cx = abs.args[5];
|
|
218
|
+
cy = abs.args[6];
|
|
219
|
+
break;
|
|
220
|
+
case "Z":
|
|
221
|
+
cx = startX;
|
|
222
|
+
cy = startY;
|
|
223
|
+
break;
|
|
162
224
|
}
|
|
163
225
|
}
|
|
164
226
|
|
|
@@ -182,28 +244,30 @@ export function lineShorthands(d, tolerance = 1e-6, precision = 3) {
|
|
|
182
244
|
const commands = parsePath(d);
|
|
183
245
|
if (commands.length === 0) return d;
|
|
184
246
|
|
|
185
|
-
let cx = 0,
|
|
186
|
-
|
|
247
|
+
let cx = 0,
|
|
248
|
+
cy = 0;
|
|
249
|
+
let startX = 0,
|
|
250
|
+
startY = 0;
|
|
187
251
|
const result = [];
|
|
188
252
|
|
|
189
253
|
for (const cmd of commands) {
|
|
190
254
|
let newCmd = cmd;
|
|
191
255
|
|
|
192
|
-
if (cmd.command ===
|
|
193
|
-
const isAbs = cmd.command ===
|
|
256
|
+
if (cmd.command === "L" || cmd.command === "l") {
|
|
257
|
+
const isAbs = cmd.command === "L";
|
|
194
258
|
const endX = isAbs ? cmd.args[0] : cx + cmd.args[0];
|
|
195
259
|
const endY = isAbs ? cmd.args[1] : cy + cmd.args[1];
|
|
196
260
|
|
|
197
261
|
if (Math.abs(endY - cy) < tolerance) {
|
|
198
262
|
// Horizontal line
|
|
199
263
|
newCmd = isAbs
|
|
200
|
-
? { command:
|
|
201
|
-
: { command:
|
|
264
|
+
? { command: "H", args: [endX] }
|
|
265
|
+
: { command: "h", args: [endX - cx] };
|
|
202
266
|
} else if (Math.abs(endX - cx) < tolerance) {
|
|
203
267
|
// Vertical line
|
|
204
268
|
newCmd = isAbs
|
|
205
|
-
? { command:
|
|
206
|
-
: { command:
|
|
269
|
+
? { command: "V", args: [endY] }
|
|
270
|
+
: { command: "v", args: [endY - cy] };
|
|
207
271
|
}
|
|
208
272
|
}
|
|
209
273
|
|
|
@@ -212,14 +276,40 @@ export function lineShorthands(d, tolerance = 1e-6, precision = 3) {
|
|
|
212
276
|
// Update position
|
|
213
277
|
const abs = toAbsolute(newCmd, cx, cy);
|
|
214
278
|
switch (abs.command) {
|
|
215
|
-
case
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
case
|
|
222
|
-
case
|
|
279
|
+
case "M":
|
|
280
|
+
cx = abs.args[0];
|
|
281
|
+
cy = abs.args[1];
|
|
282
|
+
startX = cx;
|
|
283
|
+
startY = cy;
|
|
284
|
+
break;
|
|
285
|
+
case "L":
|
|
286
|
+
case "T":
|
|
287
|
+
cx = abs.args[0];
|
|
288
|
+
cy = abs.args[1];
|
|
289
|
+
break;
|
|
290
|
+
case "H":
|
|
291
|
+
cx = abs.args[0];
|
|
292
|
+
break;
|
|
293
|
+
case "V":
|
|
294
|
+
cy = abs.args[0];
|
|
295
|
+
break;
|
|
296
|
+
case "C":
|
|
297
|
+
cx = abs.args[4];
|
|
298
|
+
cy = abs.args[5];
|
|
299
|
+
break;
|
|
300
|
+
case "S":
|
|
301
|
+
case "Q":
|
|
302
|
+
cx = abs.args[2];
|
|
303
|
+
cy = abs.args[3];
|
|
304
|
+
break;
|
|
305
|
+
case "A":
|
|
306
|
+
cx = abs.args[5];
|
|
307
|
+
cy = abs.args[6];
|
|
308
|
+
break;
|
|
309
|
+
case "Z":
|
|
310
|
+
cx = startX;
|
|
311
|
+
cy = startY;
|
|
312
|
+
break;
|
|
223
313
|
}
|
|
224
314
|
}
|
|
225
315
|
|
|
@@ -243,8 +333,10 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
|
|
|
243
333
|
const commands = parsePath(d);
|
|
244
334
|
if (commands.length === 0) return d;
|
|
245
335
|
|
|
246
|
-
let cx = 0,
|
|
247
|
-
|
|
336
|
+
let cx = 0,
|
|
337
|
+
cy = 0;
|
|
338
|
+
let startX = 0,
|
|
339
|
+
startY = 0;
|
|
248
340
|
const result = [];
|
|
249
341
|
|
|
250
342
|
for (let i = 0; i < commands.length; i++) {
|
|
@@ -252,14 +344,17 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
|
|
|
252
344
|
let newCmd = cmd;
|
|
253
345
|
|
|
254
346
|
// Check if this L command goes back to start
|
|
255
|
-
if (cmd.command ===
|
|
256
|
-
const isAbs = cmd.command ===
|
|
347
|
+
if (cmd.command === "L" || cmd.command === "l") {
|
|
348
|
+
const isAbs = cmd.command === "L";
|
|
257
349
|
const endX = isAbs ? cmd.args[0] : cx + cmd.args[0];
|
|
258
350
|
const endY = isAbs ? cmd.args[1] : cy + cmd.args[1];
|
|
259
351
|
|
|
260
|
-
if (
|
|
352
|
+
if (
|
|
353
|
+
Math.abs(endX - startX) < tolerance &&
|
|
354
|
+
Math.abs(endY - startY) < tolerance
|
|
355
|
+
) {
|
|
261
356
|
// This line closes the path
|
|
262
|
-
newCmd = { command:
|
|
357
|
+
newCmd = { command: "z", args: [] };
|
|
263
358
|
}
|
|
264
359
|
}
|
|
265
360
|
|
|
@@ -268,14 +363,40 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
|
|
|
268
363
|
// Update position
|
|
269
364
|
const abs = toAbsolute(newCmd, cx, cy);
|
|
270
365
|
switch (abs.command) {
|
|
271
|
-
case
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
case
|
|
278
|
-
case
|
|
366
|
+
case "M":
|
|
367
|
+
cx = abs.args[0];
|
|
368
|
+
cy = abs.args[1];
|
|
369
|
+
startX = cx;
|
|
370
|
+
startY = cy;
|
|
371
|
+
break;
|
|
372
|
+
case "L":
|
|
373
|
+
case "T":
|
|
374
|
+
cx = abs.args[0];
|
|
375
|
+
cy = abs.args[1];
|
|
376
|
+
break;
|
|
377
|
+
case "H":
|
|
378
|
+
cx = abs.args[0];
|
|
379
|
+
break;
|
|
380
|
+
case "V":
|
|
381
|
+
cy = abs.args[0];
|
|
382
|
+
break;
|
|
383
|
+
case "C":
|
|
384
|
+
cx = abs.args[4];
|
|
385
|
+
cy = abs.args[5];
|
|
386
|
+
break;
|
|
387
|
+
case "S":
|
|
388
|
+
case "Q":
|
|
389
|
+
cx = abs.args[2];
|
|
390
|
+
cy = abs.args[3];
|
|
391
|
+
break;
|
|
392
|
+
case "A":
|
|
393
|
+
cx = abs.args[5];
|
|
394
|
+
cy = abs.args[6];
|
|
395
|
+
break;
|
|
396
|
+
case "Z":
|
|
397
|
+
cx = startX;
|
|
398
|
+
cy = startY;
|
|
399
|
+
break;
|
|
279
400
|
}
|
|
280
401
|
}
|
|
281
402
|
|
|
@@ -289,6 +410,16 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
|
|
|
289
410
|
/**
|
|
290
411
|
* Evaluate a cubic Bezier curve at parameter t.
|
|
291
412
|
* B(t) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3
|
|
413
|
+
* @param {number} t - Parameter value (0 to 1)
|
|
414
|
+
* @param {number} p0x - Start point X coordinate
|
|
415
|
+
* @param {number} p0y - Start point Y coordinate
|
|
416
|
+
* @param {number} p1x - First control point X coordinate
|
|
417
|
+
* @param {number} p1y - First control point Y coordinate
|
|
418
|
+
* @param {number} p2x - Second control point X coordinate
|
|
419
|
+
* @param {number} p2y - Second control point Y coordinate
|
|
420
|
+
* @param {number} p3x - End point X coordinate
|
|
421
|
+
* @param {number} p3y - End point Y coordinate
|
|
422
|
+
* @returns {{x: number, y: number}} Point on curve
|
|
292
423
|
*/
|
|
293
424
|
function cubicBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
|
|
294
425
|
const mt = 1 - t;
|
|
@@ -298,13 +429,23 @@ function cubicBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
|
|
|
298
429
|
const t3 = t2 * t;
|
|
299
430
|
return {
|
|
300
431
|
x: mt3 * p0x + 3 * mt2 * t * p1x + 3 * mt * t2 * p2x + t3 * p3x,
|
|
301
|
-
y: mt3 * p0y + 3 * mt2 * t * p1y + 3 * mt * t2 * p2y + t3 * p3y
|
|
432
|
+
y: mt3 * p0y + 3 * mt2 * t * p1y + 3 * mt * t2 * p2y + t3 * p3y,
|
|
302
433
|
};
|
|
303
434
|
}
|
|
304
435
|
|
|
305
436
|
/**
|
|
306
437
|
* First derivative of cubic Bezier at t.
|
|
307
438
|
* B'(t) = 3(1-t)^2(P1-P0) + 6(1-t)t(P2-P1) + 3t^2(P3-P2)
|
|
439
|
+
* @param {number} t - Parameter value (0 to 1)
|
|
440
|
+
* @param {number} p0x - Start point X coordinate
|
|
441
|
+
* @param {number} p0y - Start point Y coordinate
|
|
442
|
+
* @param {number} p1x - First control point X coordinate
|
|
443
|
+
* @param {number} p1y - First control point Y coordinate
|
|
444
|
+
* @param {number} p2x - Second control point X coordinate
|
|
445
|
+
* @param {number} p2y - Second control point Y coordinate
|
|
446
|
+
* @param {number} p3x - End point X coordinate
|
|
447
|
+
* @param {number} p3y - End point Y coordinate
|
|
448
|
+
* @returns {{x: number, y: number}} First derivative vector
|
|
308
449
|
*/
|
|
309
450
|
function cubicBezierDeriv1(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
|
|
310
451
|
const mt = 1 - t;
|
|
@@ -312,27 +453,61 @@ function cubicBezierDeriv1(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
|
|
|
312
453
|
const t2 = t * t;
|
|
313
454
|
return {
|
|
314
455
|
x: 3 * mt2 * (p1x - p0x) + 6 * mt * t * (p2x - p1x) + 3 * t2 * (p3x - p2x),
|
|
315
|
-
y: 3 * mt2 * (p1y - p0y) + 6 * mt * t * (p2y - p1y) + 3 * t2 * (p3y - p2y)
|
|
456
|
+
y: 3 * mt2 * (p1y - p0y) + 6 * mt * t * (p2y - p1y) + 3 * t2 * (p3y - p2y),
|
|
316
457
|
};
|
|
317
458
|
}
|
|
318
459
|
|
|
319
460
|
/**
|
|
320
461
|
* Second derivative of cubic Bezier at t.
|
|
321
462
|
* B''(t) = 6(1-t)(P2-2P1+P0) + 6t(P3-2P2+P1)
|
|
463
|
+
* @param {number} t - Parameter value (0 to 1)
|
|
464
|
+
* @param {number} p0x - Start point X coordinate
|
|
465
|
+
* @param {number} p0y - Start point Y coordinate
|
|
466
|
+
* @param {number} p1x - First control point X coordinate
|
|
467
|
+
* @param {number} p1y - First control point Y coordinate
|
|
468
|
+
* @param {number} p2x - Second control point X coordinate
|
|
469
|
+
* @param {number} p2y - Second control point Y coordinate
|
|
470
|
+
* @param {number} p3x - End point X coordinate
|
|
471
|
+
* @param {number} p3y - End point Y coordinate
|
|
472
|
+
* @returns {{x: number, y: number}} Second derivative vector
|
|
322
473
|
*/
|
|
323
474
|
function cubicBezierDeriv2(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
|
|
324
475
|
const mt = 1 - t;
|
|
325
476
|
return {
|
|
326
477
|
x: 6 * mt * (p2x - 2 * p1x + p0x) + 6 * t * (p3x - 2 * p2x + p1x),
|
|
327
|
-
y: 6 * mt * (p2y - 2 * p1y + p0y) + 6 * t * (p3y - 2 * p2y + p1y)
|
|
478
|
+
y: 6 * mt * (p2y - 2 * p1y + p0y) + 6 * t * (p3y - 2 * p2y + p1y),
|
|
328
479
|
};
|
|
329
480
|
}
|
|
330
481
|
|
|
331
482
|
/**
|
|
332
483
|
* Find closest t on cubic Bezier to point p using Newton's method.
|
|
333
484
|
* Minimizes |B(t) - p|^2 by finding root of d/dt |B(t)-p|^2 = 2(B(t)-p)·B'(t) = 0
|
|
485
|
+
* @param {number} px - Query point X coordinate
|
|
486
|
+
* @param {number} py - Query point Y coordinate
|
|
487
|
+
* @param {number} p0x - Bezier start point X
|
|
488
|
+
* @param {number} p0y - Bezier start point Y
|
|
489
|
+
* @param {number} p1x - Bezier first control point X
|
|
490
|
+
* @param {number} p1y - Bezier first control point Y
|
|
491
|
+
* @param {number} p2x - Bezier second control point X
|
|
492
|
+
* @param {number} p2y - Bezier second control point Y
|
|
493
|
+
* @param {number} p3x - Bezier end point X
|
|
494
|
+
* @param {number} p3y - Bezier end point Y
|
|
495
|
+
* @param {number} tInit - Initial t value for Newton iteration
|
|
496
|
+
* @returns {number} Closest parameter t (0 to 1)
|
|
334
497
|
*/
|
|
335
|
-
function
|
|
498
|
+
function _closestTOnCubicBezier(
|
|
499
|
+
px,
|
|
500
|
+
py,
|
|
501
|
+
p0x,
|
|
502
|
+
p0y,
|
|
503
|
+
p1x,
|
|
504
|
+
p1y,
|
|
505
|
+
p2x,
|
|
506
|
+
p2y,
|
|
507
|
+
p3x,
|
|
508
|
+
p3y,
|
|
509
|
+
tInit = 0.5,
|
|
510
|
+
) {
|
|
336
511
|
let t = Math.max(0, Math.min(1, tInit));
|
|
337
512
|
|
|
338
513
|
for (let iter = 0; iter < 10; iter++) {
|
|
@@ -351,7 +526,10 @@ function closestTOnCubicBezier(px, py, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t
|
|
|
351
526
|
if (Math.abs(fp) < 1e-12) break;
|
|
352
527
|
|
|
353
528
|
const tNext = Math.max(0, Math.min(1, t - f / fp));
|
|
354
|
-
if (Math.abs(tNext - t) < 1e-9) {
|
|
529
|
+
if (Math.abs(tNext - t) < 1e-9) {
|
|
530
|
+
t = tNext;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
355
533
|
t = tNext;
|
|
356
534
|
}
|
|
357
535
|
|
|
@@ -360,6 +538,13 @@ function closestTOnCubicBezier(px, py, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t
|
|
|
360
538
|
|
|
361
539
|
/**
|
|
362
540
|
* Calculate distance from point to line segment (for straight curve check).
|
|
541
|
+
* @param {number} px - Point X coordinate
|
|
542
|
+
* @param {number} py - Point Y coordinate
|
|
543
|
+
* @param {number} x0 - Line segment start X
|
|
544
|
+
* @param {number} y0 - Line segment start Y
|
|
545
|
+
* @param {number} x1 - Line segment end X
|
|
546
|
+
* @param {number} y1 - Line segment end Y
|
|
547
|
+
* @returns {number} Distance from point to line segment
|
|
363
548
|
*/
|
|
364
549
|
function pointToLineDistance(px, py, x0, y0, x1, y1) {
|
|
365
550
|
const dx = x1 - x0;
|
|
@@ -370,7 +555,10 @@ function pointToLineDistance(px, py, x0, y0, x1, y1) {
|
|
|
370
555
|
return Math.sqrt((px - x0) ** 2 + (py - y0) ** 2);
|
|
371
556
|
}
|
|
372
557
|
|
|
373
|
-
const t = Math.max(
|
|
558
|
+
const t = Math.max(
|
|
559
|
+
0,
|
|
560
|
+
Math.min(1, ((px - x0) * dx + (py - y0) * dy) / lengthSq),
|
|
561
|
+
);
|
|
374
562
|
const projX = x0 + t * dx;
|
|
375
563
|
const projY = y0 + t * dy;
|
|
376
564
|
|
|
@@ -380,7 +568,14 @@ function pointToLineDistance(px, py, x0, y0, x1, y1) {
|
|
|
380
568
|
/**
|
|
381
569
|
* Compute maximum error between cubic Bezier and a line segment.
|
|
382
570
|
* Uses Newton's method to find closest points + midpoint checks to catch bulges.
|
|
383
|
-
*
|
|
571
|
+
* @param {number} p0x - Bezier start point X
|
|
572
|
+
* @param {number} p0y - Bezier start point Y
|
|
573
|
+
* @param {number} cp1x - First control point X
|
|
574
|
+
* @param {number} cp1y - First control point Y
|
|
575
|
+
* @param {number} cp2x - Second control point X
|
|
576
|
+
* @param {number} cp2y - Second control point Y
|
|
577
|
+
* @param {number} p3x - Bezier end point X
|
|
578
|
+
* @param {number} p3y - Bezier end point Y
|
|
384
579
|
* @returns {number} Maximum distance from any curve point to the line
|
|
385
580
|
*/
|
|
386
581
|
function maxErrorCurveToLine(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y) {
|
|
@@ -397,7 +592,17 @@ function maxErrorCurveToLine(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y) {
|
|
|
397
592
|
// Check midpoints between samples to catch bulges
|
|
398
593
|
for (let i = 0; i < samples.length - 1; i++) {
|
|
399
594
|
const tMid = (samples[i] + samples[i + 1]) / 2;
|
|
400
|
-
const pt = cubicBezierPoint(
|
|
595
|
+
const pt = cubicBezierPoint(
|
|
596
|
+
tMid,
|
|
597
|
+
p0x,
|
|
598
|
+
p0y,
|
|
599
|
+
cp1x,
|
|
600
|
+
cp1y,
|
|
601
|
+
cp2x,
|
|
602
|
+
cp2y,
|
|
603
|
+
p3x,
|
|
604
|
+
p3y,
|
|
605
|
+
);
|
|
401
606
|
const dist = pointToLineDistance(pt.x, pt.y, p0x, p0y, p3x, p3y);
|
|
402
607
|
if (dist > maxErr) maxErr = dist;
|
|
403
608
|
}
|
|
@@ -420,6 +625,16 @@ function maxErrorCurveToLine(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y) {
|
|
|
420
625
|
/**
|
|
421
626
|
* Check if a cubic bezier is effectively a straight line.
|
|
422
627
|
* Uses comprehensive sampling + midpoint checks to find actual max deviation.
|
|
628
|
+
* @param {number} x0 - Bezier start point X
|
|
629
|
+
* @param {number} y0 - Bezier start point Y
|
|
630
|
+
* @param {number} cp1x - First control point X
|
|
631
|
+
* @param {number} cp1y - First control point Y
|
|
632
|
+
* @param {number} cp2x - Second control point X
|
|
633
|
+
* @param {number} cp2y - Second control point Y
|
|
634
|
+
* @param {number} x3 - Bezier end point X
|
|
635
|
+
* @param {number} y3 - Bezier end point Y
|
|
636
|
+
* @param {number} tolerance - Maximum deviation to consider straight
|
|
637
|
+
* @returns {boolean} True if curve is effectively straight
|
|
423
638
|
*/
|
|
424
639
|
function isCurveStraight(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, tolerance) {
|
|
425
640
|
const maxError = maxErrorCurveToLine(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3);
|
|
@@ -438,15 +653,17 @@ export function straightCurves(d, tolerance = 0.5, precision = 3) {
|
|
|
438
653
|
const commands = parsePath(d);
|
|
439
654
|
if (commands.length === 0) return d;
|
|
440
655
|
|
|
441
|
-
let cx = 0,
|
|
442
|
-
|
|
656
|
+
let cx = 0,
|
|
657
|
+
cy = 0;
|
|
658
|
+
let startX = 0,
|
|
659
|
+
startY = 0;
|
|
443
660
|
const result = [];
|
|
444
661
|
|
|
445
662
|
for (const cmd of commands) {
|
|
446
663
|
let newCmd = cmd;
|
|
447
664
|
|
|
448
|
-
if (cmd.command ===
|
|
449
|
-
const isAbs = cmd.command ===
|
|
665
|
+
if (cmd.command === "C" || cmd.command === "c") {
|
|
666
|
+
const isAbs = cmd.command === "C";
|
|
450
667
|
const cp1x = isAbs ? cmd.args[0] : cx + cmd.args[0];
|
|
451
668
|
const cp1y = isAbs ? cmd.args[1] : cy + cmd.args[1];
|
|
452
669
|
const cp2x = isAbs ? cmd.args[2] : cx + cmd.args[2];
|
|
@@ -454,10 +671,12 @@ export function straightCurves(d, tolerance = 0.5, precision = 3) {
|
|
|
454
671
|
const endX = isAbs ? cmd.args[4] : cx + cmd.args[4];
|
|
455
672
|
const endY = isAbs ? cmd.args[5] : cy + cmd.args[5];
|
|
456
673
|
|
|
457
|
-
if (
|
|
674
|
+
if (
|
|
675
|
+
isCurveStraight(cx, cy, cp1x, cp1y, cp2x, cp2y, endX, endY, tolerance)
|
|
676
|
+
) {
|
|
458
677
|
newCmd = isAbs
|
|
459
|
-
? { command:
|
|
460
|
-
: { command:
|
|
678
|
+
? { command: "L", args: [endX, endY] }
|
|
679
|
+
: { command: "l", args: [endX - cx, endY - cy] };
|
|
461
680
|
}
|
|
462
681
|
}
|
|
463
682
|
|
|
@@ -466,14 +685,40 @@ export function straightCurves(d, tolerance = 0.5, precision = 3) {
|
|
|
466
685
|
// Update position
|
|
467
686
|
const abs = toAbsolute(newCmd, cx, cy);
|
|
468
687
|
switch (abs.command) {
|
|
469
|
-
case
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
case
|
|
476
|
-
case
|
|
688
|
+
case "M":
|
|
689
|
+
cx = abs.args[0];
|
|
690
|
+
cy = abs.args[1];
|
|
691
|
+
startX = cx;
|
|
692
|
+
startY = cy;
|
|
693
|
+
break;
|
|
694
|
+
case "L":
|
|
695
|
+
case "T":
|
|
696
|
+
cx = abs.args[0];
|
|
697
|
+
cy = abs.args[1];
|
|
698
|
+
break;
|
|
699
|
+
case "H":
|
|
700
|
+
cx = abs.args[0];
|
|
701
|
+
break;
|
|
702
|
+
case "V":
|
|
703
|
+
cy = abs.args[0];
|
|
704
|
+
break;
|
|
705
|
+
case "C":
|
|
706
|
+
cx = abs.args[4];
|
|
707
|
+
cy = abs.args[5];
|
|
708
|
+
break;
|
|
709
|
+
case "S":
|
|
710
|
+
case "Q":
|
|
711
|
+
cx = abs.args[2];
|
|
712
|
+
cy = abs.args[3];
|
|
713
|
+
break;
|
|
714
|
+
case "A":
|
|
715
|
+
cx = abs.args[5];
|
|
716
|
+
cy = abs.args[6];
|
|
717
|
+
break;
|
|
718
|
+
case "Z":
|
|
719
|
+
cx = startX;
|
|
720
|
+
cy = startY;
|
|
721
|
+
break;
|
|
477
722
|
}
|
|
478
723
|
}
|
|
479
724
|
|
|
@@ -518,9 +763,9 @@ export function floatPrecision(d, precision = 3) {
|
|
|
518
763
|
if (commands.length === 0) return d;
|
|
519
764
|
|
|
520
765
|
const factor = Math.pow(10, precision);
|
|
521
|
-
const rounded = commands.map(cmd => ({
|
|
766
|
+
const rounded = commands.map((cmd) => ({
|
|
522
767
|
command: cmd.command,
|
|
523
|
-
args: cmd.args.map(n => Math.round(n * factor) / factor)
|
|
768
|
+
args: cmd.args.map((n) => Math.round(n * factor) / factor),
|
|
524
769
|
}));
|
|
525
770
|
|
|
526
771
|
return serializePath(rounded, precision);
|
|
@@ -543,8 +788,10 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
|
|
|
543
788
|
const commands = parsePath(d);
|
|
544
789
|
if (commands.length === 0) return d;
|
|
545
790
|
|
|
546
|
-
let cx = 0,
|
|
547
|
-
|
|
791
|
+
let cx = 0,
|
|
792
|
+
cy = 0;
|
|
793
|
+
let startX = 0,
|
|
794
|
+
startY = 0;
|
|
548
795
|
const result = [];
|
|
549
796
|
|
|
550
797
|
for (const cmd of commands) {
|
|
@@ -553,30 +800,35 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
|
|
|
553
800
|
|
|
554
801
|
// Check for zero-length commands
|
|
555
802
|
switch (abs.command) {
|
|
556
|
-
case
|
|
557
|
-
|
|
558
|
-
|
|
803
|
+
case "L":
|
|
804
|
+
case "T":
|
|
805
|
+
if (
|
|
806
|
+
Math.abs(abs.args[0] - cx) < tolerance &&
|
|
807
|
+
Math.abs(abs.args[1] - cy) < tolerance
|
|
808
|
+
) {
|
|
559
809
|
keep = false;
|
|
560
810
|
}
|
|
561
811
|
break;
|
|
562
|
-
case
|
|
812
|
+
case "H":
|
|
563
813
|
if (Math.abs(abs.args[0] - cx) < tolerance) {
|
|
564
814
|
keep = false;
|
|
565
815
|
}
|
|
566
816
|
break;
|
|
567
|
-
case
|
|
817
|
+
case "V":
|
|
568
818
|
if (Math.abs(abs.args[0] - cy) < tolerance) {
|
|
569
819
|
keep = false;
|
|
570
820
|
}
|
|
571
821
|
break;
|
|
572
|
-
case
|
|
822
|
+
case "C":
|
|
573
823
|
// Zero-length curve where all points are the same
|
|
574
|
-
if (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
824
|
+
if (
|
|
825
|
+
Math.abs(abs.args[4] - cx) < tolerance &&
|
|
826
|
+
Math.abs(abs.args[5] - cy) < tolerance &&
|
|
827
|
+
Math.abs(abs.args[0] - cx) < tolerance &&
|
|
828
|
+
Math.abs(abs.args[1] - cy) < tolerance &&
|
|
829
|
+
Math.abs(abs.args[2] - cx) < tolerance &&
|
|
830
|
+
Math.abs(abs.args[3] - cy) < tolerance
|
|
831
|
+
) {
|
|
580
832
|
keep = false;
|
|
581
833
|
}
|
|
582
834
|
break;
|
|
@@ -587,14 +839,40 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
|
|
|
587
839
|
|
|
588
840
|
// Update position
|
|
589
841
|
switch (abs.command) {
|
|
590
|
-
case
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
case
|
|
597
|
-
case
|
|
842
|
+
case "M":
|
|
843
|
+
cx = abs.args[0];
|
|
844
|
+
cy = abs.args[1];
|
|
845
|
+
startX = cx;
|
|
846
|
+
startY = cy;
|
|
847
|
+
break;
|
|
848
|
+
case "L":
|
|
849
|
+
case "T":
|
|
850
|
+
cx = abs.args[0];
|
|
851
|
+
cy = abs.args[1];
|
|
852
|
+
break;
|
|
853
|
+
case "H":
|
|
854
|
+
cx = abs.args[0];
|
|
855
|
+
break;
|
|
856
|
+
case "V":
|
|
857
|
+
cy = abs.args[0];
|
|
858
|
+
break;
|
|
859
|
+
case "C":
|
|
860
|
+
cx = abs.args[4];
|
|
861
|
+
cy = abs.args[5];
|
|
862
|
+
break;
|
|
863
|
+
case "S":
|
|
864
|
+
case "Q":
|
|
865
|
+
cx = abs.args[2];
|
|
866
|
+
cy = abs.args[3];
|
|
867
|
+
break;
|
|
868
|
+
case "A":
|
|
869
|
+
cx = abs.args[5];
|
|
870
|
+
cy = abs.args[6];
|
|
871
|
+
break;
|
|
872
|
+
case "Z":
|
|
873
|
+
cx = startX;
|
|
874
|
+
cy = startY;
|
|
875
|
+
break;
|
|
598
876
|
}
|
|
599
877
|
}
|
|
600
878
|
}
|
|
@@ -610,6 +888,14 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
|
|
|
610
888
|
/**
|
|
611
889
|
* Evaluate a quadratic Bezier curve at parameter t.
|
|
612
890
|
* B(t) = (1-t)^2*P0 + 2*(1-t)*t*P1 + t^2*P2
|
|
891
|
+
* @param {number} t - Parameter value (0 to 1)
|
|
892
|
+
* @param {number} p0x - Start point X coordinate
|
|
893
|
+
* @param {number} p0y - Start point Y coordinate
|
|
894
|
+
* @param {number} p1x - Control point X coordinate
|
|
895
|
+
* @param {number} p1y - Control point Y coordinate
|
|
896
|
+
* @param {number} p2x - End point X coordinate
|
|
897
|
+
* @param {number} p2y - End point Y coordinate
|
|
898
|
+
* @returns {{x: number, y: number}} Point on curve
|
|
613
899
|
*/
|
|
614
900
|
function quadraticBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y) {
|
|
615
901
|
const mt = 1 - t;
|
|
@@ -617,26 +903,58 @@ function quadraticBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y) {
|
|
|
617
903
|
const t2 = t * t;
|
|
618
904
|
return {
|
|
619
905
|
x: mt2 * p0x + 2 * mt * t * p1x + t2 * p2x,
|
|
620
|
-
y: mt2 * p0y + 2 * mt * t * p1y + t2 * p2y
|
|
906
|
+
y: mt2 * p0y + 2 * mt * t * p1y + t2 * p2y,
|
|
621
907
|
};
|
|
622
908
|
}
|
|
623
909
|
|
|
624
910
|
/**
|
|
625
911
|
* Compute maximum error between cubic Bezier and quadratic Bezier.
|
|
626
912
|
* Samples both curves and checks midpoints to find actual max deviation.
|
|
627
|
-
*
|
|
913
|
+
* @param {number} p0x - Bezier start point X
|
|
914
|
+
* @param {number} p0y - Bezier start point Y
|
|
915
|
+
* @param {number} cp1x - Cubic first control point X
|
|
916
|
+
* @param {number} cp1y - Cubic first control point Y
|
|
917
|
+
* @param {number} cp2x - Cubic second control point X
|
|
918
|
+
* @param {number} cp2y - Cubic second control point Y
|
|
919
|
+
* @param {number} p3x - Bezier end point X
|
|
920
|
+
* @param {number} p3y - Bezier end point Y
|
|
921
|
+
* @param {number} qx - Quadratic control point X
|
|
922
|
+
* @param {number} qy - Quadratic control point Y
|
|
628
923
|
* @returns {number} Maximum distance between corresponding points on both curves
|
|
629
924
|
*/
|
|
630
|
-
function maxErrorCubicToQuadratic(
|
|
925
|
+
function maxErrorCubicToQuadratic(
|
|
926
|
+
p0x,
|
|
927
|
+
p0y,
|
|
928
|
+
cp1x,
|
|
929
|
+
cp1y,
|
|
930
|
+
cp2x,
|
|
931
|
+
cp2y,
|
|
932
|
+
p3x,
|
|
933
|
+
p3y,
|
|
934
|
+
qx,
|
|
935
|
+
qy,
|
|
936
|
+
) {
|
|
631
937
|
let maxErr = 0;
|
|
632
938
|
|
|
633
939
|
// Dense sampling including midpoints
|
|
634
|
-
const samples = [
|
|
635
|
-
|
|
940
|
+
const samples = [
|
|
941
|
+
0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7,
|
|
942
|
+
0.75, 0.8, 0.85, 0.9, 0.95,
|
|
943
|
+
];
|
|
636
944
|
|
|
637
945
|
for (const t of samples) {
|
|
638
946
|
// Point on original cubic
|
|
639
|
-
const cubic = cubicBezierPoint(
|
|
947
|
+
const cubic = cubicBezierPoint(
|
|
948
|
+
t,
|
|
949
|
+
p0x,
|
|
950
|
+
p0y,
|
|
951
|
+
cp1x,
|
|
952
|
+
cp1y,
|
|
953
|
+
cp2x,
|
|
954
|
+
cp2y,
|
|
955
|
+
p3x,
|
|
956
|
+
p3y,
|
|
957
|
+
);
|
|
640
958
|
// Point on proposed quadratic
|
|
641
959
|
const quad = quadraticBezierPoint(t, p0x, p0y, qx, qy, p3x, p3y);
|
|
642
960
|
|
|
@@ -650,10 +968,28 @@ function maxErrorCubicToQuadratic(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y, qx
|
|
|
650
968
|
/**
|
|
651
969
|
* Check if a cubic bezier can be approximated by a quadratic.
|
|
652
970
|
* VERIFIED by comparing actual curve points with comprehensive sampling.
|
|
653
|
-
*
|
|
971
|
+
* @param {number} x0 - Bezier start point X
|
|
972
|
+
* @param {number} y0 - Bezier start point Y
|
|
973
|
+
* @param {number} cp1x - Cubic first control point X
|
|
974
|
+
* @param {number} cp1y - Cubic first control point Y
|
|
975
|
+
* @param {number} cp2x - Cubic second control point X
|
|
976
|
+
* @param {number} cp2y - Cubic second control point Y
|
|
977
|
+
* @param {number} x3 - Bezier end point X
|
|
978
|
+
* @param {number} y3 - Bezier end point Y
|
|
979
|
+
* @param {number} tolerance - Maximum deviation for conversion
|
|
654
980
|
* @returns {{cpx: number, cpy: number} | null} Quadratic control point or null
|
|
655
981
|
*/
|
|
656
|
-
function cubicToQuadraticControlPoint(
|
|
982
|
+
function cubicToQuadraticControlPoint(
|
|
983
|
+
x0,
|
|
984
|
+
y0,
|
|
985
|
+
cp1x,
|
|
986
|
+
cp1y,
|
|
987
|
+
cp2x,
|
|
988
|
+
cp2y,
|
|
989
|
+
x3,
|
|
990
|
+
y3,
|
|
991
|
+
tolerance,
|
|
992
|
+
) {
|
|
657
993
|
// Calculate the best-fit quadratic control point
|
|
658
994
|
// For a cubic to be exactly representable as quadratic:
|
|
659
995
|
// Q = (3*(P1 + P2) - P0 - P3) / 4
|
|
@@ -661,7 +997,18 @@ function cubicToQuadraticControlPoint(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, to
|
|
|
661
997
|
const qy = (3 * (cp1y + cp2y) - y0 - y3) / 4;
|
|
662
998
|
|
|
663
999
|
// VERIFY by computing actual max error between the curves
|
|
664
|
-
const maxError = maxErrorCubicToQuadratic(
|
|
1000
|
+
const maxError = maxErrorCubicToQuadratic(
|
|
1001
|
+
x0,
|
|
1002
|
+
y0,
|
|
1003
|
+
cp1x,
|
|
1004
|
+
cp1y,
|
|
1005
|
+
cp2x,
|
|
1006
|
+
cp2y,
|
|
1007
|
+
x3,
|
|
1008
|
+
y3,
|
|
1009
|
+
qx,
|
|
1010
|
+
qy,
|
|
1011
|
+
);
|
|
665
1012
|
|
|
666
1013
|
if (maxError <= tolerance) {
|
|
667
1014
|
return { cpx: qx, cpy: qy };
|
|
@@ -682,15 +1029,17 @@ export function convertCubicToQuadratic(d, tolerance = 0.5, precision = 3) {
|
|
|
682
1029
|
const commands = parsePath(d);
|
|
683
1030
|
if (commands.length === 0) return d;
|
|
684
1031
|
|
|
685
|
-
let cx = 0,
|
|
686
|
-
|
|
1032
|
+
let cx = 0,
|
|
1033
|
+
cy = 0;
|
|
1034
|
+
let startX = 0,
|
|
1035
|
+
startY = 0;
|
|
687
1036
|
const result = [];
|
|
688
1037
|
|
|
689
1038
|
for (const cmd of commands) {
|
|
690
1039
|
let newCmd = cmd;
|
|
691
1040
|
|
|
692
|
-
if (cmd.command ===
|
|
693
|
-
const isAbs = cmd.command ===
|
|
1041
|
+
if (cmd.command === "C" || cmd.command === "c") {
|
|
1042
|
+
const isAbs = cmd.command === "C";
|
|
694
1043
|
const cp1x = isAbs ? cmd.args[0] : cx + cmd.args[0];
|
|
695
1044
|
const cp1y = isAbs ? cmd.args[1] : cy + cmd.args[1];
|
|
696
1045
|
const cp2x = isAbs ? cmd.args[2] : cx + cmd.args[2];
|
|
@@ -698,12 +1047,25 @@ export function convertCubicToQuadratic(d, tolerance = 0.5, precision = 3) {
|
|
|
698
1047
|
const endX = isAbs ? cmd.args[4] : cx + cmd.args[4];
|
|
699
1048
|
const endY = isAbs ? cmd.args[5] : cy + cmd.args[5];
|
|
700
1049
|
|
|
701
|
-
const quadCP = cubicToQuadraticControlPoint(
|
|
1050
|
+
const quadCP = cubicToQuadraticControlPoint(
|
|
1051
|
+
cx,
|
|
1052
|
+
cy,
|
|
1053
|
+
cp1x,
|
|
1054
|
+
cp1y,
|
|
1055
|
+
cp2x,
|
|
1056
|
+
cp2y,
|
|
1057
|
+
endX,
|
|
1058
|
+
endY,
|
|
1059
|
+
tolerance,
|
|
1060
|
+
);
|
|
702
1061
|
|
|
703
1062
|
if (quadCP) {
|
|
704
1063
|
newCmd = isAbs
|
|
705
|
-
? { command:
|
|
706
|
-
: {
|
|
1064
|
+
? { command: "Q", args: [quadCP.cpx, quadCP.cpy, endX, endY] }
|
|
1065
|
+
: {
|
|
1066
|
+
command: "q",
|
|
1067
|
+
args: [quadCP.cpx - cx, quadCP.cpy - cy, endX - cx, endY - cy],
|
|
1068
|
+
};
|
|
707
1069
|
}
|
|
708
1070
|
}
|
|
709
1071
|
|
|
@@ -712,14 +1074,40 @@ export function convertCubicToQuadratic(d, tolerance = 0.5, precision = 3) {
|
|
|
712
1074
|
// Update position
|
|
713
1075
|
const abs = toAbsolute(newCmd, cx, cy);
|
|
714
1076
|
switch (abs.command) {
|
|
715
|
-
case
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
case
|
|
722
|
-
case
|
|
1077
|
+
case "M":
|
|
1078
|
+
cx = abs.args[0];
|
|
1079
|
+
cy = abs.args[1];
|
|
1080
|
+
startX = cx;
|
|
1081
|
+
startY = cy;
|
|
1082
|
+
break;
|
|
1083
|
+
case "L":
|
|
1084
|
+
case "T":
|
|
1085
|
+
cx = abs.args[0];
|
|
1086
|
+
cy = abs.args[1];
|
|
1087
|
+
break;
|
|
1088
|
+
case "H":
|
|
1089
|
+
cx = abs.args[0];
|
|
1090
|
+
break;
|
|
1091
|
+
case "V":
|
|
1092
|
+
cy = abs.args[0];
|
|
1093
|
+
break;
|
|
1094
|
+
case "C":
|
|
1095
|
+
cx = abs.args[4];
|
|
1096
|
+
cy = abs.args[5];
|
|
1097
|
+
break;
|
|
1098
|
+
case "S":
|
|
1099
|
+
case "Q":
|
|
1100
|
+
cx = abs.args[2];
|
|
1101
|
+
cy = abs.args[3];
|
|
1102
|
+
break;
|
|
1103
|
+
case "A":
|
|
1104
|
+
cx = abs.args[5];
|
|
1105
|
+
cy = abs.args[6];
|
|
1106
|
+
break;
|
|
1107
|
+
case "Z":
|
|
1108
|
+
cx = startX;
|
|
1109
|
+
cy = startY;
|
|
1110
|
+
break;
|
|
723
1111
|
}
|
|
724
1112
|
}
|
|
725
1113
|
|
|
@@ -743,38 +1131,43 @@ export function convertQuadraticToSmooth(d, tolerance = 1e-6, precision = 3) {
|
|
|
743
1131
|
const commands = parsePath(d);
|
|
744
1132
|
if (commands.length === 0) return d;
|
|
745
1133
|
|
|
746
|
-
let cx = 0,
|
|
747
|
-
|
|
748
|
-
let
|
|
1134
|
+
let cx = 0,
|
|
1135
|
+
cy = 0;
|
|
1136
|
+
let startX = 0,
|
|
1137
|
+
startY = 0;
|
|
1138
|
+
let lastQcpX = null,
|
|
1139
|
+
lastQcpY = null;
|
|
749
1140
|
const result = [];
|
|
750
1141
|
|
|
751
1142
|
for (const cmd of commands) {
|
|
752
1143
|
let newCmd = cmd;
|
|
753
1144
|
const abs = toAbsolute(cmd, cx, cy);
|
|
754
1145
|
|
|
755
|
-
if (abs.command ===
|
|
1146
|
+
if (abs.command === "Q" && lastQcpX !== null) {
|
|
756
1147
|
// Calculate reflected control point
|
|
757
1148
|
const reflectedCpX = 2 * cx - lastQcpX;
|
|
758
1149
|
const reflectedCpY = 2 * cy - lastQcpY;
|
|
759
1150
|
|
|
760
1151
|
// Check if current control point matches reflection
|
|
761
|
-
if (
|
|
762
|
-
|
|
1152
|
+
if (
|
|
1153
|
+
Math.abs(abs.args[0] - reflectedCpX) < tolerance &&
|
|
1154
|
+
Math.abs(abs.args[1] - reflectedCpY) < tolerance
|
|
1155
|
+
) {
|
|
763
1156
|
// Can use T command
|
|
764
|
-
const isAbs = cmd.command ===
|
|
1157
|
+
const isAbs = cmd.command === "Q";
|
|
765
1158
|
newCmd = isAbs
|
|
766
|
-
? { command:
|
|
767
|
-
: { command:
|
|
1159
|
+
? { command: "T", args: [abs.args[2], abs.args[3]] }
|
|
1160
|
+
: { command: "t", args: [abs.args[2] - cx, abs.args[3] - cy] };
|
|
768
1161
|
}
|
|
769
1162
|
}
|
|
770
1163
|
|
|
771
1164
|
result.push(newCmd);
|
|
772
1165
|
|
|
773
1166
|
// Track control points for reflection
|
|
774
|
-
if (abs.command ===
|
|
1167
|
+
if (abs.command === "Q") {
|
|
775
1168
|
lastQcpX = abs.args[0];
|
|
776
1169
|
lastQcpY = abs.args[1];
|
|
777
|
-
} else if (abs.command ===
|
|
1170
|
+
} else if (abs.command === "T") {
|
|
778
1171
|
// T uses reflected control point
|
|
779
1172
|
lastQcpX = 2 * cx - lastQcpX;
|
|
780
1173
|
lastQcpY = 2 * cy - lastQcpY;
|
|
@@ -785,14 +1178,40 @@ export function convertQuadraticToSmooth(d, tolerance = 1e-6, precision = 3) {
|
|
|
785
1178
|
|
|
786
1179
|
// Update position
|
|
787
1180
|
switch (abs.command) {
|
|
788
|
-
case
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
case
|
|
795
|
-
case
|
|
1181
|
+
case "M":
|
|
1182
|
+
cx = abs.args[0];
|
|
1183
|
+
cy = abs.args[1];
|
|
1184
|
+
startX = cx;
|
|
1185
|
+
startY = cy;
|
|
1186
|
+
break;
|
|
1187
|
+
case "L":
|
|
1188
|
+
case "T":
|
|
1189
|
+
cx = abs.args[0];
|
|
1190
|
+
cy = abs.args[1];
|
|
1191
|
+
break;
|
|
1192
|
+
case "H":
|
|
1193
|
+
cx = abs.args[0];
|
|
1194
|
+
break;
|
|
1195
|
+
case "V":
|
|
1196
|
+
cy = abs.args[0];
|
|
1197
|
+
break;
|
|
1198
|
+
case "C":
|
|
1199
|
+
cx = abs.args[4];
|
|
1200
|
+
cy = abs.args[5];
|
|
1201
|
+
break;
|
|
1202
|
+
case "S":
|
|
1203
|
+
case "Q":
|
|
1204
|
+
cx = abs.args[2];
|
|
1205
|
+
cy = abs.args[3];
|
|
1206
|
+
break;
|
|
1207
|
+
case "A":
|
|
1208
|
+
cx = abs.args[5];
|
|
1209
|
+
cy = abs.args[6];
|
|
1210
|
+
break;
|
|
1211
|
+
case "Z":
|
|
1212
|
+
cx = startX;
|
|
1213
|
+
cy = startY;
|
|
1214
|
+
break;
|
|
796
1215
|
}
|
|
797
1216
|
}
|
|
798
1217
|
|
|
@@ -816,38 +1235,54 @@ export function convertCubicToSmooth(d, tolerance = 1e-6, precision = 3) {
|
|
|
816
1235
|
const commands = parsePath(d);
|
|
817
1236
|
if (commands.length === 0) return d;
|
|
818
1237
|
|
|
819
|
-
let cx = 0,
|
|
820
|
-
|
|
821
|
-
let
|
|
1238
|
+
let cx = 0,
|
|
1239
|
+
cy = 0;
|
|
1240
|
+
let startX = 0,
|
|
1241
|
+
startY = 0;
|
|
1242
|
+
let lastCcp2X = null,
|
|
1243
|
+
lastCcp2Y = null;
|
|
822
1244
|
const result = [];
|
|
823
1245
|
|
|
824
1246
|
for (const cmd of commands) {
|
|
825
1247
|
let newCmd = cmd;
|
|
826
1248
|
const abs = toAbsolute(cmd, cx, cy);
|
|
827
1249
|
|
|
828
|
-
if (abs.command ===
|
|
1250
|
+
if (abs.command === "C" && lastCcp2X !== null) {
|
|
829
1251
|
// Calculate reflected control point
|
|
830
1252
|
const reflectedCpX = 2 * cx - lastCcp2X;
|
|
831
1253
|
const reflectedCpY = 2 * cy - lastCcp2Y;
|
|
832
1254
|
|
|
833
1255
|
// Check if first control point matches reflection
|
|
834
|
-
if (
|
|
835
|
-
|
|
1256
|
+
if (
|
|
1257
|
+
Math.abs(abs.args[0] - reflectedCpX) < tolerance &&
|
|
1258
|
+
Math.abs(abs.args[1] - reflectedCpY) < tolerance
|
|
1259
|
+
) {
|
|
836
1260
|
// Can use S command
|
|
837
|
-
const isAbs = cmd.command ===
|
|
1261
|
+
const isAbs = cmd.command === "C";
|
|
838
1262
|
newCmd = isAbs
|
|
839
|
-
? {
|
|
840
|
-
|
|
1263
|
+
? {
|
|
1264
|
+
command: "S",
|
|
1265
|
+
args: [abs.args[2], abs.args[3], abs.args[4], abs.args[5]],
|
|
1266
|
+
}
|
|
1267
|
+
: {
|
|
1268
|
+
command: "s",
|
|
1269
|
+
args: [
|
|
1270
|
+
abs.args[2] - cx,
|
|
1271
|
+
abs.args[3] - cy,
|
|
1272
|
+
abs.args[4] - cx,
|
|
1273
|
+
abs.args[5] - cy,
|
|
1274
|
+
],
|
|
1275
|
+
};
|
|
841
1276
|
}
|
|
842
1277
|
}
|
|
843
1278
|
|
|
844
1279
|
result.push(newCmd);
|
|
845
1280
|
|
|
846
1281
|
// Track control points for reflection
|
|
847
|
-
if (abs.command ===
|
|
1282
|
+
if (abs.command === "C") {
|
|
848
1283
|
lastCcp2X = abs.args[2];
|
|
849
1284
|
lastCcp2Y = abs.args[3];
|
|
850
|
-
} else if (abs.command ===
|
|
1285
|
+
} else if (abs.command === "S") {
|
|
851
1286
|
// S uses its control point as the second cubic control point
|
|
852
1287
|
lastCcp2X = abs.args[0];
|
|
853
1288
|
lastCcp2Y = abs.args[1];
|
|
@@ -858,14 +1293,40 @@ export function convertCubicToSmooth(d, tolerance = 1e-6, precision = 3) {
|
|
|
858
1293
|
|
|
859
1294
|
// Update position
|
|
860
1295
|
switch (abs.command) {
|
|
861
|
-
case
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
case
|
|
868
|
-
case
|
|
1296
|
+
case "M":
|
|
1297
|
+
cx = abs.args[0];
|
|
1298
|
+
cy = abs.args[1];
|
|
1299
|
+
startX = cx;
|
|
1300
|
+
startY = cy;
|
|
1301
|
+
break;
|
|
1302
|
+
case "L":
|
|
1303
|
+
case "T":
|
|
1304
|
+
cx = abs.args[0];
|
|
1305
|
+
cy = abs.args[1];
|
|
1306
|
+
break;
|
|
1307
|
+
case "H":
|
|
1308
|
+
cx = abs.args[0];
|
|
1309
|
+
break;
|
|
1310
|
+
case "V":
|
|
1311
|
+
cy = abs.args[0];
|
|
1312
|
+
break;
|
|
1313
|
+
case "C":
|
|
1314
|
+
cx = abs.args[4];
|
|
1315
|
+
cy = abs.args[5];
|
|
1316
|
+
break;
|
|
1317
|
+
case "S":
|
|
1318
|
+
case "Q":
|
|
1319
|
+
cx = abs.args[2];
|
|
1320
|
+
cy = abs.args[3];
|
|
1321
|
+
break;
|
|
1322
|
+
case "A":
|
|
1323
|
+
cx = abs.args[5];
|
|
1324
|
+
cy = abs.args[6];
|
|
1325
|
+
break;
|
|
1326
|
+
case "Z":
|
|
1327
|
+
cx = startX;
|
|
1328
|
+
cy = startY;
|
|
1329
|
+
break;
|
|
869
1330
|
}
|
|
870
1331
|
}
|
|
871
1332
|
|
|
@@ -889,8 +1350,8 @@ export function arcShorthands(d, precision = 3) {
|
|
|
889
1350
|
const commands = parsePath(d);
|
|
890
1351
|
if (commands.length === 0) return d;
|
|
891
1352
|
|
|
892
|
-
const result = commands.map(cmd => {
|
|
893
|
-
if (cmd.command ===
|
|
1353
|
+
const result = commands.map((cmd) => {
|
|
1354
|
+
if (cmd.command === "A" || cmd.command === "a") {
|
|
894
1355
|
const args = [...cmd.args];
|
|
895
1356
|
// Normalize rotation angle to 0-360
|
|
896
1357
|
args[2] = ((args[2] % 360) + 360) % 360;
|
|
@@ -924,5 +1385,5 @@ export default {
|
|
|
924
1385
|
convertCubicToQuadratic,
|
|
925
1386
|
convertQuadraticToSmooth,
|
|
926
1387
|
convertCubicToSmooth,
|
|
927
|
-
arcShorthands
|
|
1388
|
+
arcShorthands,
|
|
928
1389
|
};
|