@emasoft/svg-matrix 1.0.19 → 1.0.20

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.
@@ -24,6 +24,34 @@ Decimal.set({ precision: 80 });
24
24
 
25
25
  const D = x => (x instanceof Decimal ? x : new Decimal(x));
26
26
 
27
+ /**
28
+ * Detect circular references when resolving use/symbol references.
29
+ * Prevents infinite loops when SVG contains circular reference chains like:
30
+ * - <use href="#a"> where #a contains <use href="#a"> (self-reference)
31
+ * - <use href="#a"> → #a contains <use href="#b"> → #b contains <use href="#a"> (circular chain)
32
+ *
33
+ * @param {string} startId - Starting ID to check
34
+ * @param {Function} getNextId - Function that returns the next referenced ID given current ID
35
+ * @param {number} maxDepth - Maximum chain depth before considering circular (default 100)
36
+ * @returns {boolean} True if circular reference detected
37
+ */
38
+ function hasCircularReference(startId, getNextId, maxDepth = 100) {
39
+ const visited = new Set();
40
+ let currentId = startId;
41
+ let depth = 0;
42
+
43
+ while (currentId && depth < maxDepth) {
44
+ if (visited.has(currentId)) {
45
+ return true; // Circular reference detected!
46
+ }
47
+ visited.add(currentId);
48
+ currentId = getNextId(currentId);
49
+ depth++;
50
+ }
51
+
52
+ return depth >= maxDepth; // Too deep = likely circular
53
+ }
54
+
27
55
  /**
28
56
  * Parse SVG <use> element to structured data.
29
57
  *
@@ -438,13 +466,13 @@ export function calculateViewBoxTransform(viewBox, targetWidth, targetHeight, pr
438
466
  *
439
467
  * This is the core use/symbol resolution algorithm. It:
440
468
  * 1. Looks up the target element by id (can be symbol, shape, group, or nested use)
441
- * 2. Composes transforms: translation from x,y → use's transform → viewBox mapping
469
+ * 2. Composes transforms: use's transform → translation from x,y → viewBox mapping
442
470
  * 3. Recursively resolves nested <use> elements (with depth limit)
443
471
  * 4. Propagates style inheritance from <use> to referenced content
444
472
  *
445
473
  * Transform composition order (right-to-left multiplication):
446
- * - First: translate by (x, y) to position the reference
447
- * - Second: apply use element's transform attribute
474
+ * - First: apply use element's transform attribute
475
+ * - Second: translate by (x, y) to position the reference
448
476
  * - Third: apply viewBox→viewport mapping (symbols only)
449
477
  *
450
478
  * For symbols with viewBox, if <use> specifies width/height, those establish the
@@ -515,16 +543,26 @@ export function resolveUse(useData, defs, options = {}) {
515
543
  return null;
516
544
  }
517
545
 
518
- // Calculate base transform from x, y
546
+ // CORRECT ORDER per SVG spec:
547
+ // 1. Apply use element's transform attribute first
548
+ // 2. Then apply translate(x, y) for use element's x/y attributes
549
+ // 3. Then apply viewBox transform (for symbols)
550
+ //
551
+ // Matrix multiplication order: rightmost transform is applied first
552
+ // So to apply transforms in order 1→2→3, we build: 3.mul(2).mul(1)
553
+
554
+ // Start with x,y translation (step 2)
519
555
  let transform = Transforms2D.translation(useData.x, useData.y);
520
556
 
521
- // Apply use element's transform if present
557
+ // Pre-multiply by use element's transform if present (step 1)
558
+ // This makes useTransform apply FIRST, then translation
522
559
  if (useData.transform) {
523
560
  const useTransform = ClipPathResolver.parseTransform(useData.transform);
524
561
  transform = transform.mul(useTransform);
525
562
  }
526
563
 
527
- // Handle symbol with viewBox
564
+ // Handle symbol with viewBox (step 3)
565
+ // ViewBox transform applies LAST (after translation and useTransform)
528
566
  if (target.type === 'symbol' && target.viewBoxParsed) {
529
567
  const width = useData.width || target.viewBoxParsed.width;
530
568
  const height = useData.height || target.viewBoxParsed.height;
@@ -536,7 +574,8 @@ export function resolveUse(useData, defs, options = {}) {
536
574
  target.preserveAspectRatio
537
575
  );
538
576
 
539
- transform = transform.mul(viewBoxTransform);
577
+ // ViewBox transform is applied LAST, so it's the leftmost in multiplication
578
+ transform = viewBoxTransform.mul(transform);
540
579
  }
541
580
 
542
581
  // Resolve children
@@ -1110,8 +1149,37 @@ export function resolveAllUses(svgRoot, options = {}) {
1110
1149
  const useElements = svgRoot.querySelectorAll('use');
1111
1150
  const resolved = [];
1112
1151
 
1152
+ // Helper to get the next use reference from a definition
1153
+ const getUseRef = (id) => {
1154
+ const target = defs[id];
1155
+ if (!target) return null;
1156
+
1157
+ // Check if target is itself a use element
1158
+ if (target.type === 'use') {
1159
+ return target.href;
1160
+ }
1161
+
1162
+ // Check if target contains use elements in its children
1163
+ if (target.children && target.children.length > 0) {
1164
+ for (const child of target.children) {
1165
+ if (child.type === 'use') {
1166
+ return child.href;
1167
+ }
1168
+ }
1169
+ }
1170
+
1171
+ return null;
1172
+ };
1173
+
1113
1174
  for (const useEl of useElements) {
1114
1175
  const useData = parseUseElement(useEl);
1176
+
1177
+ // Check for circular reference before attempting to resolve
1178
+ if (hasCircularReference(useData.href, getUseRef)) {
1179
+ console.warn(`Circular use reference detected: #${useData.href}, skipping resolution`);
1180
+ continue;
1181
+ }
1182
+
1115
1183
  const result = resolveUse(useData, defs, options);
1116
1184
  if (result) {
1117
1185
  resolved.push({