@emasoft/svg-matrix 1.0.5 → 1.0.7

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.
@@ -0,0 +1,1126 @@
1
+ /**
2
+ * Use/Symbol Resolver Module - Expand use elements and symbols
3
+ *
4
+ * Resolves SVG <use> elements by inlining their referenced content
5
+ * with proper transforms, viewBox handling, and style inheritance.
6
+ *
7
+ * Supports:
8
+ * - <use> referencing any element by id
9
+ * - <symbol> with viewBox and preserveAspectRatio
10
+ * - x, y, width, height on use elements
11
+ * - Recursive use resolution
12
+ * - Style inheritance
13
+ *
14
+ * @module use-symbol-resolver
15
+ */
16
+
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
+
23
+ Decimal.set({ precision: 80 });
24
+
25
+ const D = x => (x instanceof Decimal ? x : new Decimal(x));
26
+
27
+ /**
28
+ * Parse SVG <use> element to structured data.
29
+ *
30
+ * SVG <use> elements reference other elements via href/xlink:href and can apply
31
+ * additional transforms and positioning. The x,y attributes translate the referenced
32
+ * content. When referencing a <symbol>, width/height establish the viewport for
33
+ * viewBox calculations.
34
+ *
35
+ * Handles both modern href and legacy xlink:href attributes, preferring href.
36
+ * The '#' prefix is stripped from internal references to get the target id.
37
+ *
38
+ * @param {Element} useElement - SVG <use> DOM element to parse
39
+ * @returns {Object} Parsed use data with the following properties:
40
+ * - href {string} - Target element id (without '#' prefix)
41
+ * - x {number} - Horizontal translation offset (default: 0)
42
+ * - y {number} - Vertical translation offset (default: 0)
43
+ * - width {number|null} - Viewport width for symbol references (null if not specified)
44
+ * - height {number|null} - Viewport height for symbol references (null if not specified)
45
+ * - transform {string|null} - Additional transform attribute (null if not specified)
46
+ * - style {Object} - Extracted style attributes that inherit to referenced content
47
+ *
48
+ * @example
49
+ * // Parse a use element referencing a symbol with positioning
50
+ * const useEl = document.querySelector('use');
51
+ * // <use href="#icon" x="10" y="20" width="100" height="100" fill="red"/>
52
+ * const parsed = parseUseElement(useEl);
53
+ * // {
54
+ * // href: 'icon',
55
+ * // x: 10, y: 20,
56
+ * // width: 100, height: 100,
57
+ * // transform: null,
58
+ * // style: { fill: 'red', stroke: null, ... }
59
+ * // }
60
+ */
61
+ export function parseUseElement(useElement) {
62
+ const href = useElement.getAttribute('href') ||
63
+ useElement.getAttribute('xlink:href') || '';
64
+
65
+ return {
66
+ href: href.startsWith('#') ? href.slice(1) : href,
67
+ x: parseFloat(useElement.getAttribute('x') || '0'),
68
+ y: parseFloat(useElement.getAttribute('y') || '0'),
69
+ width: useElement.getAttribute('width') ?
70
+ parseFloat(useElement.getAttribute('width')) : null,
71
+ height: useElement.getAttribute('height') ?
72
+ parseFloat(useElement.getAttribute('height')) : null,
73
+ transform: useElement.getAttribute('transform') || null,
74
+ style: extractStyleAttributes(useElement)
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Parse SVG <symbol> element to structured data.
80
+ *
81
+ * SVG <symbol> elements are container elements typically defined in <defs> and
82
+ * instantiated via <use>. They support viewBox for coordinate system control
83
+ * and preserveAspectRatio for scaling behavior.
84
+ *
85
+ * The viewBox defines the symbol's internal coordinate system (minX minY width height).
86
+ * When a <use> references a symbol with width/height, the viewBox is mapped to that
87
+ * viewport according to preserveAspectRatio rules.
88
+ *
89
+ * refX/refY provide an optional reference point for alignment (similar to markers).
90
+ *
91
+ * @param {Element} symbolElement - SVG <symbol> DOM element to parse
92
+ * @returns {Object} Parsed symbol data with the following properties:
93
+ * - id {string} - Symbol's id attribute
94
+ * - viewBox {string|null} - Raw viewBox attribute string
95
+ * - viewBoxParsed {Object|undefined} - Parsed viewBox with x, y, width, height (only if valid)
96
+ * - preserveAspectRatio {string} - How to scale/align viewBox (default: 'xMidYMid meet')
97
+ * - children {Array} - Parsed child elements
98
+ * - refX {number} - Reference point X coordinate (default: 0)
99
+ * - refY {number} - Reference point Y coordinate (default: 0)
100
+ *
101
+ * @example
102
+ * // Parse a symbol with viewBox
103
+ * const symbolEl = document.querySelector('symbol');
104
+ * // <symbol id="icon" viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet">
105
+ * // <circle cx="12" cy="12" r="10"/>
106
+ * // </symbol>
107
+ * const parsed = parseSymbolElement(symbolEl);
108
+ * // {
109
+ * // id: 'icon',
110
+ * // viewBox: '0 0 24 24',
111
+ * // viewBoxParsed: { x: 0, y: 0, width: 24, height: 24 },
112
+ * // preserveAspectRatio: 'xMidYMid meet',
113
+ * // children: [{ type: 'circle', cx: 12, cy: 12, r: 10, ... }],
114
+ * // refX: 0, refY: 0
115
+ * // }
116
+ */
117
+ export function parseSymbolElement(symbolElement) {
118
+ const data = {
119
+ id: symbolElement.getAttribute('id') || '',
120
+ viewBox: symbolElement.getAttribute('viewBox') || null,
121
+ preserveAspectRatio: symbolElement.getAttribute('preserveAspectRatio') || 'xMidYMid meet',
122
+ children: [],
123
+ refX: parseFloat(symbolElement.getAttribute('refX') || '0'),
124
+ refY: parseFloat(symbolElement.getAttribute('refY') || '0')
125
+ };
126
+
127
+ // Parse viewBox
128
+ if (data.viewBox) {
129
+ const parts = data.viewBox.trim().split(/[\s,]+/).map(Number);
130
+ if (parts.length === 4) {
131
+ data.viewBoxParsed = {
132
+ x: parts[0],
133
+ y: parts[1],
134
+ width: parts[2],
135
+ height: parts[3]
136
+ };
137
+ }
138
+ }
139
+
140
+ // Parse children
141
+ for (const child of symbolElement.children) {
142
+ data.children.push(parseChildElement(child));
143
+ }
144
+
145
+ return data;
146
+ }
147
+
148
+ /**
149
+ * Parse any SVG element to structured data for use/symbol resolution.
150
+ *
151
+ * Recursively parses SVG elements extracting geometry-specific attributes
152
+ * based on element type (rect, circle, path, etc.). Used to build a structured
153
+ * representation of symbol contents and referenced elements.
154
+ *
155
+ * Handles:
156
+ * - Shape elements (rect, circle, ellipse, line, path, polygon, polyline)
157
+ * - Container elements (g - group)
158
+ * - Reference elements (use - for nested use resolution)
159
+ * - Common attributes (id, transform, style)
160
+ *
161
+ * @param {Element} element - SVG DOM element to parse
162
+ * @returns {Object} Parsed element data with the following base properties:
163
+ * - type {string} - Element tag name (lowercase)
164
+ * - id {string|null} - Element's id attribute
165
+ * - transform {string|null} - Element's transform attribute
166
+ * - style {Object} - Extracted style attributes (fill, stroke, etc.)
167
+ * Plus element-specific geometry properties:
168
+ * - rect: x, y, width, height, rx, ry
169
+ * - circle: cx, cy, r
170
+ * - ellipse: cx, cy, rx, ry
171
+ * - path: d
172
+ * - polygon/polyline: points
173
+ * - line: x1, y1, x2, y2
174
+ * - g: children (array of parsed child elements)
175
+ * - use: href, x, y, width, height
176
+ *
177
+ * @example
178
+ * // Parse a circle element
179
+ * const circleEl = document.querySelector('circle');
180
+ * // <circle id="c1" cx="50" cy="50" r="20" fill="blue" transform="rotate(45)"/>
181
+ * const parsed = parseChildElement(circleEl);
182
+ * // {
183
+ * // type: 'circle',
184
+ * // id: 'c1',
185
+ * // cx: 50, cy: 50, r: 20,
186
+ * // transform: 'rotate(45)',
187
+ * // style: { fill: 'blue', ... }
188
+ * // }
189
+ *
190
+ * @example
191
+ * // Parse a group with nested elements
192
+ * const groupEl = document.querySelector('g');
193
+ * // <g id="group1" transform="translate(10, 20)">
194
+ * // <rect x="0" y="0" width="50" height="50"/>
195
+ * // <circle cx="25" cy="25" r="10"/>
196
+ * // </g>
197
+ * const parsed = parseChildElement(groupEl);
198
+ * // {
199
+ * // type: 'g',
200
+ * // id: 'group1',
201
+ * // transform: 'translate(10, 20)',
202
+ * // children: [
203
+ * // { type: 'rect', x: 0, y: 0, width: 50, height: 50, ... },
204
+ * // { type: 'circle', cx: 25, cy: 25, r: 10, ... }
205
+ * // ],
206
+ * // style: { ... }
207
+ * // }
208
+ */
209
+ export function parseChildElement(element) {
210
+ const tagName = element.tagName.toLowerCase();
211
+
212
+ const data = {
213
+ type: tagName,
214
+ id: element.getAttribute('id') || null,
215
+ transform: element.getAttribute('transform') || null,
216
+ style: extractStyleAttributes(element)
217
+ };
218
+
219
+ switch (tagName) {
220
+ case 'rect':
221
+ data.x = parseFloat(element.getAttribute('x') || '0');
222
+ data.y = parseFloat(element.getAttribute('y') || '0');
223
+ data.width = parseFloat(element.getAttribute('width') || '0');
224
+ data.height = parseFloat(element.getAttribute('height') || '0');
225
+ data.rx = parseFloat(element.getAttribute('rx') || '0');
226
+ data.ry = parseFloat(element.getAttribute('ry') || '0');
227
+ break;
228
+ case 'circle':
229
+ data.cx = parseFloat(element.getAttribute('cx') || '0');
230
+ data.cy = parseFloat(element.getAttribute('cy') || '0');
231
+ data.r = parseFloat(element.getAttribute('r') || '0');
232
+ break;
233
+ case 'ellipse':
234
+ data.cx = parseFloat(element.getAttribute('cx') || '0');
235
+ data.cy = parseFloat(element.getAttribute('cy') || '0');
236
+ data.rx = parseFloat(element.getAttribute('rx') || '0');
237
+ data.ry = parseFloat(element.getAttribute('ry') || '0');
238
+ break;
239
+ case 'path':
240
+ data.d = element.getAttribute('d') || '';
241
+ break;
242
+ case 'polygon':
243
+ data.points = element.getAttribute('points') || '';
244
+ break;
245
+ case 'polyline':
246
+ data.points = element.getAttribute('points') || '';
247
+ break;
248
+ case 'line':
249
+ data.x1 = parseFloat(element.getAttribute('x1') || '0');
250
+ data.y1 = parseFloat(element.getAttribute('y1') || '0');
251
+ data.x2 = parseFloat(element.getAttribute('x2') || '0');
252
+ data.y2 = parseFloat(element.getAttribute('y2') || '0');
253
+ break;
254
+ case 'g':
255
+ data.children = [];
256
+ for (const child of element.children) {
257
+ data.children.push(parseChildElement(child));
258
+ }
259
+ break;
260
+ case 'use':
261
+ data.href = (element.getAttribute('href') ||
262
+ element.getAttribute('xlink:href') || '').replace('#', '');
263
+ data.x = parseFloat(element.getAttribute('x') || '0');
264
+ data.y = parseFloat(element.getAttribute('y') || '0');
265
+ data.width = element.getAttribute('width') ?
266
+ parseFloat(element.getAttribute('width')) : null;
267
+ data.height = element.getAttribute('height') ?
268
+ parseFloat(element.getAttribute('height')) : null;
269
+ break;
270
+ }
271
+
272
+ return data;
273
+ }
274
+
275
+ /**
276
+ * Extract presentation style attributes from an SVG element.
277
+ *
278
+ * Extracts paint and display properties that can be inherited from <use>
279
+ * elements to their referenced content. These attributes participate in
280
+ * the CSS cascade and inheritance model.
281
+ *
282
+ * When a <use> element has fill="red", that fill inherits to the referenced
283
+ * content unless the content has its own fill attribute.
284
+ *
285
+ * @param {Element} element - SVG element to extract styles from
286
+ * @returns {Object} Style attributes object with the following properties:
287
+ * - fill {string|null} - Fill color/paint server
288
+ * - stroke {string|null} - Stroke color/paint server
289
+ * - strokeWidth {string|null} - Stroke width
290
+ * - opacity {string|null} - Overall opacity (0-1)
291
+ * - fillOpacity {string|null} - Fill opacity (0-1)
292
+ * - strokeOpacity {string|null} - Stroke opacity (0-1)
293
+ * - visibility {string|null} - Visibility ('visible', 'hidden', 'collapse')
294
+ * - display {string|null} - Display property ('none', 'inline', etc.)
295
+ * All properties are null if attribute is not present.
296
+ *
297
+ * @example
298
+ * // Extract styles from a use element
299
+ * const useEl = document.querySelector('use');
300
+ * // <use href="#icon" fill="red" stroke="blue" stroke-width="2" opacity="0.8"/>
301
+ * const styles = extractStyleAttributes(useEl);
302
+ * // {
303
+ * // fill: 'red',
304
+ * // stroke: 'blue',
305
+ * // strokeWidth: '2',
306
+ * // opacity: '0.8',
307
+ * // fillOpacity: null,
308
+ * // strokeOpacity: null,
309
+ * // visibility: null,
310
+ * // display: null
311
+ * // }
312
+ */
313
+ export function extractStyleAttributes(element) {
314
+ return {
315
+ fill: element.getAttribute('fill'),
316
+ stroke: element.getAttribute('stroke'),
317
+ strokeWidth: element.getAttribute('stroke-width'),
318
+ opacity: element.getAttribute('opacity'),
319
+ fillOpacity: element.getAttribute('fill-opacity'),
320
+ strokeOpacity: element.getAttribute('stroke-opacity'),
321
+ visibility: element.getAttribute('visibility'),
322
+ display: element.getAttribute('display')
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Calculate the transform matrix to map a viewBox to a target viewport.
328
+ *
329
+ * This implements the SVG viewBox mapping algorithm that scales and aligns
330
+ * the viewBox coordinate system to fit within the target width/height according
331
+ * to the preserveAspectRatio specification.
332
+ *
333
+ * The viewBox defines a rectangle in user space (minX, minY, width, height) that
334
+ * should be mapped to the viewport rectangle (0, 0, targetWidth, targetHeight).
335
+ *
336
+ * preserveAspectRatio controls:
337
+ * - Alignment: where to position the viewBox within viewport (xMin/xMid/xMax, YMin/YMid/YMax)
338
+ * - Scaling: 'meet' (fit inside, letterbox) or 'slice' (cover, crop)
339
+ * - 'none' allows non-uniform scaling (stretch/squash)
340
+ *
341
+ * @param {Object} viewBox - Parsed viewBox with the following properties:
342
+ * - x {number} - Minimum X of viewBox coordinate system
343
+ * - y {number} - Minimum Y of viewBox coordinate system
344
+ * - width {number} - Width of viewBox rectangle
345
+ * - height {number} - Height of viewBox rectangle
346
+ * @param {number} targetWidth - Target viewport width in user units
347
+ * @param {number} targetHeight - Target viewport height in user units
348
+ * @param {string} [preserveAspectRatio='xMidYMid meet'] - Aspect ratio preservation mode
349
+ * Format: '[none|align] [meet|slice]'
350
+ * Align values: xMinYMin, xMidYMin, xMaxYMin, xMinYMid, xMidYMid, xMaxYMid, xMinYMax, xMidYMax, xMaxYMax
351
+ * @returns {Matrix} 3x3 affine transform matrix mapping viewBox to viewport
352
+ *
353
+ * @example
354
+ * // Symbol with viewBox="0 0 100 100" used with width=200, height=150
355
+ * const viewBox = { x: 0, y: 0, width: 100, height: 100 };
356
+ * const transform = calculateViewBoxTransform(viewBox, 200, 150, 'xMidYMid meet');
357
+ * // Result: uniform scale of 1.5 (min of 200/100, 150/100)
358
+ * // Centered in 200x150 viewport: translate(25, 0) then scale(1.5, 1.5)
359
+ *
360
+ * @example
361
+ * // Non-uniform scaling with 'none'
362
+ * const viewBox = { x: 0, y: 0, width: 100, height: 50 };
363
+ * const transform = calculateViewBoxTransform(viewBox, 200, 200, 'none');
364
+ * // Result: scale(2, 4) - stretches to fill viewport
365
+ *
366
+ * @example
367
+ * // 'slice' mode crops to fill viewport
368
+ * const viewBox = { x: 0, y: 0, width: 100, height: 100 };
369
+ * const transform = calculateViewBoxTransform(viewBox, 200, 150, 'xMidYMid slice');
370
+ * // Result: uniform scale of 2.0 (max of 200/100, 150/100)
371
+ * // Centered: translate(0, -25) then scale(2, 2) - height extends beyond viewport
372
+ */
373
+ export function calculateViewBoxTransform(viewBox, targetWidth, targetHeight, preserveAspectRatio = 'xMidYMid meet') {
374
+ if (!viewBox || !targetWidth || !targetHeight) {
375
+ return Matrix.identity(3);
376
+ }
377
+
378
+ const vbW = viewBox.width;
379
+ const vbH = viewBox.height;
380
+ const vbX = viewBox.x;
381
+ const vbY = viewBox.y;
382
+
383
+ if (vbW <= 0 || vbH <= 0) {
384
+ return Matrix.identity(3);
385
+ }
386
+
387
+ // Parse preserveAspectRatio
388
+ const parts = preserveAspectRatio.trim().split(/\s+/);
389
+ const align = parts[0] || 'xMidYMid';
390
+ const meetOrSlice = parts[1] || 'meet';
391
+
392
+ if (align === 'none') {
393
+ // Non-uniform scaling
394
+ const scaleX = targetWidth / vbW;
395
+ const scaleY = targetHeight / vbH;
396
+ return Transforms2D.translation(-vbX * scaleX, -vbY * scaleY)
397
+ .mul(Transforms2D.scale(scaleX, scaleY));
398
+ }
399
+
400
+ // Uniform scaling
401
+ const scaleX = targetWidth / vbW;
402
+ const scaleY = targetHeight / vbH;
403
+ let scale;
404
+
405
+ if (meetOrSlice === 'slice') {
406
+ scale = Math.max(scaleX, scaleY);
407
+ } else {
408
+ scale = Math.min(scaleX, scaleY);
409
+ }
410
+
411
+ // Calculate alignment offsets
412
+ let tx = -vbX * scale;
413
+ let ty = -vbY * scale;
414
+
415
+ const scaledWidth = vbW * scale;
416
+ const scaledHeight = vbH * scale;
417
+
418
+ // X alignment
419
+ if (align.includes('xMid')) {
420
+ tx += (targetWidth - scaledWidth) / 2;
421
+ } else if (align.includes('xMax')) {
422
+ tx += targetWidth - scaledWidth;
423
+ }
424
+
425
+ // Y alignment
426
+ if (align.includes('YMid')) {
427
+ ty += (targetHeight - scaledHeight) / 2;
428
+ } else if (align.includes('YMax')) {
429
+ ty += targetHeight - scaledHeight;
430
+ }
431
+
432
+ return Transforms2D.translation(tx, ty)
433
+ .mul(Transforms2D.scale(scale, scale));
434
+ }
435
+
436
+ /**
437
+ * Resolve a <use> element by expanding its referenced content with transforms.
438
+ *
439
+ * This is the core use/symbol resolution algorithm. It:
440
+ * 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
442
+ * 3. Recursively resolves nested <use> elements (with depth limit)
443
+ * 4. Propagates style inheritance from <use> to referenced content
444
+ *
445
+ * 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
448
+ * - Third: apply viewBox→viewport mapping (symbols only)
449
+ *
450
+ * For symbols with viewBox, if <use> specifies width/height, those establish the
451
+ * viewport; otherwise the symbol's viewBox width/height is used.
452
+ *
453
+ * @param {Object} useData - Parsed <use> element data from parseUseElement()
454
+ * @param {Object} defs - Map of element id → parsed element data (from buildDefsMap())
455
+ * @param {Object} [options={}] - Resolution options
456
+ * @param {number} [options.maxDepth=10] - Maximum nesting depth for recursive use resolution
457
+ * Prevents infinite recursion from circular references
458
+ * @returns {Object|null} Resolved use data with the following structure:
459
+ * - element {Object} - The referenced target element (symbol, shape, group, etc.)
460
+ * - transform {Matrix} - Composed 3x3 transform matrix to apply to all children
461
+ * - children {Array} - Array of resolved child objects, each with:
462
+ * - element {Object} - Child element data
463
+ * - transform {Matrix} - Child's local transform
464
+ * - children {Array} - (if child is nested use) Recursively resolved children
465
+ * - inheritedStyle {Object} - Style attributes from <use> that cascade to children
466
+ * Returns null if target not found or max depth exceeded.
467
+ *
468
+ * @example
469
+ * // Simple shape reference
470
+ * const defs = {
471
+ * 'myCircle': { type: 'circle', cx: 0, cy: 0, r: 10 }
472
+ * };
473
+ * const useData = { href: 'myCircle', x: 100, y: 50, style: { fill: 'red' } };
474
+ * const resolved = resolveUse(useData, defs);
475
+ * // {
476
+ * // element: { type: 'circle', cx: 0, cy: 0, r: 10 },
477
+ * // transform: Matrix(translate(100, 50)),
478
+ * // children: [{ element: {...}, transform: identity }],
479
+ * // inheritedStyle: { fill: 'red' }
480
+ * // }
481
+ *
482
+ * @example
483
+ * // Symbol with viewBox
484
+ * const defs = {
485
+ * 'icon': {
486
+ * type: 'symbol',
487
+ * viewBoxParsed: { x: 0, y: 0, width: 24, height: 24 },
488
+ * preserveAspectRatio: 'xMidYMid meet',
489
+ * children: [{ type: 'path', d: 'M...' }]
490
+ * }
491
+ * };
492
+ * const useData = { href: 'icon', x: 10, y: 20, width: 48, height: 48 };
493
+ * const resolved = resolveUse(useData, defs);
494
+ * // transform composes: translate(10,20) → viewBox mapping (scale 2x)
495
+ *
496
+ * @example
497
+ * // Nested use (use referencing another use)
498
+ * const defs = {
499
+ * 'shape': { type: 'rect', x: 0, y: 0, width: 10, height: 10 },
500
+ * 'ref1': { type: 'use', href: 'shape', x: 5, y: 5 }
501
+ * };
502
+ * const useData = { href: 'ref1', x: 100, y: 100 };
503
+ * const resolved = resolveUse(useData, defs);
504
+ * // Recursively resolves ref1 → shape, composing transforms
505
+ */
506
+ export function resolveUse(useData, defs, options = {}) {
507
+ const { maxDepth = 10 } = options;
508
+
509
+ if (maxDepth <= 0) {
510
+ return null; // Prevent infinite recursion
511
+ }
512
+
513
+ const target = defs[useData.href];
514
+ if (!target) {
515
+ return null;
516
+ }
517
+
518
+ // Calculate base transform from x, y
519
+ let transform = Transforms2D.translation(useData.x, useData.y);
520
+
521
+ // Apply use element's transform if present
522
+ if (useData.transform) {
523
+ const useTransform = ClipPathResolver.parseTransform(useData.transform);
524
+ transform = transform.mul(useTransform);
525
+ }
526
+
527
+ // Handle symbol with viewBox
528
+ if (target.type === 'symbol' && target.viewBoxParsed) {
529
+ const width = useData.width || target.viewBoxParsed.width;
530
+ const height = useData.height || target.viewBoxParsed.height;
531
+
532
+ const viewBoxTransform = calculateViewBoxTransform(
533
+ target.viewBoxParsed,
534
+ width,
535
+ height,
536
+ target.preserveAspectRatio
537
+ );
538
+
539
+ transform = transform.mul(viewBoxTransform);
540
+ }
541
+
542
+ // Resolve children
543
+ const resolvedChildren = [];
544
+ const children = target.children || [target];
545
+
546
+ for (const child of children) {
547
+ if (child.type === 'use') {
548
+ // Recursive resolution
549
+ const resolved = resolveUse(child, defs, { maxDepth: maxDepth - 1 });
550
+ if (resolved) {
551
+ resolvedChildren.push(resolved);
552
+ }
553
+ } else {
554
+ resolvedChildren.push({
555
+ element: child,
556
+ transform: Matrix.identity(3)
557
+ });
558
+ }
559
+ }
560
+
561
+ return {
562
+ element: target,
563
+ transform,
564
+ children: resolvedChildren,
565
+ inheritedStyle: useData.style
566
+ };
567
+ }
568
+
569
+ /**
570
+ * Flatten a resolved <use> element tree to an array of transformed polygons.
571
+ *
572
+ * Recursively traverses the resolved use element hierarchy, converting all
573
+ * shapes to polygons and applying accumulated transforms. This produces a
574
+ * flat list of polygons ready for rendering, clipping, or geometric operations.
575
+ *
576
+ * Each shape element (rect, circle, path, etc.) is converted to a polygon
577
+ * approximation with the specified number of curve samples. Transforms are
578
+ * composed from parent to child.
579
+ *
580
+ * Style attributes are merged during flattening, combining inherited styles
581
+ * from <use> elements with element-specific styles (element styles take precedence).
582
+ *
583
+ * @param {Object} resolved - Resolved use data from resolveUse(), with structure:
584
+ * - transform {Matrix} - Transform to apply to all children
585
+ * - children {Array} - Child elements or nested resolved uses
586
+ * - inheritedStyle {Object} - Style attributes to cascade to children
587
+ * @param {number} [samples=20] - Number of samples for curve approximation
588
+ * Higher values produce smoother polygons for curved shapes (circles, arcs, etc.)
589
+ * @returns {Array<Object>} Array of polygon objects, each with:
590
+ * - polygon {Array<{x, y}>} - Array of transformed vertex points
591
+ * - style {Object} - Merged style attributes (inherited + element-specific)
592
+ *
593
+ * @example
594
+ * // Flatten a simple resolved use
595
+ * const defs = { 'c': { type: 'circle', cx: 10, cy: 10, r: 5, style: { fill: 'blue' } } };
596
+ * const useData = { href: 'c', x: 100, y: 50, style: { stroke: 'red' } };
597
+ * const resolved = resolveUse(useData, defs);
598
+ * const polygons = flattenResolvedUse(resolved, 20);
599
+ * // [
600
+ * // {
601
+ * // polygon: [{x: 115, y: 50}, {x: 114.9, y: 51.5}, ...], // 20 points approximating circle
602
+ * // style: { fill: 'blue', stroke: 'red', ... } // merged styles
603
+ * // }
604
+ * // ]
605
+ *
606
+ * @example
607
+ * // Flatten nested uses with composed transforms
608
+ * const resolved = { // Complex nested structure
609
+ * transform: Matrix(translate(10, 20)),
610
+ * children: [
611
+ * {
612
+ * element: { type: 'rect', x: 0, y: 0, width: 50, height: 30 },
613
+ * transform: Matrix(rotate(45))
614
+ * }
615
+ * ],
616
+ * inheritedStyle: { fill: 'green' }
617
+ * };
618
+ * const polygons = flattenResolvedUse(resolved);
619
+ * // Rectangle converted to 4-point polygon, transformed by translate→rotate
620
+ */
621
+ export function flattenResolvedUse(resolved, samples = 20) {
622
+ const results = [];
623
+
624
+ if (!resolved) return results;
625
+
626
+ for (const child of resolved.children) {
627
+ const childTransform = resolved.transform.mul(child.transform);
628
+ const element = child.element;
629
+
630
+ if (child.children) {
631
+ // Recursive flattening
632
+ const nested = flattenResolvedUse(child, samples);
633
+ for (const n of nested) {
634
+ n.polygon = n.polygon.map(p => {
635
+ const [x, y] = Transforms2D.applyTransform(resolved.transform, p.x, p.y);
636
+ return { x, y };
637
+ });
638
+ results.push(n);
639
+ }
640
+ } else {
641
+ // Convert element to polygon
642
+ const polygon = elementToPolygon(element, childTransform, samples);
643
+ if (polygon.length >= 3) {
644
+ results.push({
645
+ polygon,
646
+ style: mergeStyles(resolved.inheritedStyle, element.style)
647
+ });
648
+ }
649
+ }
650
+ }
651
+
652
+ return results;
653
+ }
654
+
655
+ /**
656
+ * Convert an SVG element to a transformed polygon approximation.
657
+ *
658
+ * Delegates to ClipPathResolver.shapeToPolygon() for the element→polygon
659
+ * conversion, then applies the transform matrix to all vertices.
660
+ *
661
+ * Supports all standard SVG shapes:
662
+ * - rect, circle, ellipse: converted to polygons with curved edges sampled
663
+ * - path: parsed and sampled (curves approximated)
664
+ * - polygon, polyline: parsed directly
665
+ * - line: converted to 2-point polygon
666
+ *
667
+ * @param {Object} element - Parsed element data from parseChildElement()
668
+ * Must have 'type' property and geometry attributes (x, y, cx, cy, d, points, etc.)
669
+ * @param {Matrix} transform - 3x3 affine transform matrix to apply to vertices
670
+ * @param {number} [samples=20] - Number of samples for curve approximation
671
+ * Used for circles, ellipses, path curves, rounded rect corners, etc.
672
+ * @returns {Array<{x, y}>} Array of transformed polygon vertices
673
+ * Empty array if element cannot be converted to polygon.
674
+ *
675
+ * @example
676
+ * // Convert circle to transformed polygon
677
+ * const element = { type: 'circle', cx: 10, cy: 10, r: 5 };
678
+ * const transform = Transforms2D.translation(100, 50).mul(Transforms2D.scale(2, 2));
679
+ * const polygon = elementToPolygon(element, transform, 16);
680
+ * // Returns 16 points approximating circle at (10,10) r=5, then scaled 2x and translated to (100,50)
681
+ *
682
+ * @example
683
+ * // Convert rectangle (becomes 4-point polygon)
684
+ * const element = { type: 'rect', x: 0, y: 0, width: 50, height: 30, rx: 0, ry: 0 };
685
+ * const transform = Matrix.identity(3);
686
+ * const polygon = elementToPolygon(element, transform);
687
+ * // [{ x: 0, y: 0 }, { x: 50, y: 0 }, { x: 50, y: 30 }, { x: 0, y: 30 }]
688
+ */
689
+ export function elementToPolygon(element, transform, samples = 20) {
690
+ // Use ClipPathResolver's shapeToPolygon
691
+ let polygon = ClipPathResolver.shapeToPolygon(element, null, samples);
692
+
693
+ // Apply transform
694
+ if (transform && polygon.length > 0) {
695
+ polygon = polygon.map(p => {
696
+ const [x, y] = Transforms2D.applyTransform(transform, p.x, p.y);
697
+ return { x, y };
698
+ });
699
+ }
700
+
701
+ return polygon;
702
+ }
703
+
704
+ /**
705
+ * Merge inherited styles from <use> with element-specific styles.
706
+ *
707
+ * Implements the SVG style inheritance cascade where <use> element styles
708
+ * propagate to referenced content, but element-specific styles take precedence.
709
+ *
710
+ * This follows the CSS cascade model:
711
+ * - Element's own attributes have highest priority
712
+ * - Inherited attributes from <use> fill in gaps
713
+ * - null/undefined element values allow inheritance
714
+ * - Explicit element values (even if same as inherited) prevent inheritance
715
+ *
716
+ * Example: <use fill="red" href="#shape"/> references <circle fill="blue"/>
717
+ * Result: circle gets fill="blue" (element's own style wins)
718
+ *
719
+ * Example: <use fill="red" href="#shape"/> references <circle/>
720
+ * Result: circle gets fill="red" (inherits from use)
721
+ *
722
+ * @param {Object} inherited - Style attributes from <use> element (from extractStyleAttributes)
723
+ * May be null or undefined. Properties with null values are not inherited.
724
+ * @param {Object} element - Element's own style attributes (from extractStyleAttributes)
725
+ * Properties with non-null values take precedence over inherited values.
726
+ * @returns {Object} Merged style object where:
727
+ * - Element properties are preserved if not null/undefined
728
+ * - Inherited properties fill in where element has null/undefined
729
+ * - Result contains all properties from both objects
730
+ *
731
+ * @example
732
+ * // Element style overrides inherited
733
+ * const inherited = { fill: 'red', stroke: 'blue', opacity: '0.5' };
734
+ * const element = { fill: 'green', stroke: null, strokeWidth: '2' };
735
+ * const merged = mergeStyles(inherited, element);
736
+ * // {
737
+ * // fill: 'green', // element overrides
738
+ * // stroke: 'blue', // inherited (element was null)
739
+ * // opacity: '0.5', // inherited (element didn't have it)
740
+ * // strokeWidth: '2' // from element
741
+ * // }
742
+ *
743
+ * @example
744
+ * // No inherited styles
745
+ * const merged = mergeStyles(null, { fill: 'blue' });
746
+ * // { fill: 'blue' }
747
+ */
748
+ export function mergeStyles(inherited, element) {
749
+ const result = { ...element };
750
+
751
+ for (const [key, value] of Object.entries(inherited || {})) {
752
+ // Inherit if value is not null and element doesn't have a value (null or undefined)
753
+ if (value !== null && (result[key] === null || result[key] === undefined)) {
754
+ result[key] = value;
755
+ }
756
+ }
757
+
758
+ return result;
759
+ }
760
+
761
+ /**
762
+ * Calculate the axis-aligned bounding box of a resolved <use> element.
763
+ *
764
+ * Flattens the entire use/symbol hierarchy to polygons, then computes
765
+ * the minimum rectangle that contains all transformed vertices.
766
+ *
767
+ * This is useful for:
768
+ * - Determining the rendered extent of a use element
769
+ * - Layout calculations
770
+ * - Viewport fitting
771
+ * - Collision detection
772
+ *
773
+ * The bounding box is axis-aligned (edges parallel to X/Y axes) in the
774
+ * final coordinate space after all transforms have been applied.
775
+ *
776
+ * @param {Object} resolved - Resolved use data from resolveUse()
777
+ * Contains the element tree with composed transforms
778
+ * @param {number} [samples=20] - Number of samples for curve approximation
779
+ * Higher values give tighter bounds for curved shapes
780
+ * @returns {Object} Bounding box with properties:
781
+ * - x {number} - Minimum X coordinate (left edge)
782
+ * - y {number} - Minimum Y coordinate (top edge)
783
+ * - width {number} - Width of bounding box
784
+ * - height {number} - Height of bounding box
785
+ * Returns {x:0, y:0, width:0, height:0} if no polygons or resolved is null.
786
+ *
787
+ * @example
788
+ * // Get bbox of a circle use element
789
+ * const defs = { 'c': { type: 'circle', cx: 0, cy: 0, r: 10 } };
790
+ * const useData = { href: 'c', x: 100, y: 50, style: {} };
791
+ * const resolved = resolveUse(useData, defs);
792
+ * const bbox = getResolvedBBox(resolved, 20);
793
+ * // { x: 90, y: 40, width: 20, height: 20 }
794
+ * // Circle at (0,0) r=10, translated to (100,50), bounds from 90 to 110, 40 to 60
795
+ *
796
+ * @example
797
+ * // Get bbox of rotated rectangle
798
+ * const defs = { 'r': { type: 'rect', x: 0, y: 0, width: 100, height: 50, transform: 'rotate(45)' } };
799
+ * const useData = { href: 'r', x: 0, y: 0 };
800
+ * const resolved = resolveUse(useData, defs);
801
+ * const bbox = getResolvedBBox(resolved);
802
+ * // Axis-aligned bbox enclosing the rotated rectangle (wider than original)
803
+ */
804
+ export function getResolvedBBox(resolved, samples = 20) {
805
+ const polygons = flattenResolvedUse(resolved, samples);
806
+
807
+ let minX = Infinity;
808
+ let minY = Infinity;
809
+ let maxX = -Infinity;
810
+ let maxY = -Infinity;
811
+
812
+ for (const { polygon } of polygons) {
813
+ for (const p of polygon) {
814
+ const x = Number(p.x);
815
+ const y = Number(p.y);
816
+ minX = Math.min(minX, x);
817
+ minY = Math.min(minY, y);
818
+ maxX = Math.max(maxX, x);
819
+ maxY = Math.max(maxY, y);
820
+ }
821
+ }
822
+
823
+ if (minX === Infinity) {
824
+ return { x: 0, y: 0, width: 0, height: 0 };
825
+ }
826
+
827
+ return {
828
+ x: minX,
829
+ y: minY,
830
+ width: maxX - minX,
831
+ height: maxY - minY
832
+ };
833
+ }
834
+
835
+ /**
836
+ * Apply a clipping polygon to a resolved <use> element.
837
+ *
838
+ * Flattens the use element to polygons, then computes the intersection
839
+ * of each polygon with the clip polygon using the Sutherland-Hodgman
840
+ * algorithm. This implements SVG clipPath functionality.
841
+ *
842
+ * The clip polygon should be in the same coordinate space as the
843
+ * resolved use element (i.e., after all transforms have been applied).
844
+ *
845
+ * Clipping can produce multiple output polygons per input polygon if
846
+ * the clip path splits a shape into disjoint regions.
847
+ *
848
+ * Degenerate results (< 3 vertices) are filtered out automatically.
849
+ *
850
+ * @param {Object} resolved - Resolved use data from resolveUse()
851
+ * Contains the element tree with composed transforms
852
+ * @param {Array<{x, y}>} clipPolygon - Clipping polygon vertices
853
+ * Must be a closed polygon (clockwise or counter-clockwise)
854
+ * @param {number} [samples=20] - Number of samples for curve approximation
855
+ * Affects the input polygons (curves in the use element)
856
+ * @returns {Array<Object>} Array of clipped polygon objects, each with:
857
+ * - polygon {Array<{x, y}>} - Clipped polygon vertices (intersection result)
858
+ * - style {Object} - Preserved style attributes from original polygon
859
+ * Only polygons with ≥3 vertices are included.
860
+ *
861
+ * @example
862
+ * // Clip a circle to a rectangular region
863
+ * const defs = { 'c': { type: 'circle', cx: 50, cy: 50, r: 30 } };
864
+ * const useData = { href: 'c', x: 0, y: 0, style: { fill: 'blue' } };
865
+ * const resolved = resolveUse(useData, defs);
866
+ * const clipRect = [
867
+ * { x: 40, y: 40 },
868
+ * { x: 80, y: 40 },
869
+ * { x: 80, y: 80 },
870
+ * { x: 40, y: 80 }
871
+ * ];
872
+ * const clipped = clipResolvedUse(resolved, clipRect, 20);
873
+ * // Returns polygons representing the quarter-circle intersection
874
+ * // [{ polygon: [...], style: { fill: 'blue', ... } }]
875
+ *
876
+ * @example
877
+ * // Complex clip that may split shapes
878
+ * const clipPath = [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 50, y: 100 }]; // Triangle
879
+ * const clipped = clipResolvedUse(resolved, clipPath);
880
+ * // May produce multiple disjoint polygons if use element spans outside triangle
881
+ */
882
+ export function clipResolvedUse(resolved, clipPolygon, samples = 20) {
883
+ const polygons = flattenResolvedUse(resolved, samples);
884
+ const result = [];
885
+
886
+ for (const { polygon, style } of polygons) {
887
+ const clipped = PolygonClip.polygonIntersection(polygon, clipPolygon);
888
+
889
+ for (const clippedPoly of clipped) {
890
+ if (clippedPoly.length >= 3) {
891
+ result.push({
892
+ polygon: clippedPoly,
893
+ style
894
+ });
895
+ }
896
+ }
897
+ }
898
+
899
+ return result;
900
+ }
901
+
902
+ /**
903
+ * Convert a resolved <use> element to SVG path data string.
904
+ *
905
+ * Flattens the use element hierarchy to polygons, then generates
906
+ * combined SVG path data using M (moveto), L (lineto), and Z (closepath)
907
+ * commands. All curves are approximated as polylines.
908
+ *
909
+ * The resulting path data can be used to:
910
+ * - Create a <path> element representing the expanded use
911
+ * - Export to other formats
912
+ * - Perform path-based operations
913
+ *
914
+ * Multiple shapes produce a compound path with multiple M commands.
915
+ * Coordinates are formatted to 6 decimal places for precision.
916
+ *
917
+ * @param {Object} resolved - Resolved use data from resolveUse()
918
+ * Contains the element tree with composed transforms
919
+ * @param {number} [samples=20] - Number of samples for curve approximation
920
+ * Higher values produce smoother paths but longer data strings
921
+ * @returns {string} SVG path data string with M, L, Z commands
922
+ * Multiple polygons are concatenated with spaces.
923
+ * Returns empty string if no valid polygons.
924
+ *
925
+ * @example
926
+ * // Convert circle use to path data
927
+ * const defs = { 'c': { type: 'circle', cx: 10, cy: 10, r: 5 } };
928
+ * const useData = { href: 'c', x: 100, y: 50 };
929
+ * const resolved = resolveUse(useData, defs);
930
+ * const pathData = resolvedUseToPathData(resolved, 8);
931
+ * // "M 115.000000 50.000000 L 114.619... L 113.535... ... Z"
932
+ * // 8-point approximation of circle, moved to (110, 60)
933
+ *
934
+ * @example
935
+ * // Multiple shapes produce compound path
936
+ * const defs = {
937
+ * 'icon': {
938
+ * type: 'symbol',
939
+ * children: [
940
+ * { type: 'rect', x: 0, y: 0, width: 10, height: 10 },
941
+ * { type: 'circle', cx: 15, cy: 5, r: 3 }
942
+ * ]
943
+ * }
944
+ * };
945
+ * const useData = { href: 'icon', x: 0, y: 0 };
946
+ * const resolved = resolveUse(useData, defs);
947
+ * const pathData = resolvedUseToPathData(resolved);
948
+ * // "M 0.000000 0.000000 L 10.000000 0.000000 ... Z M 18.000000 5.000000 L ... Z"
949
+ * // Two closed subpaths (rectangle + circle)
950
+ */
951
+ export function resolvedUseToPathData(resolved, samples = 20) {
952
+ const polygons = flattenResolvedUse(resolved, samples);
953
+ const paths = [];
954
+
955
+ for (const { polygon } of polygons) {
956
+ if (polygon.length >= 3) {
957
+ let d = '';
958
+ for (let i = 0; i < polygon.length; i++) {
959
+ const p = polygon[i];
960
+ const x = Number(p.x).toFixed(6);
961
+ const y = Number(p.y).toFixed(6);
962
+ d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
963
+ }
964
+ d += ' Z';
965
+ paths.push(d);
966
+ }
967
+ }
968
+
969
+ return paths.join(' ');
970
+ }
971
+
972
+ /**
973
+ * Build a definitions map from an SVG document for use/symbol resolution.
974
+ *
975
+ * Scans the entire SVG document for elements with id attributes and parses
976
+ * them into a lookup table. This map is used by resolveUse() to find
977
+ * referenced elements.
978
+ *
979
+ * Elements can be in <defs>, <symbol>, or anywhere in the document.
980
+ * The SVG spec allows <use> to reference any element with an id, not just
981
+ * those in <defs>.
982
+ *
983
+ * Special handling for <symbol> elements: they are parsed with viewBox
984
+ * and preserveAspectRatio support via parseSymbolElement(). All other
985
+ * elements use parseChildElement().
986
+ *
987
+ * @param {Element} svgRoot - SVG root element or any container element
988
+ * Typically the <svg> element, but can be any parent to search within
989
+ * @returns {Object} Map object where:
990
+ * - Keys are element id strings
991
+ * - Values are parsed element data objects with:
992
+ * - type {string} - Element tag name
993
+ * - Geometry properties specific to element type
994
+ * - children {Array} - For symbols and groups
995
+ * - viewBoxParsed {Object} - For symbols with viewBox
996
+ * - All other properties from parseSymbolElement() or parseChildElement()
997
+ *
998
+ * @example
999
+ * // Build defs map from SVG document
1000
+ * const svg = document.querySelector('svg');
1001
+ * // <svg>
1002
+ * // <defs>
1003
+ * // <symbol id="icon" viewBox="0 0 24 24">
1004
+ * // <circle cx="12" cy="12" r="10"/>
1005
+ * // </symbol>
1006
+ * // <circle id="dot" cx="0" cy="0" r="5"/>
1007
+ * // </defs>
1008
+ * // <rect id="bg" x="0" y="0" width="100" height="100"/>
1009
+ * // </svg>
1010
+ * const defs = buildDefsMap(svg);
1011
+ * // {
1012
+ * // 'icon': { type: 'symbol', viewBoxParsed: {...}, children: [...], ... },
1013
+ * // 'dot': { type: 'circle', cx: 0, cy: 0, r: 5, ... },
1014
+ * // 'bg': { type: 'rect', x: 0, y: 0, width: 100, height: 100, ... }
1015
+ * // }
1016
+ *
1017
+ * @example
1018
+ * // Use the defs map for resolution
1019
+ * const defs = buildDefsMap(svg);
1020
+ * const useData = parseUseElement(useElement);
1021
+ * const resolved = resolveUse(useData, defs);
1022
+ */
1023
+ export function buildDefsMap(svgRoot) {
1024
+ const defs = {};
1025
+
1026
+ // Find all elements with id
1027
+ const elementsWithId = svgRoot.querySelectorAll('[id]');
1028
+
1029
+ for (const element of elementsWithId) {
1030
+ const id = element.getAttribute('id');
1031
+ const tagName = element.tagName.toLowerCase();
1032
+
1033
+ if (tagName === 'symbol') {
1034
+ defs[id] = parseSymbolElement(element);
1035
+ defs[id].type = 'symbol';
1036
+ } else {
1037
+ defs[id] = parseChildElement(element);
1038
+ }
1039
+ }
1040
+
1041
+ return defs;
1042
+ }
1043
+
1044
+ /**
1045
+ * Resolve all <use> elements in an SVG document.
1046
+ *
1047
+ * This is a convenience function that:
1048
+ * 1. Builds the definitions map from the document
1049
+ * 2. Finds all <use> elements
1050
+ * 3. Resolves each one individually
1051
+ * 4. Returns an array of results with original DOM element, parsed data, and resolution
1052
+ *
1053
+ * Useful for batch processing an entire SVG document, such as:
1054
+ * - Expanding all uses for rendering
1055
+ * - Converting uses to inline elements
1056
+ * - Analyzing use element usage
1057
+ * - Generating expanded SVG output
1058
+ *
1059
+ * Failed resolutions (target not found, max depth exceeded) are filtered out.
1060
+ *
1061
+ * @param {Element} svgRoot - SVG root element to search within
1062
+ * Typically the <svg> element. All <use> elements within will be resolved.
1063
+ * @param {Object} [options={}] - Resolution options passed to resolveUse()
1064
+ * @param {number} [options.maxDepth=10] - Maximum nesting depth for recursive use resolution
1065
+ * @returns {Array<Object>} Array of successfully resolved use elements, each with:
1066
+ * - original {Element} - The original <use> DOM element
1067
+ * - useData {Object} - Parsed use element data from parseUseElement()
1068
+ * - resolved {Object} - Resolved structure from resolveUse() with:
1069
+ * - element {Object} - Referenced target element
1070
+ * - transform {Matrix} - Composed transform matrix
1071
+ * - children {Array} - Resolved children
1072
+ * - inheritedStyle {Object} - Inherited style attributes
1073
+ *
1074
+ * @example
1075
+ * // Resolve all uses in an SVG document
1076
+ * const svg = document.querySelector('svg');
1077
+ * // <svg>
1078
+ * // <defs>
1079
+ * // <circle id="dot" r="5"/>
1080
+ * // </defs>
1081
+ * // <use href="#dot" x="10" y="20" fill="red"/>
1082
+ * // <use href="#dot" x="30" y="40" fill="blue"/>
1083
+ * // </svg>
1084
+ * const allResolved = resolveAllUses(svg);
1085
+ * // [
1086
+ * // {
1087
+ * // original: <use> element,
1088
+ * // useData: { href: 'dot', x: 10, y: 20, style: { fill: 'red' }, ... },
1089
+ * // resolved: { element: {...}, transform: Matrix(...), ... }
1090
+ * // },
1091
+ * // {
1092
+ * // original: <use> element,
1093
+ * // useData: { href: 'dot', x: 30, y: 40, style: { fill: 'blue' }, ... },
1094
+ * // resolved: { element: {...}, transform: Matrix(...), ... }
1095
+ * // }
1096
+ * // ]
1097
+ *
1098
+ * @example
1099
+ * // Convert all uses to inline paths
1100
+ * const resolved = resolveAllUses(svg);
1101
+ * for (const { original, resolved: result } of resolved) {
1102
+ * const pathData = resolvedUseToPathData(result);
1103
+ * const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1104
+ * path.setAttribute('d', pathData);
1105
+ * original.parentNode.replaceChild(path, original);
1106
+ * }
1107
+ */
1108
+ export function resolveAllUses(svgRoot, options = {}) {
1109
+ const defs = buildDefsMap(svgRoot);
1110
+ const useElements = svgRoot.querySelectorAll('use');
1111
+ const resolved = [];
1112
+
1113
+ for (const useEl of useElements) {
1114
+ const useData = parseUseElement(useEl);
1115
+ const result = resolveUse(useData, defs, options);
1116
+ if (result) {
1117
+ resolved.push({
1118
+ original: useEl,
1119
+ useData,
1120
+ resolved: result
1121
+ });
1122
+ }
1123
+ }
1124
+
1125
+ return resolved;
1126
+ }