@emasoft/svg-matrix 1.1.0 → 1.2.0
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/bin/svg-matrix.js +7 -6
- package/bin/svgm.js +109 -40
- package/dist/svg-matrix.min.js +7 -7
- package/dist/svg-toolbox.min.js +148 -228
- package/dist/svgm.min.js +152 -232
- package/dist/version.json +5 -5
- package/package.json +1 -1
- package/scripts/postinstall.js +72 -41
- package/scripts/test-postinstall.js +18 -16
- package/scripts/version-sync.js +78 -60
- package/src/animation-optimization.js +190 -98
- package/src/animation-references.js +11 -3
- package/src/arc-length.js +23 -20
- package/src/bezier-analysis.js +9 -13
- package/src/bezier-intersections.js +18 -4
- package/src/browser-verify.js +35 -8
- package/src/clip-path-resolver.js +285 -114
- package/src/convert-path-data.js +20 -8
- package/src/css-specificity.js +33 -9
- package/src/douglas-peucker.js +272 -141
- package/src/geometry-to-path.js +79 -22
- package/src/gjk-collision.js +287 -126
- package/src/index.js +56 -21
- package/src/inkscape-support.js +122 -101
- package/src/logger.js +43 -27
- package/src/marker-resolver.js +201 -121
- package/src/mask-resolver.js +231 -98
- package/src/matrix.js +9 -5
- package/src/mesh-gradient.js +22 -14
- package/src/off-canvas-detection.js +53 -17
- package/src/path-optimization.js +356 -171
- package/src/path-simplification.js +671 -256
- package/src/pattern-resolver.js +1 -3
- package/src/polygon-clip.js +396 -78
- package/src/svg-boolean-ops.js +90 -23
- package/src/svg-collections.js +1546 -667
- package/src/svg-flatten.js +152 -38
- package/src/svg-matrix-lib.js +2 -2
- package/src/svg-parser.js +5 -1
- package/src/svg-rendering-context.js +3 -1
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svg-toolbox.js +99 -457
- package/src/svg-validation-data.js +513 -345
- package/src/svg2-polyfills.js +156 -93
- package/src/svgm-lib.js +8 -4
- package/src/transform-optimization.js +168 -51
- package/src/transforms2d.js +73 -40
- package/src/transforms3d.js +34 -27
- package/src/use-symbol-resolver.js +175 -76
- package/src/vector.js +80 -44
- package/src/vendor/inkscape-hatch-polyfill.js +143 -108
- package/src/vendor/inkscape-hatch-polyfill.min.js +291 -1
- package/src/vendor/inkscape-mesh-polyfill.js +953 -766
- package/src/vendor/inkscape-mesh-polyfill.min.js +896 -1
- package/src/verification.js +3 -4
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* @module animation-optimization
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import Decimal from
|
|
23
|
+
import Decimal from "decimal.js";
|
|
24
24
|
|
|
25
25
|
// Configure Decimal for high precision internally
|
|
26
26
|
Decimal.set({ precision: 20, rounding: Decimal.ROUND_HALF_UP });
|
|
@@ -32,9 +32,9 @@ Decimal.set({ precision: 20, rounding: Decimal.ROUND_HALF_UP });
|
|
|
32
32
|
export const STANDARD_EASINGS = {
|
|
33
33
|
linear: [0, 0, 1, 1],
|
|
34
34
|
ease: [0.25, 0.1, 0.25, 1],
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
"ease-in": [0.42, 0, 1, 1],
|
|
36
|
+
"ease-out": [0, 0, 0.58, 1],
|
|
37
|
+
"ease-in-out": [0.42, 0, 0.58, 1],
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -46,18 +46,26 @@ export const STANDARD_EASINGS = {
|
|
|
46
46
|
export function formatSplineValue(value, precision = 3) {
|
|
47
47
|
// Validate value parameter
|
|
48
48
|
if (value === null || value === undefined) {
|
|
49
|
-
throw new Error(
|
|
49
|
+
throw new Error("formatSplineValue: value parameter is required");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Validate precision parameter
|
|
53
|
-
if (
|
|
54
|
-
|
|
53
|
+
if (
|
|
54
|
+
typeof precision !== "number" ||
|
|
55
|
+
precision < 0 ||
|
|
56
|
+
!Number.isFinite(precision)
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"formatSplineValue: precision must be a non-negative finite number",
|
|
60
|
+
);
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
// Check for NaN/Infinity before creating Decimal
|
|
58
|
-
const numValue = typeof value ===
|
|
64
|
+
const numValue = typeof value === "number" ? value : parseFloat(value);
|
|
59
65
|
if (!Number.isFinite(numValue)) {
|
|
60
|
-
throw new Error(
|
|
66
|
+
throw new Error(
|
|
67
|
+
`formatSplineValue: value must be a finite number, got ${value}`,
|
|
68
|
+
);
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
const num = new Decimal(value);
|
|
@@ -69,19 +77,19 @@ export function formatSplineValue(value, precision = 3) {
|
|
|
69
77
|
let str = rounded.toString();
|
|
70
78
|
|
|
71
79
|
// Remove trailing zeros after decimal point
|
|
72
|
-
if (str.includes(
|
|
73
|
-
str = str.replace(/\.?0+$/,
|
|
80
|
+
if (str.includes(".")) {
|
|
81
|
+
str = str.replace(/\.?0+$/, "");
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
// Remove leading zero for values between -1 and 1 (exclusive)
|
|
77
|
-
if (str.startsWith(
|
|
85
|
+
if (str.startsWith("0.")) {
|
|
78
86
|
str = str.substring(1); // "0.5" -> ".5"
|
|
79
|
-
} else if (str.startsWith(
|
|
80
|
-
str =
|
|
87
|
+
} else if (str.startsWith("-0.")) {
|
|
88
|
+
str = "-" + str.substring(2); // "-0.5" -> "-.5"
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
// Handle edge cases: ".0" should be "0", "-0" should be "0"
|
|
84
|
-
if (str ===
|
|
92
|
+
if (str === "" || str === "." || str === "-0") str = "0";
|
|
85
93
|
|
|
86
94
|
return str;
|
|
87
95
|
}
|
|
@@ -93,25 +101,30 @@ export function formatSplineValue(value, precision = 3) {
|
|
|
93
101
|
* @returns {number[][]} Array of [x1, y1, x2, y2] arrays
|
|
94
102
|
*/
|
|
95
103
|
export function parseKeySplines(keySplines) {
|
|
96
|
-
if (!keySplines || typeof keySplines !==
|
|
104
|
+
if (!keySplines || typeof keySplines !== "string") return [];
|
|
97
105
|
|
|
98
106
|
// Split by semicolon to get individual splines
|
|
99
|
-
const splines = keySplines
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
const splines = keySplines
|
|
108
|
+
.split(";")
|
|
109
|
+
.map((s) => s.trim())
|
|
110
|
+
.filter((s) => s);
|
|
111
|
+
|
|
112
|
+
return splines.map((spline) => {
|
|
113
|
+
// Split by whitespace or comma to get control points
|
|
114
|
+
const values = spline
|
|
115
|
+
.split(/[\s,]+/)
|
|
116
|
+
.map((v) => parseFloat(v))
|
|
117
|
+
.filter((v) => Number.isFinite(v)); // Filter out NaN and Infinity
|
|
118
|
+
|
|
119
|
+
// Each spline must have exactly 4 control points
|
|
120
|
+
if (values.length !== 4) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`parseKeySplines: invalid spline "${spline}", expected 4 values, got ${values.length}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
112
125
|
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
return values;
|
|
127
|
+
});
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
/**
|
|
@@ -123,25 +136,37 @@ export function parseKeySplines(keySplines) {
|
|
|
123
136
|
export function serializeKeySplines(splines, precision = 3) {
|
|
124
137
|
// Validate splines parameter
|
|
125
138
|
if (!Array.isArray(splines)) {
|
|
126
|
-
throw new Error(
|
|
139
|
+
throw new Error("serializeKeySplines: splines must be an array");
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
// Validate precision parameter
|
|
130
|
-
if (
|
|
131
|
-
|
|
143
|
+
if (
|
|
144
|
+
typeof precision !== "number" ||
|
|
145
|
+
precision < 0 ||
|
|
146
|
+
!Number.isFinite(precision)
|
|
147
|
+
) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
"serializeKeySplines: precision must be a non-negative finite number",
|
|
150
|
+
);
|
|
132
151
|
}
|
|
133
152
|
|
|
134
|
-
return splines
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
return splines
|
|
154
|
+
.map((spline, index) => {
|
|
155
|
+
// Validate each spline is an array with 4 values
|
|
156
|
+
if (!Array.isArray(spline)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`serializeKeySplines: spline at index ${index} must be an array`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (spline.length !== 4) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`serializeKeySplines: spline at index ${index} must have exactly 4 values, got ${spline.length}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
142
166
|
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
return spline.map((v) => formatSplineValue(v, precision)).join(" ");
|
|
168
|
+
})
|
|
169
|
+
.join("; ");
|
|
145
170
|
}
|
|
146
171
|
|
|
147
172
|
/**
|
|
@@ -155,14 +180,25 @@ export function isLinearSpline(spline, tolerance = 0.001) {
|
|
|
155
180
|
if (!Array.isArray(spline) || spline.length !== 4) return false;
|
|
156
181
|
|
|
157
182
|
// Validate tolerance parameter
|
|
158
|
-
if (
|
|
159
|
-
|
|
183
|
+
if (
|
|
184
|
+
typeof tolerance !== "number" ||
|
|
185
|
+
tolerance < 0 ||
|
|
186
|
+
!Number.isFinite(tolerance)
|
|
187
|
+
) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
"isLinearSpline: tolerance must be a non-negative finite number",
|
|
190
|
+
);
|
|
160
191
|
}
|
|
161
192
|
|
|
162
193
|
const [x1, y1, x2, y2] = spline;
|
|
163
194
|
|
|
164
195
|
// Check for NaN values in spline
|
|
165
|
-
if (
|
|
196
|
+
if (
|
|
197
|
+
!Number.isFinite(x1) ||
|
|
198
|
+
!Number.isFinite(y1) ||
|
|
199
|
+
!Number.isFinite(x2) ||
|
|
200
|
+
!Number.isFinite(y2)
|
|
201
|
+
) {
|
|
166
202
|
return false;
|
|
167
203
|
}
|
|
168
204
|
|
|
@@ -183,7 +219,7 @@ export function areAllSplinesLinear(keySplines) {
|
|
|
183
219
|
const splines = parseKeySplines(keySplines);
|
|
184
220
|
if (splines.length === 0) return false;
|
|
185
221
|
// Must wrap in arrow function to avoid .every() passing index as tolerance
|
|
186
|
-
return splines.every(s => isLinearSpline(s));
|
|
222
|
+
return splines.every((s) => isLinearSpline(s));
|
|
187
223
|
}
|
|
188
224
|
|
|
189
225
|
/**
|
|
@@ -197,15 +233,23 @@ export function identifyStandardEasing(spline, tolerance = 0.01) {
|
|
|
197
233
|
if (!Array.isArray(spline) || spline.length !== 4) return null;
|
|
198
234
|
|
|
199
235
|
// Validate tolerance parameter
|
|
200
|
-
if (
|
|
201
|
-
|
|
236
|
+
if (
|
|
237
|
+
typeof tolerance !== "number" ||
|
|
238
|
+
tolerance < 0 ||
|
|
239
|
+
!Number.isFinite(tolerance)
|
|
240
|
+
) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
"identifyStandardEasing: tolerance must be a non-negative finite number",
|
|
243
|
+
);
|
|
202
244
|
}
|
|
203
245
|
|
|
204
246
|
// Check for NaN values in spline
|
|
205
|
-
if (!spline.every(val => Number.isFinite(val))) return null;
|
|
247
|
+
if (!spline.every((val) => Number.isFinite(val))) return null;
|
|
206
248
|
|
|
207
249
|
for (const [name, standard] of Object.entries(STANDARD_EASINGS)) {
|
|
208
|
-
const matches = spline.every(
|
|
250
|
+
const matches = spline.every(
|
|
251
|
+
(val, i) => Math.abs(val - standard[i]) < tolerance,
|
|
252
|
+
);
|
|
209
253
|
if (matches) return name;
|
|
210
254
|
}
|
|
211
255
|
return null;
|
|
@@ -230,10 +274,12 @@ export function optimizeKeySplines(keySplines, options = {}) {
|
|
|
230
274
|
}
|
|
231
275
|
|
|
232
276
|
// Must wrap in arrow function to avoid .every() passing index as tolerance
|
|
233
|
-
const allLinear = splines.every(s => isLinearSpline(s));
|
|
277
|
+
const allLinear = splines.every((s) => isLinearSpline(s));
|
|
234
278
|
|
|
235
279
|
// Identify standard easings
|
|
236
|
-
const standardEasings = splines
|
|
280
|
+
const standardEasings = splines
|
|
281
|
+
.map((s) => identifyStandardEasing(s))
|
|
282
|
+
.filter(Boolean);
|
|
237
283
|
|
|
238
284
|
// If all linear and removeLinear is true, suggest removing keySplines
|
|
239
285
|
if (allLinear && removeLinear) {
|
|
@@ -252,8 +298,11 @@ export function optimizeKeySplines(keySplines, options = {}) {
|
|
|
252
298
|
* @returns {number[]} Array of time values
|
|
253
299
|
*/
|
|
254
300
|
export function parseKeyTimes(keyTimes) {
|
|
255
|
-
if (!keyTimes || typeof keyTimes !==
|
|
256
|
-
return keyTimes
|
|
301
|
+
if (!keyTimes || typeof keyTimes !== "string") return [];
|
|
302
|
+
return keyTimes
|
|
303
|
+
.split(";")
|
|
304
|
+
.map((s) => parseFloat(s.trim()))
|
|
305
|
+
.filter((v) => Number.isFinite(v));
|
|
257
306
|
}
|
|
258
307
|
|
|
259
308
|
/**
|
|
@@ -265,18 +314,24 @@ export function parseKeyTimes(keyTimes) {
|
|
|
265
314
|
export function serializeKeyTimes(times, precision = 3) {
|
|
266
315
|
// Validate times parameter
|
|
267
316
|
if (!Array.isArray(times)) {
|
|
268
|
-
throw new Error(
|
|
317
|
+
throw new Error("serializeKeyTimes: times must be an array");
|
|
269
318
|
}
|
|
270
319
|
|
|
271
320
|
// Validate precision parameter
|
|
272
|
-
if (
|
|
273
|
-
|
|
321
|
+
if (
|
|
322
|
+
typeof precision !== "number" ||
|
|
323
|
+
precision < 0 ||
|
|
324
|
+
!Number.isFinite(precision)
|
|
325
|
+
) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
"serializeKeyTimes: precision must be a non-negative finite number",
|
|
328
|
+
);
|
|
274
329
|
}
|
|
275
330
|
|
|
276
331
|
// Return empty string for empty array
|
|
277
|
-
if (times.length === 0) return
|
|
332
|
+
if (times.length === 0) return "";
|
|
278
333
|
|
|
279
|
-
return times.map(t => formatSplineValue(t, precision)).join(
|
|
334
|
+
return times.map((t) => formatSplineValue(t, precision)).join("; ");
|
|
280
335
|
}
|
|
281
336
|
|
|
282
337
|
/**
|
|
@@ -288,12 +343,18 @@ export function serializeKeyTimes(times, precision = 3) {
|
|
|
288
343
|
export function optimizeKeyTimes(keyTimes, precision = 3) {
|
|
289
344
|
// Validate keyTimes parameter
|
|
290
345
|
if (keyTimes === null || keyTimes === undefined) {
|
|
291
|
-
throw new Error(
|
|
346
|
+
throw new Error("optimizeKeyTimes: keyTimes parameter is required");
|
|
292
347
|
}
|
|
293
348
|
|
|
294
349
|
// Validate precision parameter
|
|
295
|
-
if (
|
|
296
|
-
|
|
350
|
+
if (
|
|
351
|
+
typeof precision !== "number" ||
|
|
352
|
+
precision < 0 ||
|
|
353
|
+
!Number.isFinite(precision)
|
|
354
|
+
) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
"optimizeKeyTimes: precision must be a non-negative finite number",
|
|
357
|
+
);
|
|
297
358
|
}
|
|
298
359
|
|
|
299
360
|
const times = parseKeyTimes(keyTimes);
|
|
@@ -309,39 +370,45 @@ export function optimizeKeyTimes(keyTimes, precision = 3) {
|
|
|
309
370
|
* @returns {string} Optimized values
|
|
310
371
|
*/
|
|
311
372
|
export function optimizeAnimationValues(values, precision = 3) {
|
|
312
|
-
if (!values || typeof values !==
|
|
373
|
+
if (!values || typeof values !== "string") return values;
|
|
313
374
|
|
|
314
375
|
// Validate precision parameter
|
|
315
|
-
if (
|
|
316
|
-
|
|
376
|
+
if (
|
|
377
|
+
typeof precision !== "number" ||
|
|
378
|
+
precision < 0 ||
|
|
379
|
+
!Number.isFinite(precision)
|
|
380
|
+
) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
"optimizeAnimationValues: precision must be a non-negative finite number",
|
|
383
|
+
);
|
|
317
384
|
}
|
|
318
385
|
|
|
319
386
|
// Split by semicolon
|
|
320
|
-
const parts = values.split(
|
|
387
|
+
const parts = values.split(";");
|
|
321
388
|
|
|
322
389
|
// Handle empty values string
|
|
323
390
|
if (parts.length === 0) return values;
|
|
324
391
|
|
|
325
|
-
const optimized = parts.map(part => {
|
|
392
|
+
const optimized = parts.map((part) => {
|
|
326
393
|
const trimmed = part.trim();
|
|
327
394
|
|
|
328
395
|
// Preserve ID references exactly
|
|
329
|
-
if (trimmed.startsWith(
|
|
396
|
+
if (trimmed.startsWith("#") || trimmed.includes("url(")) {
|
|
330
397
|
return trimmed;
|
|
331
398
|
}
|
|
332
399
|
|
|
333
400
|
// Try to parse as numbers (could be space-separated like "0 0" for translate)
|
|
334
|
-
const nums = trimmed.split(/[\s,]+/).filter(n => n); // Filter empty strings
|
|
335
|
-
const optimizedNums = nums.map(n => {
|
|
401
|
+
const nums = trimmed.split(/[\s,]+/).filter((n) => n); // Filter empty strings
|
|
402
|
+
const optimizedNums = nums.map((n) => {
|
|
336
403
|
const num = parseFloat(n);
|
|
337
404
|
if (!Number.isFinite(num)) return n; // Not a finite number, preserve as-is
|
|
338
405
|
return formatSplineValue(num, precision);
|
|
339
406
|
});
|
|
340
407
|
|
|
341
|
-
return optimizedNums.join(
|
|
408
|
+
return optimizedNums.join(" ");
|
|
342
409
|
});
|
|
343
410
|
|
|
344
|
-
return optimized.join(
|
|
411
|
+
return optimized.join("; ");
|
|
345
412
|
}
|
|
346
413
|
|
|
347
414
|
/**
|
|
@@ -352,11 +419,19 @@ export function optimizeAnimationValues(values, precision = 3) {
|
|
|
352
419
|
*/
|
|
353
420
|
export function optimizeElementTiming(el, options = {}) {
|
|
354
421
|
// Validate el parameter
|
|
355
|
-
if (!el || typeof el !==
|
|
356
|
-
throw new Error(
|
|
422
|
+
if (!el || typeof el !== "object") {
|
|
423
|
+
throw new Error(
|
|
424
|
+
"optimizeElementTiming: el parameter must be a valid element",
|
|
425
|
+
);
|
|
357
426
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
427
|
+
if (
|
|
428
|
+
typeof el.getAttribute !== "function" ||
|
|
429
|
+
typeof el.setAttribute !== "function" ||
|
|
430
|
+
typeof el.removeAttribute !== "function"
|
|
431
|
+
) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
"optimizeElementTiming: el parameter must have getAttribute, setAttribute, and removeAttribute methods",
|
|
434
|
+
);
|
|
360
435
|
}
|
|
361
436
|
|
|
362
437
|
const precision = options.precision ?? 3;
|
|
@@ -364,40 +439,49 @@ export function optimizeElementTiming(el, options = {}) {
|
|
|
364
439
|
const optimizeValues = options.optimizeValues !== false;
|
|
365
440
|
|
|
366
441
|
// Validate precision from options
|
|
367
|
-
if (
|
|
368
|
-
|
|
442
|
+
if (
|
|
443
|
+
typeof precision !== "number" ||
|
|
444
|
+
precision < 0 ||
|
|
445
|
+
!Number.isFinite(precision)
|
|
446
|
+
) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
"optimizeElementTiming: options.precision must be a non-negative finite number",
|
|
449
|
+
);
|
|
369
450
|
}
|
|
370
451
|
|
|
371
452
|
const changes = [];
|
|
372
453
|
let modified = false;
|
|
373
454
|
|
|
374
455
|
// Optimize keySplines
|
|
375
|
-
const keySplines = el.getAttribute(
|
|
456
|
+
const keySplines = el.getAttribute("keySplines");
|
|
376
457
|
if (keySplines) {
|
|
377
|
-
const result = optimizeKeySplines(keySplines, {
|
|
458
|
+
const result = optimizeKeySplines(keySplines, {
|
|
459
|
+
precision,
|
|
460
|
+
removeLinear: removeLinearSplines,
|
|
461
|
+
});
|
|
378
462
|
|
|
379
463
|
if (result.allLinear && removeLinearSplines) {
|
|
380
464
|
// All splines are linear - can simplify to calcMode="linear"
|
|
381
|
-
const calcMode = el.getAttribute(
|
|
382
|
-
if (calcMode ===
|
|
383
|
-
el.setAttribute(
|
|
384
|
-
el.removeAttribute(
|
|
465
|
+
const calcMode = el.getAttribute("calcMode");
|
|
466
|
+
if (calcMode === "spline") {
|
|
467
|
+
el.setAttribute("calcMode", "linear");
|
|
468
|
+
el.removeAttribute("keySplines");
|
|
385
469
|
changes.push('Converted linear splines to calcMode="linear"');
|
|
386
470
|
modified = true;
|
|
387
471
|
}
|
|
388
472
|
} else if (result.value && result.value !== keySplines) {
|
|
389
|
-
el.setAttribute(
|
|
473
|
+
el.setAttribute("keySplines", result.value);
|
|
390
474
|
changes.push(`keySplines: "${keySplines}" -> "${result.value}"`);
|
|
391
475
|
modified = true;
|
|
392
476
|
}
|
|
393
477
|
}
|
|
394
478
|
|
|
395
479
|
// Optimize keyTimes
|
|
396
|
-
const keyTimes = el.getAttribute(
|
|
480
|
+
const keyTimes = el.getAttribute("keyTimes");
|
|
397
481
|
if (keyTimes) {
|
|
398
482
|
const optimized = optimizeKeyTimes(keyTimes, precision);
|
|
399
483
|
if (optimized !== keyTimes) {
|
|
400
|
-
el.setAttribute(
|
|
484
|
+
el.setAttribute("keyTimes", optimized);
|
|
401
485
|
changes.push(`keyTimes: "${keyTimes}" -> "${optimized}"`);
|
|
402
486
|
modified = true;
|
|
403
487
|
}
|
|
@@ -405,11 +489,11 @@ export function optimizeElementTiming(el, options = {}) {
|
|
|
405
489
|
|
|
406
490
|
// Optimize values (optimizeAnimationValues internally preserves ID refs)
|
|
407
491
|
if (optimizeValues) {
|
|
408
|
-
const values = el.getAttribute(
|
|
492
|
+
const values = el.getAttribute("values");
|
|
409
493
|
if (values) {
|
|
410
494
|
const optimized = optimizeAnimationValues(values, precision);
|
|
411
495
|
if (optimized !== values) {
|
|
412
|
-
el.setAttribute(
|
|
496
|
+
el.setAttribute("values", optimized);
|
|
413
497
|
changes.push(`values: "${values}" -> "${optimized}"`);
|
|
414
498
|
modified = true;
|
|
415
499
|
}
|
|
@@ -417,7 +501,7 @@ export function optimizeElementTiming(el, options = {}) {
|
|
|
417
501
|
}
|
|
418
502
|
|
|
419
503
|
// Optimize from/to/by (optimizeAnimationValues internally preserves ID refs)
|
|
420
|
-
for (const attr of [
|
|
504
|
+
for (const attr of ["from", "to", "by"]) {
|
|
421
505
|
const val = el.getAttribute(attr);
|
|
422
506
|
if (val) {
|
|
423
507
|
const optimized = optimizeAnimationValues(val, precision);
|
|
@@ -436,7 +520,13 @@ export function optimizeElementTiming(el, options = {}) {
|
|
|
436
520
|
* Animation elements that can have timing attributes
|
|
437
521
|
* Note: all lowercase to match svg-parser tagName normalization
|
|
438
522
|
*/
|
|
439
|
-
export const ANIMATION_ELEMENTS = [
|
|
523
|
+
export const ANIMATION_ELEMENTS = [
|
|
524
|
+
"animate",
|
|
525
|
+
"animatetransform",
|
|
526
|
+
"animatemotion",
|
|
527
|
+
"animatecolor",
|
|
528
|
+
"set",
|
|
529
|
+
];
|
|
440
530
|
|
|
441
531
|
/**
|
|
442
532
|
* Optimize all animation timing in an SVG document
|
|
@@ -446,8 +536,10 @@ export const ANIMATION_ELEMENTS = ['animate', 'animatetransform', 'animatemotion
|
|
|
446
536
|
*/
|
|
447
537
|
export function optimizeDocumentAnimationTiming(root, options = {}) {
|
|
448
538
|
// Validate root parameter
|
|
449
|
-
if (!root || typeof root !==
|
|
450
|
-
throw new Error(
|
|
539
|
+
if (!root || typeof root !== "object") {
|
|
540
|
+
throw new Error(
|
|
541
|
+
"optimizeDocumentAnimationTiming: root parameter must be a valid element",
|
|
542
|
+
);
|
|
451
543
|
}
|
|
452
544
|
|
|
453
545
|
let elementsModified = 0;
|
|
@@ -456,7 +548,7 @@ export function optimizeDocumentAnimationTiming(root, options = {}) {
|
|
|
456
548
|
|
|
457
549
|
const processElement = (el) => {
|
|
458
550
|
// Skip if not a valid element
|
|
459
|
-
if (!el || typeof el !==
|
|
551
|
+
if (!el || typeof el !== "object") return;
|
|
460
552
|
|
|
461
553
|
const tagName = el.tagName?.toLowerCase();
|
|
462
554
|
|
|
@@ -470,8 +562,8 @@ export function optimizeDocumentAnimationTiming(root, options = {}) {
|
|
|
470
562
|
totalChanges += result.changes.length;
|
|
471
563
|
details.push({
|
|
472
564
|
element: tagName,
|
|
473
|
-
id: el.getAttribute(
|
|
474
|
-
changes: result.changes
|
|
565
|
+
id: el.getAttribute("id") || null,
|
|
566
|
+
changes: result.changes,
|
|
475
567
|
});
|
|
476
568
|
}
|
|
477
569
|
}
|
|
@@ -642,7 +642,9 @@ export function getIdReferenceInfo(root, id) {
|
|
|
642
642
|
export function findUnreferencedDefs(root) {
|
|
643
643
|
// Validate input parameter
|
|
644
644
|
if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
|
|
645
|
-
throw new TypeError(
|
|
645
|
+
throw new TypeError(
|
|
646
|
+
"findUnreferencedDefs: root must be a valid SVGElement",
|
|
647
|
+
);
|
|
646
648
|
}
|
|
647
649
|
|
|
648
650
|
const refs = collectAllReferences(root);
|
|
@@ -668,7 +670,10 @@ export function findUnreferencedDefs(root) {
|
|
|
668
670
|
|
|
669
671
|
for (const defs of defsElements) {
|
|
670
672
|
// Validate defs.children exists and is iterable
|
|
671
|
-
if (
|
|
673
|
+
if (
|
|
674
|
+
!defs.children ||
|
|
675
|
+
typeof defs.children[Symbol.iterator] !== "function"
|
|
676
|
+
) {
|
|
672
677
|
continue; // Skip this defs element if children is not iterable
|
|
673
678
|
}
|
|
674
679
|
|
|
@@ -728,7 +733,10 @@ export function removeUnreferencedDefsSafe(root) {
|
|
|
728
733
|
|
|
729
734
|
for (const defs of defsElements) {
|
|
730
735
|
// Validate defs.children exists and is iterable before spreading
|
|
731
|
-
if (
|
|
736
|
+
if (
|
|
737
|
+
!defs.children ||
|
|
738
|
+
typeof defs.children[Symbol.iterator] !== "function"
|
|
739
|
+
) {
|
|
732
740
|
continue; // Skip this defs element if children is not iterable
|
|
733
741
|
}
|
|
734
742
|
|
package/src/arc-length.js
CHANGED
|
@@ -103,7 +103,7 @@ const DEFAULT_ARC_LENGTH_TOLERANCE = "1e-30";
|
|
|
103
103
|
* differ by less than this, we accept the higher-order result.
|
|
104
104
|
* NOTE: Currently unused - kept for potential future enhancement.
|
|
105
105
|
*/
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
const _SUBDIVISION_CONVERGENCE_THRESHOLD = new Decimal("1e-15");
|
|
108
108
|
|
|
109
109
|
/**
|
|
@@ -255,9 +255,7 @@ function adaptiveQuadrature(f, a, b, tol, maxDepth, minDepth, depth) {
|
|
|
255
255
|
throw new Error("adaptiveQuadrature: b must be a Decimal");
|
|
256
256
|
}
|
|
257
257
|
if (!tol || !(tol instanceof Decimal) || tol.lte(0)) {
|
|
258
|
-
throw new Error(
|
|
259
|
-
"adaptiveQuadrature: tol must be a positive Decimal",
|
|
260
|
-
);
|
|
258
|
+
throw new Error("adaptiveQuadrature: tol must be a positive Decimal");
|
|
261
259
|
}
|
|
262
260
|
if (
|
|
263
261
|
typeof maxDepth !== "number" ||
|
|
@@ -277,14 +275,8 @@ function adaptiveQuadrature(f, a, b, tol, maxDepth, minDepth, depth) {
|
|
|
277
275
|
"adaptiveQuadrature: minDepth must be a non-negative integer",
|
|
278
276
|
);
|
|
279
277
|
}
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
depth < 0 ||
|
|
283
|
-
!Number.isInteger(depth)
|
|
284
|
-
) {
|
|
285
|
-
throw new Error(
|
|
286
|
-
"adaptiveQuadrature: depth must be a non-negative integer",
|
|
287
|
-
);
|
|
278
|
+
if (typeof depth !== "number" || depth < 0 || !Number.isInteger(depth)) {
|
|
279
|
+
throw new Error("adaptiveQuadrature: depth must be a non-negative integer");
|
|
288
280
|
}
|
|
289
281
|
|
|
290
282
|
// Compute integral using 5-point and 10-point rules
|
|
@@ -354,7 +346,12 @@ function gaussLegendre(f, a, b, order) {
|
|
|
354
346
|
|
|
355
347
|
// INTERNAL CONSISTENCY CHECK: Verify Gauss-Legendre table has correct structure
|
|
356
348
|
// WHY: Ensures the precomputed tables haven't been corrupted or misconfigured
|
|
357
|
-
if (
|
|
349
|
+
if (
|
|
350
|
+
!gl.nodes ||
|
|
351
|
+
!gl.weights ||
|
|
352
|
+
gl.nodes.length !== order ||
|
|
353
|
+
gl.weights.length !== order
|
|
354
|
+
) {
|
|
358
355
|
throw new Error(
|
|
359
356
|
`gaussLegendre: GAUSS_LEGENDRE[${order}] table is malformed (expected ${order} nodes and weights)`,
|
|
360
357
|
);
|
|
@@ -385,16 +382,24 @@ function gaussLegendre(f, a, b, order) {
|
|
|
385
382
|
// VALIDATION: Ensure function returns a valid Decimal
|
|
386
383
|
// WHY: If f returns null, undefined, NaN, or non-Decimal, arithmetic operations will fail
|
|
387
384
|
if (fValue === null || fValue === undefined) {
|
|
388
|
-
throw new Error(
|
|
385
|
+
throw new Error(
|
|
386
|
+
"gaussLegendre: integrand function f returned null or undefined",
|
|
387
|
+
);
|
|
389
388
|
}
|
|
390
389
|
if (!(fValue instanceof Decimal)) {
|
|
391
|
-
throw new Error(
|
|
390
|
+
throw new Error(
|
|
391
|
+
"gaussLegendre: integrand function f must return a Decimal instance",
|
|
392
|
+
);
|
|
392
393
|
}
|
|
393
394
|
if (fValue.isNaN()) {
|
|
394
|
-
throw new Error(
|
|
395
|
+
throw new Error(
|
|
396
|
+
`gaussLegendre: integrand function f returned NaN at t=${t}`,
|
|
397
|
+
);
|
|
395
398
|
}
|
|
396
399
|
if (!fValue.isFinite()) {
|
|
397
|
-
throw new Error(
|
|
400
|
+
throw new Error(
|
|
401
|
+
`gaussLegendre: integrand function f returned non-finite value at t=${t}`,
|
|
402
|
+
);
|
|
398
403
|
}
|
|
399
404
|
|
|
400
405
|
sum = sum.plus(weight.times(fValue));
|
|
@@ -1112,9 +1117,7 @@ export function verifyArcLengthTable(points, samples = 50) {
|
|
|
1112
1117
|
!Number.isInteger(samples) ||
|
|
1113
1118
|
samples < 2
|
|
1114
1119
|
) {
|
|
1115
|
-
throw new Error(
|
|
1116
|
-
"verifyArcLengthTable: samples must be an integer >= 2",
|
|
1117
|
-
);
|
|
1120
|
+
throw new Error("verifyArcLengthTable: samples must be an integer >= 2");
|
|
1118
1121
|
}
|
|
1119
1122
|
|
|
1120
1123
|
const errors = [];
|