@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
|
@@ -14,16 +14,15 @@
|
|
|
14
14
|
* @module use-symbol-resolver
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import Decimal from
|
|
18
|
-
import { Matrix } from
|
|
19
|
-
import * as Transforms2D from
|
|
20
|
-
import * as PolygonClip from
|
|
21
|
-
import * as ClipPathResolver from
|
|
17
|
+
import Decimal from "decimal.js";
|
|
18
|
+
import { Matrix } from "./matrix.js";
|
|
19
|
+
import * as Transforms2D from "./transforms2d.js";
|
|
20
|
+
import * as PolygonClip from "./polygon-clip.js";
|
|
21
|
+
import * as ClipPathResolver from "./clip-path-resolver.js";
|
|
22
|
+
import { parseTransformAttribute } from "./svg-flatten.js";
|
|
22
23
|
|
|
23
24
|
Decimal.set({ precision: 80 });
|
|
24
25
|
|
|
25
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
26
|
-
|
|
27
26
|
/**
|
|
28
27
|
* Detect circular references when resolving use/symbol references.
|
|
29
28
|
* Prevents infinite loops when SVG contains circular reference chains like:
|
|
@@ -87,19 +86,23 @@ function hasCircularReference(startId, getNextId, maxDepth = 100) {
|
|
|
87
86
|
* // }
|
|
88
87
|
*/
|
|
89
88
|
export function parseUseElement(useElement) {
|
|
90
|
-
const href =
|
|
91
|
-
|
|
89
|
+
const href =
|
|
90
|
+
useElement.getAttribute("href") ||
|
|
91
|
+
useElement.getAttribute("xlink:href") ||
|
|
92
|
+
"";
|
|
92
93
|
|
|
93
94
|
return {
|
|
94
|
-
href: href.startsWith(
|
|
95
|
-
x: parseFloat(useElement.getAttribute(
|
|
96
|
-
y: parseFloat(useElement.getAttribute(
|
|
97
|
-
width: useElement.getAttribute(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
href: href.startsWith("#") ? href.slice(1) : href,
|
|
96
|
+
x: parseFloat(useElement.getAttribute("x") || "0"),
|
|
97
|
+
y: parseFloat(useElement.getAttribute("y") || "0"),
|
|
98
|
+
width: useElement.getAttribute("width")
|
|
99
|
+
? parseFloat(useElement.getAttribute("width"))
|
|
100
|
+
: null,
|
|
101
|
+
height: useElement.getAttribute("height")
|
|
102
|
+
? parseFloat(useElement.getAttribute("height"))
|
|
103
|
+
: null,
|
|
104
|
+
transform: useElement.getAttribute("transform") || null,
|
|
105
|
+
style: extractStyleAttributes(useElement),
|
|
103
106
|
};
|
|
104
107
|
}
|
|
105
108
|
|
|
@@ -144,23 +147,27 @@ export function parseUseElement(useElement) {
|
|
|
144
147
|
*/
|
|
145
148
|
export function parseSymbolElement(symbolElement) {
|
|
146
149
|
const data = {
|
|
147
|
-
id: symbolElement.getAttribute(
|
|
148
|
-
viewBox: symbolElement.getAttribute(
|
|
149
|
-
preserveAspectRatio:
|
|
150
|
+
id: symbolElement.getAttribute("id") || "",
|
|
151
|
+
viewBox: symbolElement.getAttribute("viewBox") || null,
|
|
152
|
+
preserveAspectRatio:
|
|
153
|
+
symbolElement.getAttribute("preserveAspectRatio") || "xMidYMid meet",
|
|
150
154
|
children: [],
|
|
151
|
-
refX: parseFloat(symbolElement.getAttribute(
|
|
152
|
-
refY: parseFloat(symbolElement.getAttribute(
|
|
155
|
+
refX: parseFloat(symbolElement.getAttribute("refX") || "0"),
|
|
156
|
+
refY: parseFloat(symbolElement.getAttribute("refY") || "0"),
|
|
153
157
|
};
|
|
154
158
|
|
|
155
159
|
// Parse viewBox
|
|
156
160
|
if (data.viewBox) {
|
|
157
|
-
const parts = data.viewBox
|
|
161
|
+
const parts = data.viewBox
|
|
162
|
+
.trim()
|
|
163
|
+
.split(/[\s,]+/)
|
|
164
|
+
.map(Number);
|
|
158
165
|
if (parts.length === 4) {
|
|
159
166
|
data.viewBoxParsed = {
|
|
160
167
|
x: parts[0],
|
|
161
168
|
y: parts[1],
|
|
162
169
|
width: parts[2],
|
|
163
|
-
height: parts[3]
|
|
170
|
+
height: parts[3],
|
|
164
171
|
};
|
|
165
172
|
}
|
|
166
173
|
}
|
|
@@ -239,61 +246,66 @@ export function parseChildElement(element) {
|
|
|
239
246
|
|
|
240
247
|
const data = {
|
|
241
248
|
type: tagName,
|
|
242
|
-
id: element.getAttribute(
|
|
243
|
-
transform: element.getAttribute(
|
|
244
|
-
style: extractStyleAttributes(element)
|
|
249
|
+
id: element.getAttribute("id") || null,
|
|
250
|
+
transform: element.getAttribute("transform") || null,
|
|
251
|
+
style: extractStyleAttributes(element),
|
|
245
252
|
};
|
|
246
253
|
|
|
247
254
|
switch (tagName) {
|
|
248
|
-
case
|
|
249
|
-
data.x = parseFloat(element.getAttribute(
|
|
250
|
-
data.y = parseFloat(element.getAttribute(
|
|
251
|
-
data.width = parseFloat(element.getAttribute(
|
|
252
|
-
data.height = parseFloat(element.getAttribute(
|
|
253
|
-
data.rx = parseFloat(element.getAttribute(
|
|
254
|
-
data.ry = parseFloat(element.getAttribute(
|
|
255
|
+
case "rect":
|
|
256
|
+
data.x = parseFloat(element.getAttribute("x") || "0");
|
|
257
|
+
data.y = parseFloat(element.getAttribute("y") || "0");
|
|
258
|
+
data.width = parseFloat(element.getAttribute("width") || "0");
|
|
259
|
+
data.height = parseFloat(element.getAttribute("height") || "0");
|
|
260
|
+
data.rx = parseFloat(element.getAttribute("rx") || "0");
|
|
261
|
+
data.ry = parseFloat(element.getAttribute("ry") || "0");
|
|
255
262
|
break;
|
|
256
|
-
case
|
|
257
|
-
data.cx = parseFloat(element.getAttribute(
|
|
258
|
-
data.cy = parseFloat(element.getAttribute(
|
|
259
|
-
data.r = parseFloat(element.getAttribute(
|
|
263
|
+
case "circle":
|
|
264
|
+
data.cx = parseFloat(element.getAttribute("cx") || "0");
|
|
265
|
+
data.cy = parseFloat(element.getAttribute("cy") || "0");
|
|
266
|
+
data.r = parseFloat(element.getAttribute("r") || "0");
|
|
260
267
|
break;
|
|
261
|
-
case
|
|
262
|
-
data.cx = parseFloat(element.getAttribute(
|
|
263
|
-
data.cy = parseFloat(element.getAttribute(
|
|
264
|
-
data.rx = parseFloat(element.getAttribute(
|
|
265
|
-
data.ry = parseFloat(element.getAttribute(
|
|
268
|
+
case "ellipse":
|
|
269
|
+
data.cx = parseFloat(element.getAttribute("cx") || "0");
|
|
270
|
+
data.cy = parseFloat(element.getAttribute("cy") || "0");
|
|
271
|
+
data.rx = parseFloat(element.getAttribute("rx") || "0");
|
|
272
|
+
data.ry = parseFloat(element.getAttribute("ry") || "0");
|
|
266
273
|
break;
|
|
267
|
-
case
|
|
268
|
-
data.d = element.getAttribute(
|
|
274
|
+
case "path":
|
|
275
|
+
data.d = element.getAttribute("d") || "";
|
|
269
276
|
break;
|
|
270
|
-
case
|
|
271
|
-
data.points = element.getAttribute(
|
|
277
|
+
case "polygon":
|
|
278
|
+
data.points = element.getAttribute("points") || "";
|
|
272
279
|
break;
|
|
273
|
-
case
|
|
274
|
-
data.points = element.getAttribute(
|
|
280
|
+
case "polyline":
|
|
281
|
+
data.points = element.getAttribute("points") || "";
|
|
275
282
|
break;
|
|
276
|
-
case
|
|
277
|
-
data.x1 = parseFloat(element.getAttribute(
|
|
278
|
-
data.y1 = parseFloat(element.getAttribute(
|
|
279
|
-
data.x2 = parseFloat(element.getAttribute(
|
|
280
|
-
data.y2 = parseFloat(element.getAttribute(
|
|
283
|
+
case "line":
|
|
284
|
+
data.x1 = parseFloat(element.getAttribute("x1") || "0");
|
|
285
|
+
data.y1 = parseFloat(element.getAttribute("y1") || "0");
|
|
286
|
+
data.x2 = parseFloat(element.getAttribute("x2") || "0");
|
|
287
|
+
data.y2 = parseFloat(element.getAttribute("y2") || "0");
|
|
281
288
|
break;
|
|
282
|
-
case
|
|
289
|
+
case "g":
|
|
283
290
|
data.children = [];
|
|
284
291
|
for (const child of element.children) {
|
|
285
292
|
data.children.push(parseChildElement(child));
|
|
286
293
|
}
|
|
287
294
|
break;
|
|
288
|
-
case
|
|
289
|
-
data.href = (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
data.
|
|
296
|
-
|
|
295
|
+
case "use":
|
|
296
|
+
data.href = (
|
|
297
|
+
element.getAttribute("href") ||
|
|
298
|
+
element.getAttribute("xlink:href") ||
|
|
299
|
+
""
|
|
300
|
+
).replace("#", "");
|
|
301
|
+
data.x = parseFloat(element.getAttribute("x") || "0");
|
|
302
|
+
data.y = parseFloat(element.getAttribute("y") || "0");
|
|
303
|
+
data.width = element.getAttribute("width")
|
|
304
|
+
? parseFloat(element.getAttribute("width"))
|
|
305
|
+
: null;
|
|
306
|
+
data.height = element.getAttribute("height")
|
|
307
|
+
? parseFloat(element.getAttribute("height"))
|
|
308
|
+
: null;
|
|
297
309
|
break;
|
|
298
310
|
}
|
|
299
311
|
|
|
@@ -340,14 +352,14 @@ export function parseChildElement(element) {
|
|
|
340
352
|
*/
|
|
341
353
|
export function extractStyleAttributes(element) {
|
|
342
354
|
return {
|
|
343
|
-
fill: element.getAttribute(
|
|
344
|
-
stroke: element.getAttribute(
|
|
345
|
-
strokeWidth: element.getAttribute(
|
|
346
|
-
opacity: element.getAttribute(
|
|
347
|
-
fillOpacity: element.getAttribute(
|
|
348
|
-
strokeOpacity: element.getAttribute(
|
|
349
|
-
visibility: element.getAttribute(
|
|
350
|
-
display: element.getAttribute(
|
|
355
|
+
fill: element.getAttribute("fill"),
|
|
356
|
+
stroke: element.getAttribute("stroke"),
|
|
357
|
+
strokeWidth: element.getAttribute("stroke-width"),
|
|
358
|
+
opacity: element.getAttribute("opacity"),
|
|
359
|
+
fillOpacity: element.getAttribute("fill-opacity"),
|
|
360
|
+
strokeOpacity: element.getAttribute("stroke-opacity"),
|
|
361
|
+
visibility: element.getAttribute("visibility"),
|
|
362
|
+
display: element.getAttribute("display"),
|
|
351
363
|
};
|
|
352
364
|
}
|
|
353
365
|
|
|
@@ -398,7 +410,12 @@ export function extractStyleAttributes(element) {
|
|
|
398
410
|
* // Result: uniform scale of 2.0 (max of 200/100, 150/100)
|
|
399
411
|
* // Centered: translate(0, -25) then scale(2, 2) - height extends beyond viewport
|
|
400
412
|
*/
|
|
401
|
-
export function calculateViewBoxTransform(
|
|
413
|
+
export function calculateViewBoxTransform(
|
|
414
|
+
viewBox,
|
|
415
|
+
targetWidth,
|
|
416
|
+
targetHeight,
|
|
417
|
+
preserveAspectRatio = "xMidYMid meet",
|
|
418
|
+
) {
|
|
402
419
|
if (!viewBox || !targetWidth || !targetHeight) {
|
|
403
420
|
return Matrix.identity(3);
|
|
404
421
|
}
|
|
@@ -414,15 +431,16 @@ export function calculateViewBoxTransform(viewBox, targetWidth, targetHeight, pr
|
|
|
414
431
|
|
|
415
432
|
// Parse preserveAspectRatio
|
|
416
433
|
const parts = preserveAspectRatio.trim().split(/\s+/);
|
|
417
|
-
const align = parts[0] ||
|
|
418
|
-
const meetOrSlice = parts[1] ||
|
|
434
|
+
const align = parts[0] || "xMidYMid";
|
|
435
|
+
const meetOrSlice = parts[1] || "meet";
|
|
419
436
|
|
|
420
|
-
if (align ===
|
|
437
|
+
if (align === "none") {
|
|
421
438
|
// Non-uniform scaling
|
|
422
439
|
const scaleX = targetWidth / vbW;
|
|
423
440
|
const scaleY = targetHeight / vbH;
|
|
424
|
-
return Transforms2D.translation(-vbX * scaleX, -vbY * scaleY)
|
|
425
|
-
|
|
441
|
+
return Transforms2D.translation(-vbX * scaleX, -vbY * scaleY).mul(
|
|
442
|
+
Transforms2D.scale(scaleX, scaleY),
|
|
443
|
+
);
|
|
426
444
|
}
|
|
427
445
|
|
|
428
446
|
// Uniform scaling
|
|
@@ -430,7 +448,7 @@ export function calculateViewBoxTransform(viewBox, targetWidth, targetHeight, pr
|
|
|
430
448
|
const scaleY = targetHeight / vbH;
|
|
431
449
|
let scale;
|
|
432
450
|
|
|
433
|
-
if (meetOrSlice ===
|
|
451
|
+
if (meetOrSlice === "slice") {
|
|
434
452
|
scale = Math.max(scaleX, scaleY);
|
|
435
453
|
} else {
|
|
436
454
|
scale = Math.min(scaleX, scaleY);
|
|
@@ -444,21 +462,20 @@ export function calculateViewBoxTransform(viewBox, targetWidth, targetHeight, pr
|
|
|
444
462
|
const scaledHeight = vbH * scale;
|
|
445
463
|
|
|
446
464
|
// X alignment
|
|
447
|
-
if (align.includes(
|
|
465
|
+
if (align.includes("xMid")) {
|
|
448
466
|
tx += (targetWidth - scaledWidth) / 2;
|
|
449
|
-
} else if (align.includes(
|
|
467
|
+
} else if (align.includes("xMax")) {
|
|
450
468
|
tx += targetWidth - scaledWidth;
|
|
451
469
|
}
|
|
452
470
|
|
|
453
471
|
// Y alignment
|
|
454
|
-
if (align.includes(
|
|
472
|
+
if (align.includes("YMid")) {
|
|
455
473
|
ty += (targetHeight - scaledHeight) / 2;
|
|
456
|
-
} else if (align.includes(
|
|
474
|
+
} else if (align.includes("YMax")) {
|
|
457
475
|
ty += targetHeight - scaledHeight;
|
|
458
476
|
}
|
|
459
477
|
|
|
460
|
-
return Transforms2D.translation(tx, ty)
|
|
461
|
-
.mul(Transforms2D.scale(scale, scale));
|
|
478
|
+
return Transforms2D.translation(tx, ty).mul(Transforms2D.scale(scale, scale));
|
|
462
479
|
}
|
|
463
480
|
|
|
464
481
|
/**
|
|
@@ -557,13 +574,13 @@ export function resolveUse(useData, defs, options = {}) {
|
|
|
557
574
|
// Pre-multiply by use element's transform if present (step 1)
|
|
558
575
|
// This makes useTransform apply FIRST, then translation
|
|
559
576
|
if (useData.transform) {
|
|
560
|
-
const useTransform =
|
|
577
|
+
const useTransform = parseTransformAttribute(useData.transform);
|
|
561
578
|
transform = transform.mul(useTransform);
|
|
562
579
|
}
|
|
563
580
|
|
|
564
581
|
// Handle symbol with viewBox (step 3)
|
|
565
582
|
// ViewBox transform applies LAST (after translation and useTransform)
|
|
566
|
-
if (target.type ===
|
|
583
|
+
if (target.type === "symbol" && target.viewBoxParsed) {
|
|
567
584
|
const width = useData.width || target.viewBoxParsed.width;
|
|
568
585
|
const height = useData.height || target.viewBoxParsed.height;
|
|
569
586
|
|
|
@@ -571,7 +588,7 @@ export function resolveUse(useData, defs, options = {}) {
|
|
|
571
588
|
target.viewBoxParsed,
|
|
572
589
|
width,
|
|
573
590
|
height,
|
|
574
|
-
target.preserveAspectRatio
|
|
591
|
+
target.preserveAspectRatio,
|
|
575
592
|
);
|
|
576
593
|
|
|
577
594
|
// ViewBox transform is applied LAST, so it's the leftmost in multiplication
|
|
@@ -583,7 +600,7 @@ export function resolveUse(useData, defs, options = {}) {
|
|
|
583
600
|
const children = target.children || [target];
|
|
584
601
|
|
|
585
602
|
for (const child of children) {
|
|
586
|
-
if (child.type ===
|
|
603
|
+
if (child.type === "use") {
|
|
587
604
|
// Recursive resolution
|
|
588
605
|
const resolved = resolveUse(child, defs, { maxDepth: maxDepth - 1 });
|
|
589
606
|
if (resolved) {
|
|
@@ -592,7 +609,7 @@ export function resolveUse(useData, defs, options = {}) {
|
|
|
592
609
|
} else {
|
|
593
610
|
resolvedChildren.push({
|
|
594
611
|
element: child,
|
|
595
|
-
transform: Matrix.identity(3)
|
|
612
|
+
transform: Matrix.identity(3),
|
|
596
613
|
});
|
|
597
614
|
}
|
|
598
615
|
}
|
|
@@ -601,7 +618,7 @@ export function resolveUse(useData, defs, options = {}) {
|
|
|
601
618
|
element: target,
|
|
602
619
|
transform,
|
|
603
620
|
children: resolvedChildren,
|
|
604
|
-
inheritedStyle: useData.style
|
|
621
|
+
inheritedStyle: useData.style,
|
|
605
622
|
};
|
|
606
623
|
}
|
|
607
624
|
|
|
@@ -670,8 +687,12 @@ export function flattenResolvedUse(resolved, samples = 20) {
|
|
|
670
687
|
// Recursive flattening
|
|
671
688
|
const nested = flattenResolvedUse(child, samples);
|
|
672
689
|
for (const n of nested) {
|
|
673
|
-
n.polygon = n.polygon.map(p => {
|
|
674
|
-
const [x, y] = Transforms2D.applyTransform(
|
|
690
|
+
n.polygon = n.polygon.map((p) => {
|
|
691
|
+
const [x, y] = Transforms2D.applyTransform(
|
|
692
|
+
resolved.transform,
|
|
693
|
+
p.x,
|
|
694
|
+
p.y,
|
|
695
|
+
);
|
|
675
696
|
return { x, y };
|
|
676
697
|
});
|
|
677
698
|
results.push(n);
|
|
@@ -682,7 +703,7 @@ export function flattenResolvedUse(resolved, samples = 20) {
|
|
|
682
703
|
if (polygon.length >= 3) {
|
|
683
704
|
results.push({
|
|
684
705
|
polygon,
|
|
685
|
-
style: mergeStyles(resolved.inheritedStyle, element.style)
|
|
706
|
+
style: mergeStyles(resolved.inheritedStyle, element.style),
|
|
686
707
|
});
|
|
687
708
|
}
|
|
688
709
|
}
|
|
@@ -731,7 +752,7 @@ export function elementToPolygon(element, transform, samples = 20) {
|
|
|
731
752
|
|
|
732
753
|
// Apply transform
|
|
733
754
|
if (transform && polygon.length > 0) {
|
|
734
|
-
polygon = polygon.map(p => {
|
|
755
|
+
polygon = polygon.map((p) => {
|
|
735
756
|
const [x, y] = Transforms2D.applyTransform(transform, p.x, p.y);
|
|
736
757
|
return { x, y };
|
|
737
758
|
});
|
|
@@ -867,7 +888,7 @@ export function getResolvedBBox(resolved, samples = 20) {
|
|
|
867
888
|
x: minX,
|
|
868
889
|
y: minY,
|
|
869
890
|
width: maxX - minX,
|
|
870
|
-
height: maxY - minY
|
|
891
|
+
height: maxY - minY,
|
|
871
892
|
};
|
|
872
893
|
}
|
|
873
894
|
|
|
@@ -929,7 +950,7 @@ export function clipResolvedUse(resolved, clipPolygon, samples = 20) {
|
|
|
929
950
|
if (clippedPoly.length >= 3) {
|
|
930
951
|
result.push({
|
|
931
952
|
polygon: clippedPoly,
|
|
932
|
-
style
|
|
953
|
+
style,
|
|
933
954
|
});
|
|
934
955
|
}
|
|
935
956
|
}
|
|
@@ -993,19 +1014,19 @@ export function resolvedUseToPathData(resolved, samples = 20) {
|
|
|
993
1014
|
|
|
994
1015
|
for (const { polygon } of polygons) {
|
|
995
1016
|
if (polygon.length >= 3) {
|
|
996
|
-
let d =
|
|
1017
|
+
let d = "";
|
|
997
1018
|
for (let i = 0; i < polygon.length; i++) {
|
|
998
1019
|
const p = polygon[i];
|
|
999
1020
|
const x = Number(p.x).toFixed(6);
|
|
1000
1021
|
const y = Number(p.y).toFixed(6);
|
|
1001
1022
|
d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
|
|
1002
1023
|
}
|
|
1003
|
-
d +=
|
|
1024
|
+
d += " Z";
|
|
1004
1025
|
paths.push(d);
|
|
1005
1026
|
}
|
|
1006
1027
|
}
|
|
1007
1028
|
|
|
1008
|
-
return paths.join(
|
|
1029
|
+
return paths.join(" ");
|
|
1009
1030
|
}
|
|
1010
1031
|
|
|
1011
1032
|
/**
|
|
@@ -1063,15 +1084,15 @@ export function buildDefsMap(svgRoot) {
|
|
|
1063
1084
|
const defs = {};
|
|
1064
1085
|
|
|
1065
1086
|
// Find all elements with id
|
|
1066
|
-
const elementsWithId = svgRoot.querySelectorAll(
|
|
1087
|
+
const elementsWithId = svgRoot.querySelectorAll("[id]");
|
|
1067
1088
|
|
|
1068
1089
|
for (const element of elementsWithId) {
|
|
1069
|
-
const id = element.getAttribute(
|
|
1090
|
+
const id = element.getAttribute("id");
|
|
1070
1091
|
const tagName = element.tagName.toLowerCase();
|
|
1071
1092
|
|
|
1072
|
-
if (tagName ===
|
|
1093
|
+
if (tagName === "symbol") {
|
|
1073
1094
|
defs[id] = parseSymbolElement(element);
|
|
1074
|
-
defs[id].type =
|
|
1095
|
+
defs[id].type = "symbol";
|
|
1075
1096
|
} else {
|
|
1076
1097
|
defs[id] = parseChildElement(element);
|
|
1077
1098
|
}
|
|
@@ -1146,7 +1167,7 @@ export function buildDefsMap(svgRoot) {
|
|
|
1146
1167
|
*/
|
|
1147
1168
|
export function resolveAllUses(svgRoot, options = {}) {
|
|
1148
1169
|
const defs = buildDefsMap(svgRoot);
|
|
1149
|
-
const useElements = svgRoot.querySelectorAll(
|
|
1170
|
+
const useElements = svgRoot.querySelectorAll("use");
|
|
1150
1171
|
const resolved = [];
|
|
1151
1172
|
|
|
1152
1173
|
// Helper to get the next use reference from a definition
|
|
@@ -1155,14 +1176,14 @@ export function resolveAllUses(svgRoot, options = {}) {
|
|
|
1155
1176
|
if (!target) return null;
|
|
1156
1177
|
|
|
1157
1178
|
// Check if target is itself a use element
|
|
1158
|
-
if (target.type ===
|
|
1179
|
+
if (target.type === "use") {
|
|
1159
1180
|
return target.href;
|
|
1160
1181
|
}
|
|
1161
1182
|
|
|
1162
1183
|
// Check if target contains use elements in its children
|
|
1163
1184
|
if (target.children && target.children.length > 0) {
|
|
1164
1185
|
for (const child of target.children) {
|
|
1165
|
-
if (child.type ===
|
|
1186
|
+
if (child.type === "use") {
|
|
1166
1187
|
return child.href;
|
|
1167
1188
|
}
|
|
1168
1189
|
}
|
|
@@ -1176,7 +1197,9 @@ export function resolveAllUses(svgRoot, options = {}) {
|
|
|
1176
1197
|
|
|
1177
1198
|
// Check for circular reference before attempting to resolve
|
|
1178
1199
|
if (hasCircularReference(useData.href, getUseRef)) {
|
|
1179
|
-
console.warn(
|
|
1200
|
+
console.warn(
|
|
1201
|
+
`Circular use reference detected: #${useData.href}, skipping resolution`,
|
|
1202
|
+
);
|
|
1180
1203
|
continue;
|
|
1181
1204
|
}
|
|
1182
1205
|
|
|
@@ -1185,10 +1208,27 @@ export function resolveAllUses(svgRoot, options = {}) {
|
|
|
1185
1208
|
resolved.push({
|
|
1186
1209
|
original: useEl,
|
|
1187
1210
|
useData,
|
|
1188
|
-
resolved: result
|
|
1211
|
+
resolved: result,
|
|
1189
1212
|
});
|
|
1190
1213
|
}
|
|
1191
1214
|
}
|
|
1192
1215
|
|
|
1193
1216
|
return resolved;
|
|
1194
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
export default {
|
|
1220
|
+
parseUseElement,
|
|
1221
|
+
parseSymbolElement,
|
|
1222
|
+
parseChildElement,
|
|
1223
|
+
extractStyleAttributes,
|
|
1224
|
+
calculateViewBoxTransform,
|
|
1225
|
+
resolveUse,
|
|
1226
|
+
flattenResolvedUse,
|
|
1227
|
+
elementToPolygon,
|
|
1228
|
+
mergeStyles,
|
|
1229
|
+
getResolvedBBox,
|
|
1230
|
+
clipResolvedUse,
|
|
1231
|
+
resolvedUseToPathData,
|
|
1232
|
+
buildDefsMap,
|
|
1233
|
+
resolveAllUses,
|
|
1234
|
+
};
|
package/src/vector.js
CHANGED
|
@@ -15,6 +15,10 @@ const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
|
15
15
|
* Supports basic operations (add, sub, scale), products (dot, cross, outer),
|
|
16
16
|
* and geometric operations (norm, normalize, angle, projection).
|
|
17
17
|
*
|
|
18
|
+
* @class
|
|
19
|
+
* @property {Array<Decimal>} data - Array of Decimal vector components
|
|
20
|
+
* @property {number} length - Number of components in the vector
|
|
21
|
+
*
|
|
18
22
|
* @example
|
|
19
23
|
* const v = Vector.from([1, 2, 3]);
|
|
20
24
|
* const w = Vector.from(['1.5', '2.5', '3.5']);
|
|
@@ -83,8 +87,12 @@ export class Vector {
|
|
|
83
87
|
* @throws {Error} If other is not a Vector or dimensions mismatch
|
|
84
88
|
*/
|
|
85
89
|
add(other) {
|
|
86
|
-
if (!(other instanceof Vector))
|
|
87
|
-
|
|
90
|
+
if (!other || !(other instanceof Vector)) {
|
|
91
|
+
throw new Error('add: argument must be a Vector');
|
|
92
|
+
}
|
|
93
|
+
if (this.length !== other.length) {
|
|
94
|
+
throw new Error(`add: dimension mismatch (${this.length} vs ${other.length})`);
|
|
95
|
+
}
|
|
88
96
|
return new Vector(this.data.map((v, i) => v.plus(other.data[i])));
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -95,8 +103,12 @@ export class Vector {
|
|
|
95
103
|
* @throws {Error} If other is not a Vector or dimensions mismatch
|
|
96
104
|
*/
|
|
97
105
|
sub(other) {
|
|
98
|
-
if (!(other instanceof Vector))
|
|
99
|
-
|
|
106
|
+
if (!other || !(other instanceof Vector)) {
|
|
107
|
+
throw new Error('sub: argument must be a Vector');
|
|
108
|
+
}
|
|
109
|
+
if (this.length !== other.length) {
|
|
110
|
+
throw new Error(`sub: dimension mismatch (${this.length} vs ${other.length})`);
|
|
111
|
+
}
|
|
100
112
|
return new Vector(this.data.map((v, i) => v.minus(other.data[i])));
|
|
101
113
|
}
|
|
102
114
|
|
|
@@ -125,8 +137,12 @@ export class Vector {
|
|
|
125
137
|
* @throws {Error} If other is not a Vector or dimensions mismatch
|
|
126
138
|
*/
|
|
127
139
|
dot(other) {
|
|
128
|
-
if (!(other instanceof Vector))
|
|
129
|
-
|
|
140
|
+
if (!other || !(other instanceof Vector)) {
|
|
141
|
+
throw new Error('dot: argument must be a Vector');
|
|
142
|
+
}
|
|
143
|
+
if (this.length !== other.length) {
|
|
144
|
+
throw new Error(`dot: dimension mismatch (${this.length} vs ${other.length})`);
|
|
145
|
+
}
|
|
130
146
|
return this.data.reduce((acc, v, i) => acc.plus(v.mul(other.data[i])), new Decimal(0));
|
|
131
147
|
}
|
|
132
148
|
|
|
@@ -143,7 +159,9 @@ export class Vector {
|
|
|
143
159
|
* @throws {Error} If other is not a Vector
|
|
144
160
|
*/
|
|
145
161
|
outer(other) {
|
|
146
|
-
if (!(other instanceof Vector))
|
|
162
|
+
if (!other || !(other instanceof Vector)) {
|
|
163
|
+
throw new Error('outer: argument must be a Vector');
|
|
164
|
+
}
|
|
147
165
|
const rows = this.length, cols = other.length;
|
|
148
166
|
const out = Array.from({ length: rows }, (_, i) =>
|
|
149
167
|
Array.from({ length: cols }, (_, j) => this.data[i].mul(other.data[j]))
|
|
@@ -155,10 +173,15 @@ export class Vector {
|
|
|
155
173
|
* Cross product for 3D vectors only.
|
|
156
174
|
* @param {Vector} other - 3D Vector to compute cross product with
|
|
157
175
|
* @returns {Vector} New 3D Vector perpendicular to both inputs
|
|
158
|
-
* @throws {Error} If either vector is not 3D
|
|
176
|
+
* @throws {Error} If other is not a Vector or either vector is not 3D
|
|
159
177
|
*/
|
|
160
178
|
cross(other) {
|
|
161
|
-
if (
|
|
179
|
+
if (!other || !(other instanceof Vector)) {
|
|
180
|
+
throw new Error('cross: argument must be a Vector');
|
|
181
|
+
}
|
|
182
|
+
if (this.length !== 3 || other.length !== 3) {
|
|
183
|
+
throw new Error(`cross: requires 3D vectors (got ${this.length}D and ${other.length}D)`);
|
|
184
|
+
}
|
|
162
185
|
const [a1, a2, a3] = this.data;
|
|
163
186
|
const [b1, b2, b3] = other.data;
|
|
164
187
|
return new Vector([
|
|
@@ -193,15 +216,23 @@ export class Vector {
|
|
|
193
216
|
* Angle between this vector and another (in radians).
|
|
194
217
|
* @param {Vector} other - Vector to compute angle with
|
|
195
218
|
* @returns {Decimal} Angle in radians as a Decimal
|
|
196
|
-
* @throws {Error} If either vector is zero
|
|
219
|
+
* @throws {Error} If other is not a Vector, dimensions mismatch, or either vector is zero
|
|
197
220
|
*/
|
|
198
221
|
angleBetween(other) {
|
|
222
|
+
if (!other || !(other instanceof Vector)) {
|
|
223
|
+
throw new Error('angleBetween: argument must be a Vector');
|
|
224
|
+
}
|
|
225
|
+
if (this.length !== other.length) {
|
|
226
|
+
throw new Error(`angleBetween: dimension mismatch (${this.length} vs ${other.length})`);
|
|
227
|
+
}
|
|
199
228
|
const dotProduct = this.dot(other);
|
|
200
229
|
const n1 = this.norm();
|
|
201
230
|
const n2 = other.norm();
|
|
202
|
-
if (n1.isZero() || n2.isZero())
|
|
231
|
+
if (n1.isZero() || n2.isZero()) {
|
|
232
|
+
throw new Error('angleBetween: angle with zero vector is undefined');
|
|
233
|
+
}
|
|
203
234
|
// Clamp cosine to [-1, 1] for numerical safety
|
|
204
|
-
|
|
235
|
+
const cosv = dotProduct.div(n1.mul(n2));
|
|
205
236
|
const cosNum = cosv.toNumber();
|
|
206
237
|
const clamped = Math.min(1, Math.max(-1, cosNum));
|
|
207
238
|
return new Decimal(Math.acos(clamped));
|
|
@@ -211,11 +242,19 @@ export class Vector {
|
|
|
211
242
|
* Project this vector onto another vector.
|
|
212
243
|
* @param {Vector} other - Vector to project onto
|
|
213
244
|
* @returns {Vector} The projection of this onto other
|
|
214
|
-
* @throws {Error} If other is zero vector
|
|
245
|
+
* @throws {Error} If other is not a Vector, dimensions mismatch, or other is zero vector
|
|
215
246
|
*/
|
|
216
247
|
projectOnto(other) {
|
|
248
|
+
if (!other || !(other instanceof Vector)) {
|
|
249
|
+
throw new Error('projectOnto: argument must be a Vector');
|
|
250
|
+
}
|
|
251
|
+
if (this.length !== other.length) {
|
|
252
|
+
throw new Error(`projectOnto: dimension mismatch (${this.length} vs ${other.length})`);
|
|
253
|
+
}
|
|
217
254
|
const denom = other.dot(other);
|
|
218
|
-
if (denom.isZero())
|
|
255
|
+
if (denom.isZero()) {
|
|
256
|
+
throw new Error('projectOnto: cannot project onto zero vector');
|
|
257
|
+
}
|
|
219
258
|
const coef = this.dot(other).div(denom);
|
|
220
259
|
return other.scale(coef);
|
|
221
260
|
}
|
|
@@ -250,8 +289,15 @@ export class Vector {
|
|
|
250
289
|
* Check if this vector is orthogonal to another.
|
|
251
290
|
* @param {Vector} other - Vector to check orthogonality with
|
|
252
291
|
* @returns {boolean} True if vectors are orthogonal (dot product is zero)
|
|
292
|
+
* @throws {Error} If other is not a Vector or dimensions mismatch
|
|
253
293
|
*/
|
|
254
294
|
isOrthogonalTo(other) {
|
|
295
|
+
if (!other || !(other instanceof Vector)) {
|
|
296
|
+
throw new Error('isOrthogonalTo: argument must be a Vector');
|
|
297
|
+
}
|
|
298
|
+
if (this.length !== other.length) {
|
|
299
|
+
throw new Error(`isOrthogonalTo: dimension mismatch (${this.length} vs ${other.length})`);
|
|
300
|
+
}
|
|
255
301
|
return this.dot(other).isZero();
|
|
256
302
|
}
|
|
257
303
|
|
|
@@ -259,9 +305,15 @@ export class Vector {
|
|
|
259
305
|
* Euclidean distance to another vector.
|
|
260
306
|
* @param {Vector} other - Vector to compute distance to
|
|
261
307
|
* @returns {Decimal} The Euclidean distance
|
|
262
|
-
* @throws {Error} If dimensions mismatch
|
|
308
|
+
* @throws {Error} If other is not a Vector or dimensions mismatch
|
|
263
309
|
*/
|
|
264
310
|
distance(other) {
|
|
311
|
+
if (!other || !(other instanceof Vector)) {
|
|
312
|
+
throw new Error('distance: argument must be a Vector');
|
|
313
|
+
}
|
|
314
|
+
if (this.length !== other.length) {
|
|
315
|
+
throw new Error(`distance: dimension mismatch (${this.length} vs ${other.length})`);
|
|
316
|
+
}
|
|
265
317
|
return this.sub(other).norm();
|
|
266
318
|
}
|
|
267
319
|
|