@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/convert-path-data.js
CHANGED
|
@@ -12,24 +12,44 @@
|
|
|
12
12
|
* @module convert-path-data
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import Decimal from
|
|
15
|
+
import Decimal from "decimal.js";
|
|
16
16
|
|
|
17
17
|
Decimal.set({ precision: 80 });
|
|
18
18
|
|
|
19
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
19
|
+
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
20
20
|
|
|
21
21
|
// SVG path command parameters count
|
|
22
22
|
const COMMAND_PARAMS = {
|
|
23
|
-
M: 2,
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
M: 2,
|
|
24
|
+
m: 2,
|
|
25
|
+
L: 2,
|
|
26
|
+
l: 2,
|
|
27
|
+
H: 1,
|
|
28
|
+
h: 1,
|
|
29
|
+
V: 1,
|
|
30
|
+
v: 1,
|
|
31
|
+
C: 6,
|
|
32
|
+
c: 6,
|
|
33
|
+
S: 4,
|
|
34
|
+
s: 4,
|
|
35
|
+
Q: 4,
|
|
36
|
+
q: 4,
|
|
37
|
+
T: 2,
|
|
38
|
+
t: 2,
|
|
39
|
+
A: 7,
|
|
40
|
+
a: 7,
|
|
41
|
+
Z: 0,
|
|
42
|
+
z: 0,
|
|
26
43
|
};
|
|
27
44
|
|
|
28
45
|
/**
|
|
29
|
-
* Parse arc arguments specially - flags are always single 0 or 1 digits
|
|
46
|
+
* Parse arc arguments specially - flags are always single 0 or 1 digits.
|
|
30
47
|
* Arc format: rx ry x-axis-rotation large-arc-flag sweep-flag x y
|
|
31
48
|
* Flags can be written without separators: "0 01 20" or "0120"
|
|
32
49
|
* BUG FIX #1: Handle compact notation where flags are concatenated with next number
|
|
50
|
+
* BUG FIX #5: Handle invalid arc flags gracefully - return null to signal parsing failure
|
|
51
|
+
* @param {string} argsStr - Arc arguments string to parse
|
|
52
|
+
* @returns {Array<number>|null} Parsed arc parameters or null if invalid
|
|
33
53
|
*/
|
|
34
54
|
function parseArcArgs(argsStr) {
|
|
35
55
|
const args = [];
|
|
@@ -50,13 +70,14 @@ function parseArcArgs(argsStr) {
|
|
|
50
70
|
if (paramInArc === 3 || paramInArc === 4) {
|
|
51
71
|
// Flags: must be single 0 or 1 (arc flags are always exactly one character)
|
|
52
72
|
// BUG FIX #1: Handle compact notation like "0120" -> "01" (flags) + "20" (next number)
|
|
53
|
-
if (argsStr[pos] ===
|
|
54
|
-
args.push(argsStr[pos] ===
|
|
73
|
+
if (argsStr[pos] === "0" || argsStr[pos] === "1") {
|
|
74
|
+
args.push(argsStr[pos] === "1" ? 1 : 0);
|
|
55
75
|
pos++;
|
|
56
76
|
arcIndex++;
|
|
57
77
|
} else {
|
|
58
|
-
// BUG FIX #
|
|
59
|
-
|
|
78
|
+
// BUG FIX #5: Arc flags MUST be exactly 0 or 1 - return null for invalid values
|
|
79
|
+
// This signals the caller to skip this arc command gracefully
|
|
80
|
+
return null;
|
|
60
81
|
}
|
|
61
82
|
} else {
|
|
62
83
|
// Regular number
|
|
@@ -82,7 +103,7 @@ function parseArcArgs(argsStr) {
|
|
|
82
103
|
* @returns {Array<{command: string, args: number[]}>} Parsed commands
|
|
83
104
|
*/
|
|
84
105
|
export function parsePath(d) {
|
|
85
|
-
if (!d || typeof d !==
|
|
106
|
+
if (!d || typeof d !== "string") return [];
|
|
86
107
|
|
|
87
108
|
const commands = [];
|
|
88
109
|
const cmdRegex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
|
|
@@ -92,21 +113,24 @@ export function parsePath(d) {
|
|
|
92
113
|
const cmd = match[1];
|
|
93
114
|
const argsStr = match[2].trim();
|
|
94
115
|
|
|
95
|
-
if (cmd ===
|
|
96
|
-
commands.push({ command:
|
|
116
|
+
if (cmd === "Z" || cmd === "z") {
|
|
117
|
+
commands.push({ command: "Z", args: [] });
|
|
97
118
|
// BUG FIX #1: Check for implicit M after Z (numbers after Z should start a new subpath)
|
|
98
119
|
const remainingNums = argsStr.match(/-?\d*\.?\d+(?:[eE][+-]?\d+)?/g);
|
|
99
120
|
if (remainingNums && remainingNums.length >= 2) {
|
|
100
121
|
// Implicit M command after Z
|
|
101
122
|
commands.push({
|
|
102
|
-
command:
|
|
103
|
-
args: [parseFloat(remainingNums[0]), parseFloat(remainingNums[1])]
|
|
123
|
+
command: "M",
|
|
124
|
+
args: [parseFloat(remainingNums[0]), parseFloat(remainingNums[1])],
|
|
104
125
|
});
|
|
105
126
|
// Continue parsing remaining args as implicit L
|
|
106
127
|
for (let i = 2; i + 1 < remainingNums.length; i += 2) {
|
|
107
128
|
commands.push({
|
|
108
|
-
command:
|
|
109
|
-
args: [
|
|
129
|
+
command: "L",
|
|
130
|
+
args: [
|
|
131
|
+
parseFloat(remainingNums[i]),
|
|
132
|
+
parseFloat(remainingNums[i + 1]),
|
|
133
|
+
],
|
|
110
134
|
});
|
|
111
135
|
}
|
|
112
136
|
}
|
|
@@ -114,15 +138,23 @@ export function parsePath(d) {
|
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
let nums;
|
|
117
|
-
if (cmd ===
|
|
141
|
+
if (cmd === "A" || cmd === "a") {
|
|
118
142
|
// Arc commands need special parsing for flags
|
|
119
143
|
nums = parseArcArgs(argsStr);
|
|
120
144
|
|
|
145
|
+
// BUG FIX #5: If arc parsing failed due to invalid flags, skip this command
|
|
146
|
+
if (nums === null) {
|
|
147
|
+
console.warn(
|
|
148
|
+
`Invalid arc command with malformed flags - skipping: ${cmd}${argsStr}`,
|
|
149
|
+
);
|
|
150
|
+
continue; // Skip this command and continue processing the rest
|
|
151
|
+
}
|
|
152
|
+
|
|
121
153
|
// BUG FIX #2: Normalize negative arc radii to absolute values per SVG spec Section 8.3.8
|
|
122
154
|
// Process each complete arc (7 parameters)
|
|
123
155
|
for (let i = 0; i < nums.length; i += 7) {
|
|
124
156
|
if (i + 6 < nums.length) {
|
|
125
|
-
nums[i] = Math.abs(nums[i]);
|
|
157
|
+
nums[i] = Math.abs(nums[i]); // rx
|
|
126
158
|
nums[i + 1] = Math.abs(nums[i + 1]); // ry
|
|
127
159
|
}
|
|
128
160
|
}
|
|
@@ -144,13 +176,18 @@ export function parsePath(d) {
|
|
|
144
176
|
for (let i = 0; i < nums.length; i += paramCount) {
|
|
145
177
|
const args = nums.slice(i, i + paramCount);
|
|
146
178
|
if (args.length === paramCount) {
|
|
147
|
-
const effectiveCmd =
|
|
148
|
-
|
|
149
|
-
|
|
179
|
+
const effectiveCmd =
|
|
180
|
+
i > 0 && (cmd === "M" || cmd === "m")
|
|
181
|
+
? cmd === "M"
|
|
182
|
+
? "L"
|
|
183
|
+
: "l"
|
|
184
|
+
: cmd;
|
|
150
185
|
commands.push({ command: effectiveCmd, args });
|
|
151
186
|
} else if (args.length > 0) {
|
|
152
187
|
// BUG FIX #4: Warn when args are incomplete
|
|
153
|
-
console.warn(
|
|
188
|
+
console.warn(
|
|
189
|
+
`Incomplete ${cmd} command: expected ${paramCount} args, got ${args.length} - remaining args dropped`,
|
|
190
|
+
);
|
|
154
191
|
}
|
|
155
192
|
}
|
|
156
193
|
}
|
|
@@ -167,26 +204,26 @@ export function parsePath(d) {
|
|
|
167
204
|
*/
|
|
168
205
|
export function formatNumber(num, precision = 3) {
|
|
169
206
|
// BUG FIX #3: Handle NaN, Infinity, -Infinity
|
|
170
|
-
if (!isFinite(num)) return
|
|
171
|
-
if (num === 0) return
|
|
207
|
+
if (!isFinite(num)) return "0";
|
|
208
|
+
if (num === 0) return "0";
|
|
172
209
|
|
|
173
210
|
const factor = Math.pow(10, precision);
|
|
174
211
|
const rounded = Math.round(num * factor) / factor;
|
|
175
212
|
|
|
176
213
|
let str = rounded.toFixed(precision);
|
|
177
214
|
|
|
178
|
-
if (str.includes(
|
|
179
|
-
str = str.replace(/\.?0+$/,
|
|
215
|
+
if (str.includes(".")) {
|
|
216
|
+
str = str.replace(/\.?0+$/, "");
|
|
180
217
|
}
|
|
181
218
|
|
|
182
|
-
if (str.startsWith(
|
|
219
|
+
if (str.startsWith("0.")) {
|
|
183
220
|
str = str.substring(1);
|
|
184
|
-
} else if (str.startsWith(
|
|
185
|
-
str =
|
|
221
|
+
} else if (str.startsWith("-0.")) {
|
|
222
|
+
str = "-" + str.substring(2);
|
|
186
223
|
}
|
|
187
224
|
|
|
188
|
-
if (str ===
|
|
189
|
-
str =
|
|
225
|
+
if (str === "" || str === "." || str === "-.") {
|
|
226
|
+
str = "0";
|
|
190
227
|
}
|
|
191
228
|
|
|
192
229
|
return str;
|
|
@@ -204,19 +241,37 @@ export function toAbsolute(cmd, cx, cy) {
|
|
|
204
241
|
const absArgs = [...args];
|
|
205
242
|
|
|
206
243
|
switch (command) {
|
|
207
|
-
case
|
|
208
|
-
|
|
209
|
-
case
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
absArgs[
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
absArgs[
|
|
218
|
-
|
|
219
|
-
|
|
244
|
+
case "m":
|
|
245
|
+
case "l":
|
|
246
|
+
case "t":
|
|
247
|
+
absArgs[0] += cx;
|
|
248
|
+
absArgs[1] += cy;
|
|
249
|
+
break;
|
|
250
|
+
case "h":
|
|
251
|
+
absArgs[0] += cx;
|
|
252
|
+
break;
|
|
253
|
+
case "v":
|
|
254
|
+
absArgs[0] += cy;
|
|
255
|
+
break;
|
|
256
|
+
case "c":
|
|
257
|
+
absArgs[0] += cx;
|
|
258
|
+
absArgs[1] += cy;
|
|
259
|
+
absArgs[2] += cx;
|
|
260
|
+
absArgs[3] += cy;
|
|
261
|
+
absArgs[4] += cx;
|
|
262
|
+
absArgs[5] += cy;
|
|
263
|
+
break;
|
|
264
|
+
case "s":
|
|
265
|
+
case "q":
|
|
266
|
+
absArgs[0] += cx;
|
|
267
|
+
absArgs[1] += cy;
|
|
268
|
+
absArgs[2] += cx;
|
|
269
|
+
absArgs[3] += cy;
|
|
270
|
+
break;
|
|
271
|
+
case "a":
|
|
272
|
+
absArgs[5] += cx;
|
|
273
|
+
absArgs[6] += cy;
|
|
274
|
+
break;
|
|
220
275
|
}
|
|
221
276
|
|
|
222
277
|
return { command: absCmd, args: absArgs };
|
|
@@ -228,26 +283,44 @@ export function toAbsolute(cmd, cx, cy) {
|
|
|
228
283
|
export function toRelative(cmd, cx, cy) {
|
|
229
284
|
const { command, args } = cmd;
|
|
230
285
|
|
|
231
|
-
if (command === command.toLowerCase() && command !==
|
|
232
|
-
if (command ===
|
|
286
|
+
if (command === command.toLowerCase() && command !== "z") return cmd;
|
|
287
|
+
if (command === "Z" || command === "z") return { command: "z", args: [] };
|
|
233
288
|
|
|
234
289
|
const relCmd = command.toLowerCase();
|
|
235
290
|
const relArgs = [...args];
|
|
236
291
|
|
|
237
292
|
switch (command) {
|
|
238
|
-
case
|
|
239
|
-
|
|
240
|
-
case
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
relArgs[
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
relArgs[
|
|
249
|
-
|
|
250
|
-
|
|
293
|
+
case "M":
|
|
294
|
+
case "L":
|
|
295
|
+
case "T":
|
|
296
|
+
relArgs[0] -= cx;
|
|
297
|
+
relArgs[1] -= cy;
|
|
298
|
+
break;
|
|
299
|
+
case "H":
|
|
300
|
+
relArgs[0] -= cx;
|
|
301
|
+
break;
|
|
302
|
+
case "V":
|
|
303
|
+
relArgs[0] -= cy;
|
|
304
|
+
break;
|
|
305
|
+
case "C":
|
|
306
|
+
relArgs[0] -= cx;
|
|
307
|
+
relArgs[1] -= cy;
|
|
308
|
+
relArgs[2] -= cx;
|
|
309
|
+
relArgs[3] -= cy;
|
|
310
|
+
relArgs[4] -= cx;
|
|
311
|
+
relArgs[5] -= cy;
|
|
312
|
+
break;
|
|
313
|
+
case "S":
|
|
314
|
+
case "Q":
|
|
315
|
+
relArgs[0] -= cx;
|
|
316
|
+
relArgs[1] -= cy;
|
|
317
|
+
relArgs[2] -= cx;
|
|
318
|
+
relArgs[3] -= cy;
|
|
319
|
+
break;
|
|
320
|
+
case "A":
|
|
321
|
+
relArgs[5] -= cx;
|
|
322
|
+
relArgs[6] -= cy;
|
|
323
|
+
break;
|
|
251
324
|
}
|
|
252
325
|
|
|
253
326
|
return { command: relCmd, args: relArgs };
|
|
@@ -258,17 +331,21 @@ export function toRelative(cmd, cx, cy) {
|
|
|
258
331
|
*/
|
|
259
332
|
export function lineToHV(cmd, cx, cy, tolerance = 1e-6) {
|
|
260
333
|
const { command, args } = cmd;
|
|
261
|
-
if (command !==
|
|
334
|
+
if (command !== "L" && command !== "l") return null;
|
|
262
335
|
|
|
263
|
-
const isAbs = command ===
|
|
336
|
+
const isAbs = command === "L";
|
|
264
337
|
const endX = isAbs ? args[0] : cx + args[0];
|
|
265
338
|
const endY = isAbs ? args[1] : cy + args[1];
|
|
266
339
|
|
|
267
340
|
if (Math.abs(endY - cy) < tolerance) {
|
|
268
|
-
return isAbs
|
|
341
|
+
return isAbs
|
|
342
|
+
? { command: "H", args: [endX] }
|
|
343
|
+
: { command: "h", args: [endX - cx] };
|
|
269
344
|
}
|
|
270
345
|
if (Math.abs(endX - cx) < tolerance) {
|
|
271
|
-
return isAbs
|
|
346
|
+
return isAbs
|
|
347
|
+
? { command: "V", args: [endY] }
|
|
348
|
+
: { command: "v", args: [endY - cy] };
|
|
272
349
|
}
|
|
273
350
|
return null;
|
|
274
351
|
}
|
|
@@ -278,19 +355,31 @@ export function lineToHV(cmd, cx, cy, tolerance = 1e-6) {
|
|
|
278
355
|
*/
|
|
279
356
|
export function lineToZ(cmd, cx, cy, startX, startY, tolerance = 1e-6) {
|
|
280
357
|
const { command, args } = cmd;
|
|
281
|
-
if (command !==
|
|
358
|
+
if (command !== "L" && command !== "l") return false;
|
|
282
359
|
|
|
283
|
-
const isAbs = command ===
|
|
360
|
+
const isAbs = command === "L";
|
|
284
361
|
const endX = isAbs ? args[0] : cx + args[0];
|
|
285
362
|
const endY = isAbs ? args[1] : cy + args[1];
|
|
286
363
|
|
|
287
|
-
return
|
|
364
|
+
return (
|
|
365
|
+
Math.abs(endX - startX) < tolerance && Math.abs(endY - startY) < tolerance
|
|
366
|
+
);
|
|
288
367
|
}
|
|
289
368
|
|
|
290
369
|
/**
|
|
291
370
|
* Check if a cubic bezier is effectively a straight line.
|
|
292
371
|
*/
|
|
293
|
-
export function isCurveStraight(
|
|
372
|
+
export function isCurveStraight(
|
|
373
|
+
x0,
|
|
374
|
+
y0,
|
|
375
|
+
cp1x,
|
|
376
|
+
cp1y,
|
|
377
|
+
cp2x,
|
|
378
|
+
cp2y,
|
|
379
|
+
x3,
|
|
380
|
+
y3,
|
|
381
|
+
tolerance = 0.5,
|
|
382
|
+
) {
|
|
294
383
|
const chordLengthSq = (x3 - x0) ** 2 + (y3 - y0) ** 2;
|
|
295
384
|
|
|
296
385
|
if (chordLengthSq < 1e-10) {
|
|
@@ -300,8 +389,12 @@ export function isCurveStraight(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, toleranc
|
|
|
300
389
|
}
|
|
301
390
|
|
|
302
391
|
const chordLength = Math.sqrt(chordLengthSq);
|
|
303
|
-
const d1 =
|
|
304
|
-
|
|
392
|
+
const d1 =
|
|
393
|
+
Math.abs((y3 - y0) * cp1x - (x3 - x0) * cp1y + x3 * y0 - y3 * x0) /
|
|
394
|
+
chordLength;
|
|
395
|
+
const d2 =
|
|
396
|
+
Math.abs((y3 - y0) * cp2x - (x3 - x0) * cp2y + x3 * y0 - y3 * x0) /
|
|
397
|
+
chordLength;
|
|
305
398
|
|
|
306
399
|
return Math.max(d1, d2) < tolerance;
|
|
307
400
|
}
|
|
@@ -311,9 +404,9 @@ export function isCurveStraight(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, toleranc
|
|
|
311
404
|
*/
|
|
312
405
|
export function straightCurveToLine(cmd, cx, cy, tolerance = 0.5) {
|
|
313
406
|
const { command, args } = cmd;
|
|
314
|
-
if (command !==
|
|
407
|
+
if (command !== "C" && command !== "c") return null;
|
|
315
408
|
|
|
316
|
-
const isAbs = command ===
|
|
409
|
+
const isAbs = command === "C";
|
|
317
410
|
const cp1x = isAbs ? args[0] : cx + args[0];
|
|
318
411
|
const cp1y = isAbs ? args[1] : cy + args[1];
|
|
319
412
|
const cp2x = isAbs ? args[2] : cx + args[2];
|
|
@@ -323,8 +416,8 @@ export function straightCurveToLine(cmd, cx, cy, tolerance = 0.5) {
|
|
|
323
416
|
|
|
324
417
|
if (isCurveStraight(cx, cy, cp1x, cp1y, cp2x, cp2y, endX, endY, tolerance)) {
|
|
325
418
|
return isAbs
|
|
326
|
-
? { command:
|
|
327
|
-
: { command:
|
|
419
|
+
? { command: "L", args: [endX, endY] }
|
|
420
|
+
: { command: "l", args: [endX - cx, endY - cy] };
|
|
328
421
|
}
|
|
329
422
|
return null;
|
|
330
423
|
}
|
|
@@ -337,87 +430,93 @@ export function straightCurveToLine(cmd, cx, cy, tolerance = 0.5) {
|
|
|
337
430
|
* @param {boolean} prevLastArgHadDecimal - Whether previous command's last arg had a decimal point
|
|
338
431
|
* @returns {{str: string, lastArgHadDecimal: boolean}} Serialized string and decimal info for next command
|
|
339
432
|
*/
|
|
340
|
-
export function serializeCommand(
|
|
433
|
+
export function serializeCommand(
|
|
434
|
+
cmd,
|
|
435
|
+
prevCommand,
|
|
436
|
+
precision = 3,
|
|
437
|
+
prevLastArgHadDecimal = false,
|
|
438
|
+
) {
|
|
341
439
|
const { command, args } = cmd;
|
|
342
440
|
|
|
343
|
-
if (command ===
|
|
344
|
-
return { str:
|
|
441
|
+
if (command === "Z" || command === "z") {
|
|
442
|
+
return { str: "z", lastArgHadDecimal: false };
|
|
345
443
|
}
|
|
346
444
|
|
|
347
445
|
// SPECIAL HANDLING FOR ARC COMMANDS
|
|
348
446
|
// Arc format: rx ry rotation large-arc-flag sweep-flag x y
|
|
349
447
|
// Per SVG spec: flags MUST be exactly 0 or 1, arc commands CANNOT be implicitly repeated
|
|
350
|
-
if (command ===
|
|
448
|
+
if (command === "A" || command === "a") {
|
|
351
449
|
const arcArgs = [
|
|
352
|
-
formatNumber(args[0], precision),
|
|
353
|
-
formatNumber(args[1], precision),
|
|
354
|
-
formatNumber(args[2], precision),
|
|
355
|
-
args[3] ?
|
|
356
|
-
args[4] ?
|
|
357
|
-
formatNumber(args[5], precision),
|
|
358
|
-
formatNumber(args[6], precision)
|
|
359
|
-
].join(
|
|
450
|
+
formatNumber(args[0], precision), // rx
|
|
451
|
+
formatNumber(args[1], precision), // ry
|
|
452
|
+
formatNumber(args[2], precision), // rotation
|
|
453
|
+
args[3] ? "1" : "0", // large-arc-flag (FORCE 0/1)
|
|
454
|
+
args[4] ? "1" : "0", // sweep-flag (FORCE 0/1)
|
|
455
|
+
formatNumber(args[5], precision), // x
|
|
456
|
+
formatNumber(args[6], precision), // y
|
|
457
|
+
].join(" "); // ALWAYS use space delimiters for arcs to avoid invalid double-decimals
|
|
360
458
|
|
|
361
459
|
// Arc commands CANNOT be implicitly repeated - always include command letter
|
|
362
460
|
return {
|
|
363
461
|
str: command + arcArgs,
|
|
364
|
-
lastArgHadDecimal: arcArgs.includes(
|
|
462
|
+
lastArgHadDecimal: arcArgs.includes("."),
|
|
365
463
|
};
|
|
366
464
|
}
|
|
367
465
|
|
|
368
|
-
const formattedArgs = args.map(n => formatNumber(n, precision));
|
|
466
|
+
const formattedArgs = args.map((n) => formatNumber(n, precision));
|
|
369
467
|
|
|
370
|
-
let argsStr =
|
|
468
|
+
let argsStr = "";
|
|
371
469
|
for (let i = 0; i < formattedArgs.length; i++) {
|
|
372
470
|
const arg = formattedArgs[i];
|
|
373
471
|
if (i === 0) {
|
|
374
472
|
argsStr = arg;
|
|
375
473
|
} else {
|
|
376
474
|
const prevArg = formattedArgs[i - 1];
|
|
377
|
-
const prevHasDecimal = prevArg.includes(
|
|
475
|
+
const prevHasDecimal = prevArg.includes(".");
|
|
378
476
|
|
|
379
|
-
if (arg.startsWith(
|
|
477
|
+
if (arg.startsWith("-")) {
|
|
380
478
|
// Negative sign is always a valid delimiter
|
|
381
479
|
argsStr += arg;
|
|
382
|
-
} else if (arg.startsWith(
|
|
480
|
+
} else if (arg.startsWith(".") && prevHasDecimal) {
|
|
383
481
|
// Decimal point works as delimiter only if prev already has a decimal
|
|
384
482
|
// e.g., "-2.5" + ".3" = "-2.5.3" parses as -2.5 and .3 (number can't have two decimals)
|
|
385
483
|
// but "0" + ".3" = "0.3" would merge into single number 0.3!
|
|
386
484
|
argsStr += arg;
|
|
387
485
|
} else {
|
|
388
486
|
// Need space delimiter
|
|
389
|
-
argsStr +=
|
|
487
|
+
argsStr += " " + arg;
|
|
390
488
|
}
|
|
391
489
|
}
|
|
392
490
|
}
|
|
393
491
|
|
|
394
492
|
// Track whether our last arg has a decimal (for next command's delimiter decision)
|
|
395
|
-
const lastArg = formattedArgs[formattedArgs.length - 1] ||
|
|
396
|
-
const thisLastArgHadDecimal = lastArg.includes(
|
|
493
|
+
const lastArg = formattedArgs[formattedArgs.length - 1] || "";
|
|
494
|
+
const thisLastArgHadDecimal = lastArg.includes(".");
|
|
397
495
|
|
|
398
496
|
// Decide if we need command letter or can use implicit continuation
|
|
399
497
|
// Arc commands CANNOT be implicitly repeated per SVG spec
|
|
400
498
|
let cmdStr = command;
|
|
401
|
-
if (prevCommand ===
|
|
402
|
-
else if (prevCommand ===
|
|
403
|
-
else if (prevCommand === command && command !==
|
|
499
|
+
if (prevCommand === "M" && command === "L") cmdStr = "";
|
|
500
|
+
else if (prevCommand === "m" && command === "l") cmdStr = "";
|
|
501
|
+
else if (prevCommand === command && command !== "A" && command !== "a")
|
|
502
|
+
cmdStr = "";
|
|
404
503
|
|
|
405
504
|
if (cmdStr) {
|
|
406
505
|
// Explicit command letter - always safe, no delimiter needed
|
|
407
506
|
return { str: cmdStr + argsStr, lastArgHadDecimal: thisLastArgHadDecimal };
|
|
408
507
|
} else {
|
|
409
508
|
// Implicit command - need to check delimiter between commands
|
|
410
|
-
const firstArg = formattedArgs[0] ||
|
|
509
|
+
const firstArg = formattedArgs[0] || "";
|
|
411
510
|
|
|
412
|
-
if (firstArg.startsWith(
|
|
511
|
+
if (firstArg.startsWith("-")) {
|
|
413
512
|
// Negative sign always works as delimiter
|
|
414
513
|
return { str: argsStr, lastArgHadDecimal: thisLastArgHadDecimal };
|
|
415
|
-
} else if (firstArg.startsWith(
|
|
514
|
+
} else if (firstArg.startsWith(".") && prevLastArgHadDecimal) {
|
|
416
515
|
// Decimal point works only if prev command's last arg already had a decimal
|
|
417
516
|
return { str: argsStr, lastArgHadDecimal: thisLastArgHadDecimal };
|
|
418
517
|
} else {
|
|
419
518
|
// Need space delimiter
|
|
420
|
-
return { str:
|
|
519
|
+
return { str: " " + argsStr, lastArgHadDecimal: thisLastArgHadDecimal };
|
|
421
520
|
}
|
|
422
521
|
}
|
|
423
522
|
}
|
|
@@ -426,14 +525,19 @@ export function serializeCommand(cmd, prevCommand, precision = 3, prevLastArgHad
|
|
|
426
525
|
* Serialize a complete path to minimal string.
|
|
427
526
|
*/
|
|
428
527
|
export function serializePath(commands, precision = 3) {
|
|
429
|
-
if (commands.length === 0) return
|
|
528
|
+
if (commands.length === 0) return "";
|
|
430
529
|
|
|
431
|
-
let result =
|
|
530
|
+
let result = "";
|
|
432
531
|
let prevCommand = null;
|
|
433
532
|
let prevLastArgHadDecimal = false;
|
|
434
533
|
|
|
435
534
|
for (const cmd of commands) {
|
|
436
|
-
const { str, lastArgHadDecimal } = serializeCommand(
|
|
535
|
+
const { str, lastArgHadDecimal } = serializeCommand(
|
|
536
|
+
cmd,
|
|
537
|
+
prevCommand,
|
|
538
|
+
precision,
|
|
539
|
+
prevLastArgHadDecimal,
|
|
540
|
+
);
|
|
437
541
|
result += str;
|
|
438
542
|
prevCommand = cmd.command;
|
|
439
543
|
prevLastArgHadDecimal = lastArgHadDecimal;
|
|
@@ -455,17 +559,20 @@ export function convertPathData(d, options = {}) {
|
|
|
455
559
|
lineShorthands = true,
|
|
456
560
|
convertToZ = true,
|
|
457
561
|
utilizeAbsolute = true,
|
|
458
|
-
straightTolerance = 0.5
|
|
562
|
+
straightTolerance = 0.5,
|
|
459
563
|
} = options;
|
|
460
564
|
|
|
461
565
|
const originalLength = d.length;
|
|
462
|
-
|
|
566
|
+
const commands = parsePath(d);
|
|
463
567
|
|
|
464
568
|
if (commands.length === 0) {
|
|
465
569
|
return { d, originalLength, optimizedLength: originalLength, savings: 0 };
|
|
466
570
|
}
|
|
467
571
|
|
|
468
|
-
let cx = 0,
|
|
572
|
+
let cx = 0,
|
|
573
|
+
cy = 0,
|
|
574
|
+
startX = 0,
|
|
575
|
+
startY = 0;
|
|
469
576
|
const optimized = [];
|
|
470
577
|
|
|
471
578
|
for (let i = 0; i < commands.length; i++) {
|
|
@@ -473,36 +580,39 @@ export function convertPathData(d, options = {}) {
|
|
|
473
580
|
|
|
474
581
|
// BUG FIX #3: Convert zero arc radii to line per SVG spec Section 8.3.4
|
|
475
582
|
// When rx or ry is 0, the arc degenerates to a straight line
|
|
476
|
-
if (
|
|
477
|
-
|
|
583
|
+
if (
|
|
584
|
+
(cmd.command === "A" || cmd.command === "a") &&
|
|
585
|
+
(cmd.args[0] === 0 || cmd.args[1] === 0)
|
|
586
|
+
) {
|
|
587
|
+
const isAbs = cmd.command === "A";
|
|
478
588
|
const endX = cmd.args[5];
|
|
479
589
|
const endY = cmd.args[6];
|
|
480
590
|
cmd = isAbs
|
|
481
|
-
? { command:
|
|
482
|
-
: { command:
|
|
591
|
+
? { command: "L", args: [endX, endY] }
|
|
592
|
+
: { command: "l", args: [endX, endY] };
|
|
483
593
|
}
|
|
484
594
|
|
|
485
595
|
// 1. Straight curve to line
|
|
486
|
-
if (straightCurves && (cmd.command ===
|
|
596
|
+
if (straightCurves && (cmd.command === "C" || cmd.command === "c")) {
|
|
487
597
|
const lineCmd = straightCurveToLine(cmd, cx, cy, straightTolerance);
|
|
488
598
|
if (lineCmd) cmd = lineCmd;
|
|
489
599
|
}
|
|
490
600
|
|
|
491
601
|
// 2. Line shorthands (L -> H/V)
|
|
492
|
-
if (lineShorthands && (cmd.command ===
|
|
602
|
+
if (lineShorthands && (cmd.command === "L" || cmd.command === "l")) {
|
|
493
603
|
const hvCmd = lineToHV(cmd, cx, cy);
|
|
494
604
|
if (hvCmd) cmd = hvCmd;
|
|
495
605
|
}
|
|
496
606
|
|
|
497
607
|
// 3. Line to Z
|
|
498
|
-
if (convertToZ && (cmd.command ===
|
|
608
|
+
if (convertToZ && (cmd.command === "L" || cmd.command === "l")) {
|
|
499
609
|
if (lineToZ(cmd, cx, cy, startX, startY)) {
|
|
500
|
-
cmd = { command:
|
|
610
|
+
cmd = { command: "z", args: [] };
|
|
501
611
|
}
|
|
502
612
|
}
|
|
503
613
|
|
|
504
614
|
// 4. Choose shorter form (absolute vs relative)
|
|
505
|
-
if (utilizeAbsolute && cmd.command !==
|
|
615
|
+
if (utilizeAbsolute && cmd.command !== "Z" && cmd.command !== "z") {
|
|
506
616
|
const abs = toAbsolute(cmd, cx, cy);
|
|
507
617
|
const rel = toRelative(cmd, cx, cy);
|
|
508
618
|
// Bug fix: serializeCommand returns {str, lastArgHadDecimal} object,
|
|
@@ -517,14 +627,40 @@ export function convertPathData(d, options = {}) {
|
|
|
517
627
|
// Update position
|
|
518
628
|
const finalCmd = toAbsolute(cmd, cx, cy);
|
|
519
629
|
switch (finalCmd.command) {
|
|
520
|
-
case
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
case
|
|
527
|
-
case
|
|
630
|
+
case "M":
|
|
631
|
+
cx = finalCmd.args[0];
|
|
632
|
+
cy = finalCmd.args[1];
|
|
633
|
+
startX = cx;
|
|
634
|
+
startY = cy;
|
|
635
|
+
break;
|
|
636
|
+
case "L":
|
|
637
|
+
case "T":
|
|
638
|
+
cx = finalCmd.args[0];
|
|
639
|
+
cy = finalCmd.args[1];
|
|
640
|
+
break;
|
|
641
|
+
case "H":
|
|
642
|
+
cx = finalCmd.args[0];
|
|
643
|
+
break;
|
|
644
|
+
case "V":
|
|
645
|
+
cy = finalCmd.args[0];
|
|
646
|
+
break;
|
|
647
|
+
case "C":
|
|
648
|
+
cx = finalCmd.args[4];
|
|
649
|
+
cy = finalCmd.args[5];
|
|
650
|
+
break;
|
|
651
|
+
case "S":
|
|
652
|
+
case "Q":
|
|
653
|
+
cx = finalCmd.args[2];
|
|
654
|
+
cy = finalCmd.args[3];
|
|
655
|
+
break;
|
|
656
|
+
case "A":
|
|
657
|
+
cx = finalCmd.args[5];
|
|
658
|
+
cy = finalCmd.args[6];
|
|
659
|
+
break;
|
|
660
|
+
case "Z":
|
|
661
|
+
cx = startX;
|
|
662
|
+
cy = startY;
|
|
663
|
+
break;
|
|
528
664
|
}
|
|
529
665
|
}
|
|
530
666
|
|
|
@@ -534,7 +670,7 @@ export function convertPathData(d, options = {}) {
|
|
|
534
670
|
d: optimizedD,
|
|
535
671
|
originalLength,
|
|
536
672
|
optimizedLength: optimizedD.length,
|
|
537
|
-
savings: originalLength - optimizedD.length
|
|
673
|
+
savings: originalLength - optimizedD.length,
|
|
538
674
|
};
|
|
539
675
|
}
|
|
540
676
|
|
|
@@ -549,19 +685,19 @@ export function optimizeDocumentPaths(root, options = {}) {
|
|
|
549
685
|
const processElement = (el) => {
|
|
550
686
|
const tagName = el.tagName?.toLowerCase();
|
|
551
687
|
|
|
552
|
-
if (tagName ===
|
|
553
|
-
const d = el.getAttribute(
|
|
688
|
+
if (tagName === "path") {
|
|
689
|
+
const d = el.getAttribute("d");
|
|
554
690
|
if (d) {
|
|
555
691
|
const result = convertPathData(d, options);
|
|
556
692
|
if (result.savings > 0) {
|
|
557
|
-
el.setAttribute(
|
|
693
|
+
el.setAttribute("d", result.d);
|
|
558
694
|
pathsOptimized++;
|
|
559
695
|
totalSavings += result.savings;
|
|
560
696
|
details.push({
|
|
561
|
-
id: el.getAttribute(
|
|
697
|
+
id: el.getAttribute("id") || null,
|
|
562
698
|
originalLength: result.originalLength,
|
|
563
699
|
optimizedLength: result.optimizedLength,
|
|
564
|
-
savings: result.savings
|
|
700
|
+
savings: result.savings,
|
|
565
701
|
});
|
|
566
702
|
}
|
|
567
703
|
}
|
|
@@ -577,7 +713,16 @@ export function optimizeDocumentPaths(root, options = {}) {
|
|
|
577
713
|
}
|
|
578
714
|
|
|
579
715
|
export default {
|
|
580
|
-
parsePath,
|
|
581
|
-
|
|
582
|
-
|
|
716
|
+
parsePath,
|
|
717
|
+
formatNumber,
|
|
718
|
+
toAbsolute,
|
|
719
|
+
toRelative,
|
|
720
|
+
lineToHV,
|
|
721
|
+
lineToZ,
|
|
722
|
+
isCurveStraight,
|
|
723
|
+
straightCurveToLine,
|
|
724
|
+
serializeCommand,
|
|
725
|
+
serializePath,
|
|
726
|
+
convertPathData,
|
|
727
|
+
optimizeDocumentPaths,
|
|
583
728
|
};
|