@emasoft/svg-matrix 1.0.3 → 1.0.5

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 CHANGED
@@ -7,7 +7,9 @@ Arbitrary-precision matrix, vector and affine transformation library for JavaScr
7
7
  - **Decimal-backed** Matrix and Vector classes for high-precision geometry
8
8
  - **2D transforms** (3x3 homogeneous matrices): translation, rotation, scale, skew, reflection
9
9
  - **3D transforms** (4x4 homogeneous matrices): translation, rotation (X/Y/Z axis), scale, reflection
10
+ - **SVG transform flattening**: parse transform attributes, build CTMs, flatten nested hierarchies
10
11
  - **Linear algebra**: LU/QR decomposition, determinant, inverse, solve, matrix exponential
12
+ - **10^77 times better precision** than JavaScript floats for round-trip transforms
11
13
  - Works in **Node.js** and **browsers** (via CDN)
12
14
 
13
15
  ## Installation
@@ -19,7 +21,7 @@ npm install @emasoft/svg-matrix
19
21
  ```
20
22
 
21
23
  ```js
22
- import { Decimal, Matrix, Vector, Transforms2D, Transforms3D } from '@emasoft/svg-matrix';
24
+ import { Decimal, Matrix, Vector, Transforms2D, Transforms3D, SVGFlatten } from '@emasoft/svg-matrix';
23
25
  ```
24
26
 
25
27
  ### CDN (Browser)
@@ -28,7 +30,7 @@ Using esm.sh (recommended - auto-resolves dependencies):
28
30
 
29
31
  ```html
30
32
  <script type="module">
31
- import { Decimal, Matrix, Vector, Transforms2D, Transforms3D } from 'https://esm.sh/@emasoft/svg-matrix';
33
+ import { Decimal, Matrix, Vector, Transforms2D, Transforms3D, SVGFlatten } from 'https://esm.sh/@emasoft/svg-matrix';
32
34
 
33
35
  Decimal.set({ precision: 80 });
34
36
 
@@ -272,6 +274,258 @@ const [x, y, z] = Transforms3D.applyTransform(M, 1, 0, 0);
272
274
  | `reflectOrigin()` | Reflect through origin |
273
275
  | `applyTransform(M, x, y, z)` | Apply matrix to point |
274
276
 
277
+ ### SVGFlatten
278
+
279
+ | Function | Description |
280
+ |----------|-------------|
281
+ | **viewBox & Viewport** | |
282
+ | `parseViewBox(str)` | Parse viewBox attribute "minX minY width height" |
283
+ | `parsePreserveAspectRatio(str)` | Parse preserveAspectRatio (align, meet/slice) |
284
+ | `computeViewBoxTransform(vb, vpW, vpH, par)` | Compute viewBox to viewport matrix |
285
+ | `SVGViewport` class | Represents viewport with viewBox + preserveAspectRatio |
286
+ | `buildFullCTM(hierarchy)` | Build CTM from SVG/group/element hierarchy |
287
+ | **Units & Percentages** | |
288
+ | `resolveLength(value, ref, dpi?)` | Resolve px, %, em, pt, in, cm, mm, pc units |
289
+ | `resolvePercentages(x, y, vpW, vpH)` | Resolve x/y percentages to viewport |
290
+ | `normalizedDiagonal(w, h)` | Compute sqrt(w^2+h^2)/sqrt(2) for percentages |
291
+ | **Object Bounding Box** | |
292
+ | `objectBoundingBoxTransform(x, y, w, h)` | Transform for objectBoundingBox units |
293
+ | **Transform Parsing** | |
294
+ | `parseTransformFunction(func, args)` | Parse a single SVG transform function |
295
+ | `parseTransformAttribute(str)` | Parse a full SVG transform attribute string |
296
+ | `buildCTM(transformStack)` | Build CTM from array of transform strings |
297
+ | `applyToPoint(ctm, x, y)` | Apply CTM to a 2D point |
298
+ | `toSVGMatrix(ctm, precision?)` | Convert CTM back to SVG matrix() notation |
299
+ | `isIdentity(m, tolerance?)` | Check if matrix is effectively identity |
300
+ | `transformPathData(pathD, ctm)` | Transform path data coordinates |
301
+ | `PRECISION_INFO` | Object with precision comparison data |
302
+
303
+ ## SVG Transform Flattening
304
+
305
+ The `SVGFlatten` module provides tools for parsing SVG transform attributes, building CTMs (Current Transform Matrices), and flattening nested transforms with arbitrary precision.
306
+
307
+ ### Why Use SVGFlatten?
308
+
309
+ SVG elements can have deeply nested transforms through parent groups. When coordinates are transformed from local space to viewport and back using JavaScript's native 64-bit floats, precision is lost:
310
+
311
+ ```
312
+ Original coordinates: (10, 10)
313
+ After round-trip (float): (9.9857, 9.9857) // Error: 0.0143
314
+ ```
315
+
316
+ With `@emasoft/svg-matrix` using 80-digit Decimal precision:
317
+
318
+ ```
319
+ Original coordinates: (10, 10)
320
+ After round-trip: (10.00000000000000000000000000000000000000,
321
+ 9.999999999999999999999999999999999999999999999999999999999999999999999999999999998)
322
+ Round-trip error: X=0, Y=2e-79
323
+ ```
324
+
325
+ **Improvement: 10^77 times better precision than JavaScript floats.**
326
+
327
+ ### Parsing SVG Transforms
328
+
329
+ ```js
330
+ import { SVGFlatten } from '@emasoft/svg-matrix';
331
+
332
+ // Parse individual transforms
333
+ const m1 = SVGFlatten.parseTransformAttribute('translate(10, 20)');
334
+ const m2 = SVGFlatten.parseTransformAttribute('rotate(45)');
335
+ const m3 = SVGFlatten.parseTransformAttribute('scale(2, 0.5)');
336
+ const m4 = SVGFlatten.parseTransformAttribute('skewX(15)');
337
+ const m5 = SVGFlatten.parseTransformAttribute('matrix(0.866, 0.5, -0.5, 0.866, 0, 0)');
338
+
339
+ // Parse chained transforms
340
+ const combined = SVGFlatten.parseTransformAttribute('translate(50,50) rotate(45) scale(2)');
341
+ ```
342
+
343
+ ### Building CTM from Nested Elements
344
+
345
+ ```js
346
+ import { Decimal, SVGFlatten } from '@emasoft/svg-matrix';
347
+
348
+ Decimal.set({ precision: 80 });
349
+
350
+ // Simulate a 6-level SVG hierarchy:
351
+ // <svg viewBox="..."> <!-- viewBox scaling -->
352
+ // <g transform="translate(-13.6,-10.2)"> <!-- g1 -->
353
+ // <g transform="translate(-1144.8,517.6)"> <!-- g2 -->
354
+ // <g transform="rotate(15)"> <!-- g3 -->
355
+ // <g transform="scale(1.2, 0.8)"> <!-- g4 -->
356
+ // <path transform="matrix(...)"/> <!-- element -->
357
+
358
+ const transformStack = [
359
+ 'scale(1.5)', // viewBox scaling
360
+ 'translate(-13.613145,-10.209854)', // g1
361
+ 'translate(-1144.8563,517.64642)', // g2
362
+ 'rotate(15)', // g3
363
+ 'scale(1.2, 0.8)', // g4
364
+ 'matrix(0.71577068,0,0,1.3970955,0,0)' // element
365
+ ];
366
+
367
+ // Build combined CTM
368
+ const ctm = SVGFlatten.buildCTM(transformStack);
369
+
370
+ // Transform a point from local to viewport coordinates
371
+ const local = { x: new Decimal('10'), y: new Decimal('10') };
372
+ const viewport = SVGFlatten.applyToPoint(ctm, local.x, local.y);
373
+
374
+ // Transform back to local coordinates
375
+ const inverseCTM = ctm.inverse();
376
+ const recovered = SVGFlatten.applyToPoint(inverseCTM, viewport.x, viewport.y);
377
+
378
+ // Verify precision
379
+ const errorX = recovered.x.minus(local.x).abs();
380
+ const errorY = recovered.y.minus(local.y).abs();
381
+ console.log('Round-trip error X:', errorX.toString()); // 0
382
+ console.log('Round-trip error Y:', errorY.toString()); // ~2e-79
383
+ ```
384
+
385
+ ### Transforming Path Data
386
+
387
+ ```js
388
+ import { SVGFlatten } from '@emasoft/svg-matrix';
389
+
390
+ const pathD = 'M 100 100 L 200 100 L 200 200 L 100 200 Z';
391
+ const ctm = SVGFlatten.parseTransformAttribute('translate(50, 50) scale(2)');
392
+ const transformed = SVGFlatten.transformPathData(pathD, ctm);
393
+
394
+ console.log(transformed);
395
+ // M 250.000000 250.000000 L 450.000000 250.000000 L 450.000000 450.000000 L 250.000000 450.000000 Z
396
+ ```
397
+
398
+ ### Flattening All Transforms
399
+
400
+ To flatten an SVG (remove all transform attributes and apply them directly to coordinates):
401
+
402
+ ```js
403
+ import { SVGFlatten } from '@emasoft/svg-matrix';
404
+
405
+ // 1. For each element, collect transforms from root to element
406
+ // 2. Build CTM: const ctm = SVGFlatten.buildCTM(transformStack);
407
+ // 3. Transform path data: const newD = SVGFlatten.transformPathData(d, ctm);
408
+ // 4. Remove transform attribute, update path d attribute
409
+ // 5. Convert CTM back to SVG: SVGFlatten.toSVGMatrix(ctm, 6)
410
+ ```
411
+
412
+ ### viewBox and preserveAspectRatio
413
+
414
+ Per the [SVG 2 specification](https://www.w3.org/TR/SVG2/coords.html), the viewBox establishes a new coordinate system that maps to the viewport:
415
+
416
+ ```js
417
+ import { Decimal, SVGFlatten } from '@emasoft/svg-matrix';
418
+
419
+ Decimal.set({ precision: 80 });
420
+
421
+ // Parse viewBox and preserveAspectRatio
422
+ const viewBox = SVGFlatten.parseViewBox('0 0 100 100');
423
+ const par = SVGFlatten.parsePreserveAspectRatio('xMidYMid meet');
424
+
425
+ // Compute the viewBox-to-viewport transform
426
+ const vbTransform = SVGFlatten.computeViewBoxTransform(viewBox, 800, 600, par);
427
+
428
+ // Point (50, 50) in viewBox coords maps to viewport
429
+ const point = SVGFlatten.applyToPoint(vbTransform, 50, 50);
430
+ console.log('Viewport coords:', point.x.toFixed(2), point.y.toFixed(2));
431
+ ```
432
+
433
+ ### Full CTM with viewBox and Nested Elements
434
+
435
+ Use `buildFullCTM` for complete SVG hierarchies including viewBox transforms:
436
+
437
+ ```js
438
+ import { Decimal, SVGFlatten } from '@emasoft/svg-matrix';
439
+
440
+ Decimal.set({ precision: 80 });
441
+
442
+ // Real SVG structure with viewBox and nested transforms
443
+ const hierarchy = [
444
+ { type: 'svg', width: 800, height: 600, viewBox: '0 0 400 300', preserveAspectRatio: 'xMidYMid meet' },
445
+ { type: 'g', transform: 'translate(50, 50)' },
446
+ { type: 'g', transform: 'rotate(45)' },
447
+ { type: 'element', transform: 'scale(2)' }
448
+ ];
449
+
450
+ const ctm = SVGFlatten.buildFullCTM(hierarchy);
451
+ const local = { x: new Decimal('10'), y: new Decimal('10') };
452
+ const viewport = SVGFlatten.applyToPoint(ctm, local.x, local.y);
453
+
454
+ // Round-trip with perfect precision
455
+ const inverse = ctm.inverse();
456
+ const recovered = SVGFlatten.applyToPoint(inverse, viewport.x, viewport.y);
457
+ // Error: X=2e-78, Y=2e-78
458
+ ```
459
+
460
+ ### Nested SVG Viewports
461
+
462
+ Handle deeply nested `<svg>` elements, each with its own viewBox:
463
+
464
+ ```js
465
+ const nestedHierarchy = [
466
+ { type: 'svg', width: 1000, height: 800, viewBox: '0 0 500 400' },
467
+ { type: 'g', transform: 'translate(100, 100)' },
468
+ { type: 'svg', width: 200, height: 150, viewBox: '0 0 100 75' },
469
+ { type: 'element' }
470
+ ];
471
+
472
+ const ctm = SVGFlatten.buildFullCTM(nestedHierarchy);
473
+ // Point (10, 10) in innermost viewBox -> (240, 240) in outer viewport
474
+ ```
475
+
476
+ ### Unit and Percentage Resolution
477
+
478
+ Resolve SVG length values with units or percentages:
479
+
480
+ ```js
481
+ import { Decimal, SVGFlatten } from '@emasoft/svg-matrix';
482
+
483
+ const viewportWidth = new Decimal(800);
484
+
485
+ // Percentages resolve relative to reference size
486
+ SVGFlatten.resolveLength('50%', viewportWidth); // -> 400
487
+ SVGFlatten.resolveLength('25%', viewportWidth); // -> 200
488
+
489
+ // Absolute units (at 96 DPI)
490
+ SVGFlatten.resolveLength('1in', viewportWidth); // -> 96
491
+ SVGFlatten.resolveLength('2.54cm', viewportWidth);// -> 96 (1 inch)
492
+ SVGFlatten.resolveLength('72pt', viewportWidth); // -> 96 (1 inch)
493
+ SVGFlatten.resolveLength('6pc', viewportWidth); // -> 96 (1 inch)
494
+
495
+ // Font-relative units (assumes 16px base)
496
+ SVGFlatten.resolveLength('2em', viewportWidth); // -> 32
497
+
498
+ // Normalized diagonal for non-directional percentages
499
+ const diag = SVGFlatten.normalizedDiagonal(800, 600); // -> 707.1
500
+ ```
501
+
502
+ ### Object Bounding Box Transform
503
+
504
+ For gradients/patterns with `objectBoundingBox` units:
505
+
506
+ ```js
507
+ // Element bounding box: x=100, y=50, width=200, height=100
508
+ const bboxTransform = SVGFlatten.objectBoundingBoxTransform(100, 50, 200, 100);
509
+
510
+ // (0, 0) in bbox -> (100, 50) in user space
511
+ // (1, 1) in bbox -> (300, 150) in user space
512
+ // (0.5, 0.5) in bbox -> (200, 100) in user space (center)
513
+ ```
514
+
515
+ ### CDN Usage
516
+
517
+ ```html
518
+ <script type="module">
519
+ import { Decimal, SVGFlatten } from 'https://esm.sh/@emasoft/svg-matrix';
520
+
521
+ Decimal.set({ precision: 80 });
522
+
523
+ const ctm = SVGFlatten.parseTransformAttribute('rotate(45, 100, 100)');
524
+ const point = SVGFlatten.applyToPoint(ctm, 50, 50);
525
+ console.log('Transformed:', point.x.toFixed(6), point.y.toFixed(6));
526
+ </script>
527
+ ```
528
+
275
529
  ## License
276
530
 
277
531
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emasoft/svg-matrix",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Arbitrary-precision matrix, vector and affine transformation library for JavaScript using decimal.js",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -0,0 +1,63 @@
1
+ <?xml version="1.0" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20000802//EN"
3
+ "http://www.w3.org/TR/2000/CR-SVG-20000802/DTD/svg-20000802.dtd" [
4
+ <!ENTITY Smile "
5
+ <rect x='.5' y='.5' width='29' height='39' style='fill:yellow; stroke:red'/>
6
+ <g transform='rotate(90)'>
7
+ <text x='10' y='10' style='font-family:Verdana;
8
+ font-weight:bold; font-size:14'>:)</text>
9
+ </g>">
10
+ <!ENTITY Viewport1 "<rect x='.5' y='.5' width='49' height='29'
11
+ style='fill:none; stroke:blue'/>">
12
+ <!ENTITY Viewport2 "<rect x='.5' y='.5' width='29' height='59'
13
+ style='fill:none; stroke:blue'/>">
14
+ ]>
15
+ <svg width="480px" height="270px" style="font-family:Verdana; font-size:8">
16
+ <desc>Example PreserveAspectRatio - demonstrate available options</desc>
17
+ <text x="10" y="30">SVG to fit</text>
18
+ <g transform="translate(20,40)">&Smile;</g>
19
+ <text x="10" y="110">Viewport 1</text>
20
+ <g transform="translate(10,120)">&Viewport1;</g>
21
+ <text x="10" y="180">Viewport 2</text>
22
+ <g transform="translate(20,190)">&Viewport2;</g>
23
+ <text x="100" y="30">--------------- meet ---------------</text>
24
+ <g transform="translate(100,60)"><text y="-10">xMin*</text>&Viewport1;
25
+ <svg preserveAspectRatio="xMinYMin meet" viewBox="0 0 30 40"
26
+ width="50" height="30">&Smile;</svg></g>
27
+ <g transform="translate(170,60)"><text y="-10">xMid*</text>&Viewport1;
28
+ <svg preserveAspectRatio="xMidYMid meet" viewBox="0 0 30 40"
29
+ width="50" height="30">&Smile;</svg></g>
30
+ <g transform="translate(240,60)"><text y="-10">xMax*</text>&Viewport1;
31
+ <svg preserveAspectRatio="xMaxYMax meet" viewBox="0 0 30 40"
32
+ width="50" height="30">&Smile;</svg></g>
33
+ <text x="330" y="30">---------- meet ----------</text>
34
+ <g transform="translate(330,60)"><text y="-10">*YMin</text>&Viewport2;
35
+ <svg preserveAspectRatio="xMinYMin meet" viewBox="0 0 30 40"
36
+ width="30" height="60">&Smile;</svg></g>
37
+ <g transform="translate(380,60)"><text y="-10">*YMid</text>&Viewport2;
38
+ <svg preserveAspectRatio="xMidYMid meet" viewBox="0 0 30 40"
39
+ width="30" height="60">&Smile;</svg></g>
40
+ <g transform="translate(430,60)"><text y="-10">*YMax</text>&Viewport2;
41
+ <svg preserveAspectRatio="xMaxYMax meet" viewBox="0 0 30 40"
42
+ width="30" height="60">&Smile;</svg></g>
43
+ <text x="100" y="160">---------- slice ----------</text>
44
+ <g transform="translate(100,190)"><text y="-10">xMin*</text>&Viewport2;
45
+ <svg preserveAspectRatio="xMinYMin slice" viewBox="0 0 30 40"
46
+ width="30" height="60">&Smile;</svg></g>
47
+ <g transform="translate(150,190)"><text y="-10">xMid*</text>&Viewport2;
48
+ <svg preserveAspectRatio="xMidYMid slice" viewBox="0 0 30 40"
49
+ width="30" height="60">&Smile;</svg></g>
50
+ <g transform="translate(200,190)"><text y="-10">xMax*</text>&Viewport2;
51
+ <svg preserveAspectRatio="xMaxYMax slice" viewBox="0 0 30 40"
52
+ width="30" height="60">&Smile;</svg></g>
53
+ <text x="270" y="160">--------------- slice ---------------</text>
54
+ <g transform="translate(270,190)"><text y="-10">*YMin</text>&Viewport1;
55
+ <svg preserveAspectRatio="xMinYMin slice" viewBox="0 0 30 40"
56
+ width="50" height="30">&Smile;</svg></g>
57
+ <g transform="translate(340,190)"><text y="-10">*YMid</text>&Viewport1;
58
+ <svg preserveAspectRatio="xMidYMid slice" viewBox="0 0 30 40"
59
+ width="50" height="30">&Smile;</svg></g>
60
+ <g transform="translate(410,190)"><text y="-10">*YMax</text>&Viewport1;
61
+ <svg preserveAspectRatio="xMaxYMax slice" viewBox="0 0 30 40"
62
+ width="50" height="30">&Smile;</svg></g>
63
+ </svg>
@@ -0,0 +1,39 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg id="pure_smill_no_javascript" version="1.1" x="0px" y="0px" width="1037.2269" height="1486.5736" viewBox="0 0 1037.227 2892.792" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
3
+ <defs id="defs82" />
4
+ <defs id="defs83" />
5
+ <g id="g37" transform="translate(-13.613145,-10.209854)">
6
+ <rect style="fill:#ffffff;stroke-width:16.1377" id="rect46" width="2018.3876" height="2892.792" x="-476.96716" y="10.209854" />
7
+ <g id="g1" transform="translate(-1144.8563,517.64642)">
8
+ <path id="rect1851" style="fill:none;stroke:#fe0000;stroke-width:14.5557;stroke-dasharray:87.3339, 14.5557;stroke-dashoffset:0;stroke-opacity:1" d="m 1318.7684,284.08405 c 307.4737,-77.43295 477.4693,-79.6744 779.5799,0 132.9776,50.26731 130.7718,132.64482 0,168.79753 -297.0165,89.1289 -485.8097,78.65553 -779.5799,0 -143.454,-38.49011 -151.4589,-124.05689 0,-168.79753 z" />
9
+ </g>
10
+ <g id="g6" transform="translate(13.613181,10.209854)">
11
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:94.4263px;font-family:Futura;-inkscape-font-specification:'Futura, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#808000;stroke-width:16.0447" x="535.36902" y="880.21216" id="text2"><tspan id="tspan2" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:94.4263px;font-family:Futura;-inkscape-font-specification:'Futura, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#808000;stroke-width:16.0447" x="535.36902" y="880.21216">100% PURE SMIL</tspan></text>
12
+ </g>
13
+ <g id="g7" transform="translate(13.613181,10.209854)">
14
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:51.2678px;font-family:Futura;-inkscape-font-specification:'Futura, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#808000;stroke-width:8.7113" x="550.46289" y="939.87573" id="text3"><tspan id="tspan3" style="font-style:italic;font-variant:normal;font-weight:500;font-stretch:normal;font-size:51.2678px;font-family:Futura;-inkscape-font-specification:'Futura, Medium Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#808000;stroke-width:8.7113" x="550.46289" y="939.87573">No javascript or css needed!</tspan></text>
15
+ </g>
16
+ <text xml:space="preserve" style="font-style:italic;font-weight:600;font-size:85.6631px;font-family:'EB Garamond', serif;-inkscape-font-specification:'EB Garamond, serif, Semi-Bold Italic';writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="388.92661" y="1477.7195" id="text37" text-anchor="middle"><tspan id="tspan1" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:85.6631px;font-family:Futura;-inkscape-font-specification:'Futura, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="388.92661" y="1477.7195">100% PURE SMIL</tspan></text>
17
+ <text xml:space="preserve" style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'.New York';-inkscape-font-specification:'.New York, Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="363.31311" y="1603.9994" id="text39" text-anchor="middle"><tspan id="tspan38" style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'.New York';-inkscape-font-specification:'.New York, Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="363.31311" y="1603.9994">No javascript or css needed!</tspan></text>
18
+ <text xml:space="preserve" style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.6396" x="678.02161" y="1757.4683" id="text40" transform="scale(0.71577068,1.3970955)" text-anchor="middle"><tspan id="tspan39" style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:21.6396" x="678.02161" y="1757.4683">No javascript or css needed!</tspan></text>
19
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:Luminari;-inkscape-font-specification:'Luminari, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="404.79761" y="2284.7117" id="text41" text-anchor="middle"><tspan id="tspan40" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:Luminari;-inkscape-font-specification:'Luminari, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="404.79761" y="2284.7117">100% PURE SMIL</tspan></text>
20
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-size:85.6631px;font-family:'American Typewriter';-inkscape-font-specification:'American Typewriter, Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="341.14975" y="1755.0848" id="text42" text-anchor="middle"><tspan id="tspan41" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-size:85.6631px;font-family:'American Typewriter';-inkscape-font-specification:'American Typewriter, Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="341.14975" y="1755.0848">100% PURE SMIL</tspan></text>
21
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:85.6631px;font-family:'Comic Sans MS';-inkscape-font-specification:'Comic Sans MS, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="419.90002" y="1326.3029" id="text43" text-anchor="middle"><tspan id="tspan42" style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:85.6631px;font-family:'Comic Sans MS';-inkscape-font-specification:'Comic Sans MS, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="419.90002" y="1326.3029">100% PURE SMIL</tspan></text>
22
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:'.SF Arabic Rounded';-inkscape-font-specification:'.SF Arabic Rounded, @opsz=80,wght=510';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'opsz' 80, 'wght' 510;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;inline-size:1228.52;display:inline;fill:#000080;stroke-width:26.6984" x="1381.6643" y="1890.0889" id="text44" text-anchor="middle" transform="matrix(1.3203255,0,0,1.3203255,-1375.0262,-560.41607)"><tspan x="1381.6643" y="1890.0889" id="tspan27"><tspan style="text-align:end;text-anchor:end" id="tspan24">منتررف؛</tspan><tspan dx="212.47826" style="text-align:end;text-anchor:end" id="tspan25">٦٥</tspan><tspan dx="714.30957 -42.915234" style="text-align:end;text-anchor:end" id="tspan26">غفبممهت ورعبب</tspan><tspan y="1890.0889" id="tspan29"> </tspan></tspan><tspan x="1381.6643" y="1997.1678" id="tspan45"><tspan style="text-align:end;text-anchor:end" id="tspan30">٣٤٥٨</tspan><tspan dx="538.61511 0 0 0 0 25.01298 0 14.890675 -39.903576" style="text-align:end;text-anchor:end" id="tspan32">حخهـعنٌففڤك</tspan><tspan dx="296.26553" style="text-align:end;text-anchor:end" id="tspan33">٦</tspan><tspan dx="132.55197" style="text-align:end;text-anchor:end" id="tspan34">#</tspan><tspan dx="-47.641735" style="text-align:end;text-anchor:end" id="tspan35">٤</tspan><tspan dx="514.77338" style="text-align:end;text-anchor:end" id="tspan43">وحكهههـا؛جن</tspan></tspan></text>
23
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:67.9909px;font-family:'GohuFont 14 Nerd Font';-inkscape-font-specification:'GohuFont 14 Nerd Font, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.1905" x="604.13745" y="1147.0764" id="text45" text-anchor="middle"><tspan id="tspan44" style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:67.9909px;font-family:'GohuFont 14 Nerd Font';-inkscape-font-specification:'GohuFont 14 Nerd Font, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:21.1905" x="604.13745" y="1147.0764">100% PURE SMIL تاكهعغفبقڤذطسببى٦٥!§|</tspan></text>
24
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:69.4323px;font-family:Phosphate;-inkscape-font-specification:'Phosphate, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.6396" x="536.83356" y="1861.4491" id="text47" transform="scale(0.71577068,1.3970955)" text-anchor="middle"><tspan id="tspan47" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:69.4323px;font-family:Phosphate;-inkscape-font-specification:'Phosphate, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:21.6396" x="536.83356" y="1861.4491">No javascript or css needed!</tspan></text>
25
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:69.4323px;font-family:'ProFont IIx Nerd Font';-inkscape-font-specification:'ProFont IIx Nerd Font, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.6396" x="747.38995" y="525.20264" id="text48" transform="matrix(0.71322166,-0.06035344,0.11780243,1.3921201,0,0)" text-anchor="middle"><tspan id="tspan48" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:69.4323px;font-family:'ProFont IIx Nerd Font';-inkscape-font-specification:'ProFont IIx Nerd Font, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:21.6396" x="747.39001" y="525.20264">No javascript or css needed!</tspan></text>
26
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Bodoni Ornaments';-inkscape-font-specification:'Bodoni Ornaments, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="472.37082" y="2838.6682" id="text49" text-anchor="middle"><tspan id="tspan49" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Bodoni Ornaments';-inkscape-font-specification:'Bodoni Ornaments, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="472.37082" y="2838.6682">No javascript or css needed!</tspan></text>
27
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Academy Engraved LET';-inkscape-font-specification:'Academy Engraved LET, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="533.84888" y="530.25562" id="text50" text-anchor="middle"><tspan id="tspan50" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Academy Engraved LET';-inkscape-font-specification:'Academy Engraved LET, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="533.84888" y="530.25562">No javascript or css needed!</tspan></text>
28
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Zapfino;-inkscape-font-specification:'Zapfino, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="584.37799" y="397.6167" id="text51" text-anchor="middle"><tspan id="tspan51" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Zapfino;-inkscape-font-specification:'Zapfino, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="584.37799" y="397.6167">No javascript or css needed!</tspan></text>
29
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Webdings;-inkscape-font-specification:'Webdings, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="449.30762" y="120.5267" id="text52" text-anchor="middle"><tspan id="tspan52" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Webdings;-inkscape-font-specification:'Webdings, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="449.30762" y="120.5267">No javascript or css needed!</tspan></text>
30
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Copperplate;-inkscape-font-specification:'Copperplate, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:19.8645" x="609.64258" y="246.02931" id="text53" text-anchor="middle"><tspan id="tspan53" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:Copperplate;-inkscape-font-specification:'Copperplate, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645" x="609.64258" y="246.02931">No javascript or css needed!</tspan></text>
31
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:Luminari;-inkscape-font-specification:'Luminari, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:26.6984" x="450.42053" y="2729.5894" id="text54" text-anchor="middle"><tspan id="tspan54" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:85.6631px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:26.6984" x="450.42053" y="2729.5894">100% غعخيقفٌع٩ْخنتلو،حجةضصشيذزظطققثًار،منتزقُڤخهـرزلڤ</tspan></text>
32
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:302.923px;font-family:'.SF Arabic Rounded';-inkscape-font-specification:'.SF Arabic Rounded, @opsz=80,wght=510';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'opsz' 80, 'wght' 510;text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#808000;stroke-width:51.4717" x="-101.66634" y="477.18921" id="text4"><tspan id="tspan4" style="stroke-width:51.4717" x="-101.66634" y="477.18921">兛</tspan><tspan style="stroke-width:51.4717" x="-101.66634" y="864.82745" id="tspan5">瓩</tspan><tspan style="stroke-width:51.4717" x="-101.66634" y="1252.4657" id="tspan6">☞</tspan></text>
33
+ <path style="font-variation-settings:'opsz' 80, 'wght' 510;fill:none;stroke:#e40000;stroke-width:17.0854;stroke-dasharray:none;stroke-opacity:0.648148" d="m 1417.5151,346.27451 c -172.195,68.09973 -181.5149,120.68753 -227.2008,324.94298 -10.7474,48.04981 -1.555,122.34751 21.0816,166.07303 l 3.347,6.46516 c 40.3542,77.94932 296.6243,309.72422 275.2567,92.27938" id="path7" />
34
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Academy Engraved LET';-inkscape-font-specification:'Academy Engraved LET, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;text-anchor:middle;white-space:pre;fill:#000080;stroke-width:19.8645" id="text7" text-anchor="middle" transform="matrix(0.89589712,0,0,0.89589712,151.55716,65.666192)"><textPath xlink:href="#path7" startOffset="50%" id="textPath7"><tspan id="tspan7" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.7367px;font-family:'Academy Engraved LET';-inkscape-font-specification:'Academy Engraved LET, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000080;stroke-width:19.8645">No javascript or css needed!</tspan></textPath></text>
35
+ <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:268.424px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#6d2eb8;fill-opacity:0.574074;stroke:none;stroke-width:2.43157;stroke-dasharray:none;stroke-opacity:0.648148" x="-50.072258" y="1466.8563" id="text8" transform="scale(0.86535508,1.155595)"><tspan id="tspan8" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:268.424px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#6d2eb8;fill-opacity:0.574074;stroke:none;stroke-width:2.43157;stroke-dasharray:none" x="-50.072266" y="1466.8563">Λοπ</tspan></text>
36
+ <text xml:space="preserve" style="font-style:italic;font-variant:normal;font-weight:300;font-stretch:normal;font-size:92.6956px;font-family:Superclarendon;-inkscape-font-specification:'Superclarendon, Light Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;writing-mode:lr-tb;direction:ltr;text-anchor:end;fill:#6d2eb8;fill-opacity:0.574074;stroke:none;stroke-width:0.839701;stroke-dasharray:none;stroke-opacity:0.648148" x="-41.03904" y="1797.0054" id="text9"><tspan id="tspan9" style="font-style:italic;font-variant:normal;font-weight:300;font-stretch:normal;font-size:92.6956px;font-family:Superclarendon;-inkscape-font-specification:'Superclarendon, Light Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#6d2eb8;fill-opacity:0.574074;stroke:none;stroke-width:0.839701;stroke-dasharray:none" x="-41.039047" y="1797.0054">lkœtrëå</tspan></text>
37
+ <text xml:space="preserve" style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-decoration:underline;text-decoration-line:underline;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#000080;stroke-width:21.6396" x="1786.3558" y="997.44519" id="text22" transform="scale(0.71577068,1.3970955)" text-anchor="middle" text-decoration="underline"><tspan id="tspan23" x="1785.8699" y="997.44519" style="text-align:end;text-anchor:end"><tspan id="tspan22" style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#000080;stroke-width:21.6396" x="1785.8699" y="997.44519">No javascript</tspan></tspan><tspan x="1785.8699" y="997.44519" id="tspan28"><tspan id="tspan36" style="text-align:end;text-anchor:end"><tspan style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:69.4323px;font-family:'Hurmit Nerd Font';-inkscape-font-specification:'Hurmit Nerd Font, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#000080;stroke-width:21.6396" x="1785.8699" y="997.44519" id="tspan31"> </tspan></tspan></tspan><tspan x="1785.8699" y="1084.2356" id="tspan37">no css needed!</tspan></text>
38
+ </g>
39
+ </svg>
package/src/index.js CHANGED
@@ -27,5 +27,6 @@ import { Matrix } from './matrix.js';
27
27
  import { Vector } from './vector.js';
28
28
  import * as Transforms2D from './transforms2d.js';
29
29
  import * as Transforms3D from './transforms3d.js';
30
+ import * as SVGFlatten from './svg-flatten.js';
30
31
 
31
- export { Decimal, Matrix, Vector, Transforms2D, Transforms3D };
32
+ export { Decimal, Matrix, Vector, Transforms2D, Transforms3D, SVGFlatten };
@@ -0,0 +1,715 @@
1
+ /**
2
+ * SVG Transform Flattening Utility
3
+ *
4
+ * Parses SVG transform attributes, builds CTM (Current Transform Matrix) for each element,
5
+ * and can flatten all transforms by applying them directly to coordinates.
6
+ *
7
+ * @module svg-flatten
8
+ */
9
+
10
+ import Decimal from 'decimal.js';
11
+ import { Matrix } from './matrix.js';
12
+ import * as Transforms2D from './transforms2d.js';
13
+
14
+ // Set high precision for all calculations
15
+ Decimal.set({ precision: 80 });
16
+
17
+ // ============================================================================
18
+ // viewBox and preserveAspectRatio Parsing
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Parse an SVG viewBox attribute.
23
+ *
24
+ * @param {string} viewBoxStr - viewBox attribute value "minX minY width height"
25
+ * @returns {{minX: Decimal, minY: Decimal, width: Decimal, height: Decimal}|null}
26
+ */
27
+ export function parseViewBox(viewBoxStr) {
28
+ if (!viewBoxStr || viewBoxStr.trim() === '') {
29
+ return null;
30
+ }
31
+
32
+ const parts = viewBoxStr.trim().split(/[\s,]+/).map(s => new Decimal(s));
33
+ if (parts.length !== 4) {
34
+ console.warn(`Invalid viewBox: ${viewBoxStr}`);
35
+ return null;
36
+ }
37
+
38
+ return {
39
+ minX: parts[0],
40
+ minY: parts[1],
41
+ width: parts[2],
42
+ height: parts[3]
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Parse an SVG preserveAspectRatio attribute.
48
+ * Format: "[defer] <align> [<meetOrSlice>]"
49
+ *
50
+ * @param {string} parStr - preserveAspectRatio attribute value
51
+ * @returns {{defer: boolean, align: string, meetOrSlice: string}}
52
+ */
53
+ export function parsePreserveAspectRatio(parStr) {
54
+ const result = {
55
+ defer: false,
56
+ align: 'xMidYMid', // default
57
+ meetOrSlice: 'meet' // default
58
+ };
59
+
60
+ if (!parStr || parStr.trim() === '') {
61
+ return result;
62
+ }
63
+
64
+ const parts = parStr.trim().split(/\s+/);
65
+ let idx = 0;
66
+
67
+ // Check for 'defer' (only applies to <image>)
68
+ if (parts[idx] === 'defer') {
69
+ result.defer = true;
70
+ idx++;
71
+ }
72
+
73
+ // Alignment value
74
+ if (parts[idx]) {
75
+ result.align = parts[idx];
76
+ idx++;
77
+ }
78
+
79
+ // meetOrSlice
80
+ if (parts[idx]) {
81
+ result.meetOrSlice = parts[idx].toLowerCase();
82
+ }
83
+
84
+ return result;
85
+ }
86
+
87
+ /**
88
+ * Compute the transformation matrix from viewBox coordinates to viewport.
89
+ * Implements the SVG 2 algorithm for viewBox + preserveAspectRatio.
90
+ *
91
+ * @param {Object} viewBox - Parsed viewBox {minX, minY, width, height}
92
+ * @param {number|Decimal} viewportWidth - Viewport width in pixels
93
+ * @param {number|Decimal} viewportHeight - Viewport height in pixels
94
+ * @param {Object} par - Parsed preserveAspectRatio {align, meetOrSlice}
95
+ * @returns {Matrix} 3x3 transformation matrix
96
+ */
97
+ export function computeViewBoxTransform(viewBox, viewportWidth, viewportHeight, par = null) {
98
+ const D = x => new Decimal(x);
99
+
100
+ if (!viewBox) {
101
+ return Matrix.identity(3);
102
+ }
103
+
104
+ const vbX = viewBox.minX;
105
+ const vbY = viewBox.minY;
106
+ const vbW = viewBox.width;
107
+ const vbH = viewBox.height;
108
+ const vpW = D(viewportWidth);
109
+ const vpH = D(viewportHeight);
110
+
111
+ // Default preserveAspectRatio
112
+ if (!par) {
113
+ par = { align: 'xMidYMid', meetOrSlice: 'meet' };
114
+ }
115
+
116
+ // Handle 'none' - stretch to fill
117
+ if (par.align === 'none') {
118
+ const scaleX = vpW.div(vbW);
119
+ const scaleY = vpH.div(vbH);
120
+ // translate(-minX, -minY) then scale
121
+ const translateM = Transforms2D.translation(vbX.neg(), vbY.neg());
122
+ const scaleM = Transforms2D.scale(scaleX, scaleY);
123
+ return scaleM.mul(translateM);
124
+ }
125
+
126
+ // Compute uniform scale factor
127
+ let scaleX = vpW.div(vbW);
128
+ let scaleY = vpH.div(vbH);
129
+ let scale;
130
+
131
+ if (par.meetOrSlice === 'slice') {
132
+ // Use larger scale (content may overflow)
133
+ scale = Decimal.max(scaleX, scaleY);
134
+ } else {
135
+ // 'meet' - use smaller scale (content fits entirely)
136
+ scale = Decimal.min(scaleX, scaleY);
137
+ }
138
+
139
+ // Compute translation for alignment
140
+ const scaledW = vbW.mul(scale);
141
+ const scaledH = vbH.mul(scale);
142
+
143
+ let translateX = D(0);
144
+ let translateY = D(0);
145
+
146
+ // Parse alignment string (e.g., 'xMidYMid', 'xMinYMax')
147
+ const align = par.align;
148
+
149
+ // X alignment
150
+ if (align.includes('xMid')) {
151
+ translateX = vpW.minus(scaledW).div(2);
152
+ } else if (align.includes('xMax')) {
153
+ translateX = vpW.minus(scaledW);
154
+ }
155
+ // xMin is default (translateX = 0)
156
+
157
+ // Y alignment
158
+ if (align.includes('YMid')) {
159
+ translateY = vpH.minus(scaledH).div(2);
160
+ } else if (align.includes('YMax')) {
161
+ translateY = vpH.minus(scaledH);
162
+ }
163
+ // YMin is default (translateY = 0)
164
+
165
+ // Build the transform: translate(translateX, translateY) scale(scale) translate(-minX, -minY)
166
+ // Applied right-to-left: first translate by -minX,-minY, then scale, then translate for alignment
167
+ const translateMinM = Transforms2D.translation(vbX.neg(), vbY.neg());
168
+ const scaleM = Transforms2D.scale(scale, scale);
169
+ const translateAlignM = Transforms2D.translation(translateX, translateY);
170
+
171
+ return translateAlignM.mul(scaleM).mul(translateMinM);
172
+ }
173
+
174
+ /**
175
+ * Represents an SVG viewport with its coordinate system parameters.
176
+ * Used for building the full CTM through nested viewports.
177
+ */
178
+ export class SVGViewport {
179
+ /**
180
+ * @param {number|Decimal} width - Viewport width
181
+ * @param {number|Decimal} height - Viewport height
182
+ * @param {string|null} viewBox - viewBox attribute value
183
+ * @param {string|null} preserveAspectRatio - preserveAspectRatio attribute value
184
+ * @param {string|null} transform - transform attribute value
185
+ */
186
+ constructor(width, height, viewBox = null, preserveAspectRatio = null, transform = null) {
187
+ this.width = new Decimal(width);
188
+ this.height = new Decimal(height);
189
+ this.viewBox = viewBox ? parseViewBox(viewBox) : null;
190
+ this.preserveAspectRatio = parsePreserveAspectRatio(preserveAspectRatio);
191
+ this.transform = transform;
192
+ }
193
+
194
+ /**
195
+ * Compute the transformation matrix for this viewport.
196
+ * @returns {Matrix} 3x3 transformation matrix
197
+ */
198
+ getTransformMatrix() {
199
+ let result = Matrix.identity(3);
200
+
201
+ // Apply viewBox transform first (if present)
202
+ if (this.viewBox) {
203
+ const vbTransform = computeViewBoxTransform(
204
+ this.viewBox,
205
+ this.width,
206
+ this.height,
207
+ this.preserveAspectRatio
208
+ );
209
+ result = result.mul(vbTransform);
210
+ }
211
+
212
+ // Then apply the transform attribute (if present)
213
+ if (this.transform) {
214
+ const transformMatrix = parseTransformAttribute(this.transform);
215
+ result = result.mul(transformMatrix);
216
+ }
217
+
218
+ return result;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Build the complete CTM including viewports, viewBox transforms, and element transforms.
224
+ *
225
+ * @param {Array} hierarchy - Array of objects describing the hierarchy from root to element.
226
+ * Each object can be:
227
+ * - {type: 'svg', width, height, viewBox?, preserveAspectRatio?, transform?}
228
+ * - {type: 'g', transform?}
229
+ * - {type: 'element', transform?}
230
+ * Or simply a transform string for backwards compatibility
231
+ * @returns {Matrix} Combined CTM as 3x3 matrix
232
+ */
233
+ export function buildFullCTM(hierarchy) {
234
+ let ctm = Matrix.identity(3);
235
+
236
+ for (const item of hierarchy) {
237
+ if (typeof item === 'string') {
238
+ // Backwards compatibility: treat string as transform attribute
239
+ if (item) {
240
+ const matrix = parseTransformAttribute(item);
241
+ ctm = ctm.mul(matrix);
242
+ }
243
+ } else if (item.type === 'svg') {
244
+ // SVG viewport with potential viewBox
245
+ const viewport = new SVGViewport(
246
+ item.width,
247
+ item.height,
248
+ item.viewBox || null,
249
+ item.preserveAspectRatio || null,
250
+ item.transform || null
251
+ );
252
+ ctm = ctm.mul(viewport.getTransformMatrix());
253
+ } else if (item.type === 'g' || item.type === 'element') {
254
+ // Group or element with optional transform
255
+ if (item.transform) {
256
+ const matrix = parseTransformAttribute(item.transform);
257
+ ctm = ctm.mul(matrix);
258
+ }
259
+ }
260
+ }
261
+
262
+ return ctm;
263
+ }
264
+
265
+ // ============================================================================
266
+ // Unit and Percentage Resolution
267
+ // ============================================================================
268
+
269
+ /**
270
+ * Resolve a length value that may include units or percentages.
271
+ *
272
+ * @param {string|number} value - Length value (e.g., "50%", "10px", "5em", 100)
273
+ * @param {Decimal} referenceSize - Reference size for percentage resolution
274
+ * @param {number} [dpi=96] - DPI for absolute unit conversion
275
+ * @returns {Decimal} Resolved length in user units (px)
276
+ */
277
+ export function resolveLength(value, referenceSize, dpi = 96) {
278
+ const D = x => new Decimal(x);
279
+
280
+ if (typeof value === 'number') {
281
+ return D(value);
282
+ }
283
+
284
+ const str = String(value).trim();
285
+
286
+ // Percentage
287
+ if (str.endsWith('%')) {
288
+ const pct = D(str.slice(0, -1));
289
+ return pct.div(100).mul(referenceSize);
290
+ }
291
+
292
+ // Extract numeric value and unit
293
+ const match = str.match(/^([+-]?[\d.]+(?:e[+-]?\d+)?)(.*)?$/i);
294
+ if (!match) {
295
+ return D(0);
296
+ }
297
+
298
+ const num = D(match[1]);
299
+ const unit = (match[2] || '').toLowerCase().trim();
300
+
301
+ // Convert to user units (px)
302
+ switch (unit) {
303
+ case '':
304
+ case 'px':
305
+ return num;
306
+ case 'em':
307
+ return num.mul(16); // Assume 16px font-size
308
+ case 'rem':
309
+ return num.mul(16);
310
+ case 'pt':
311
+ return num.mul(dpi).div(72);
312
+ case 'pc':
313
+ return num.mul(dpi).div(6);
314
+ case 'in':
315
+ return num.mul(dpi);
316
+ case 'cm':
317
+ return num.mul(dpi).div(2.54);
318
+ case 'mm':
319
+ return num.mul(dpi).div(25.4);
320
+ default:
321
+ return num; // Unknown unit, treat as px
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Resolve percentage values for x/width (relative to viewport width)
327
+ * and y/height (relative to viewport height).
328
+ *
329
+ * @param {string|number} xOrWidth - X coordinate or width value
330
+ * @param {string|number} yOrHeight - Y coordinate or height value
331
+ * @param {Decimal} viewportWidth - Viewport width for reference
332
+ * @param {Decimal} viewportHeight - Viewport height for reference
333
+ * @returns {{x: Decimal, y: Decimal}} Resolved coordinates
334
+ */
335
+ export function resolvePercentages(xOrWidth, yOrHeight, viewportWidth, viewportHeight) {
336
+ return {
337
+ x: resolveLength(xOrWidth, viewportWidth),
338
+ y: resolveLength(yOrHeight, viewportHeight)
339
+ };
340
+ }
341
+
342
+ /**
343
+ * Compute the normalized diagonal for resolving percentages that
344
+ * aren't clearly x or y oriented (per SVG spec).
345
+ * Formula: sqrt(width^2 + height^2) / sqrt(2)
346
+ *
347
+ * @param {Decimal} width - Viewport width
348
+ * @param {Decimal} height - Viewport height
349
+ * @returns {Decimal} Normalized diagonal
350
+ */
351
+ export function normalizedDiagonal(width, height) {
352
+ const w = new Decimal(width);
353
+ const h = new Decimal(height);
354
+ const sqrt2 = Decimal.sqrt(2);
355
+ return Decimal.sqrt(w.mul(w).plus(h.mul(h))).div(sqrt2);
356
+ }
357
+
358
+ // ============================================================================
359
+ // Object Bounding Box Transform
360
+ // ============================================================================
361
+
362
+ /**
363
+ * Create a transformation matrix for objectBoundingBox coordinates.
364
+ * Maps (0,0) to (bboxX, bboxY) and (1,1) to (bboxX+bboxW, bboxY+bboxH).
365
+ *
366
+ * @param {number|Decimal} bboxX - Bounding box X
367
+ * @param {number|Decimal} bboxY - Bounding box Y
368
+ * @param {number|Decimal} bboxWidth - Bounding box width
369
+ * @param {number|Decimal} bboxHeight - Bounding box height
370
+ * @returns {Matrix} 3x3 transformation matrix
371
+ */
372
+ export function objectBoundingBoxTransform(bboxX, bboxY, bboxWidth, bboxHeight) {
373
+ const D = x => new Decimal(x);
374
+ // Transform: scale(bboxWidth, bboxHeight) then translate(bboxX, bboxY)
375
+ const scaleM = Transforms2D.scale(bboxWidth, bboxHeight);
376
+ const translateM = Transforms2D.translation(bboxX, bboxY);
377
+ return translateM.mul(scaleM);
378
+ }
379
+
380
+ // ============================================================================
381
+ // Transform Parsing (existing code)
382
+ // ============================================================================
383
+
384
+ /**
385
+ * Parse a single SVG transform function and return a 3x3 matrix.
386
+ * Supports: translate, scale, rotate, skewX, skewY, matrix
387
+ *
388
+ * @param {string} func - Transform function name
389
+ * @param {number[]} args - Numeric arguments
390
+ * @returns {Matrix} 3x3 transformation matrix
391
+ */
392
+ export function parseTransformFunction(func, args) {
393
+ const D = x => new Decimal(x);
394
+
395
+ switch (func.toLowerCase()) {
396
+ case 'translate': {
397
+ const tx = args[0] || 0;
398
+ const ty = args[1] || 0;
399
+ return Transforms2D.translation(tx, ty);
400
+ }
401
+
402
+ case 'scale': {
403
+ const sx = args[0] || 1;
404
+ const sy = args[1] !== undefined ? args[1] : sx;
405
+ return Transforms2D.scale(sx, sy);
406
+ }
407
+
408
+ case 'rotate': {
409
+ // SVG rotate is in degrees, can have optional cx, cy
410
+ const angleDeg = args[0] || 0;
411
+ const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
412
+
413
+ if (args.length >= 3) {
414
+ // rotate(angle, cx, cy) - rotation around point
415
+ const cx = args[1];
416
+ const cy = args[2];
417
+ return Transforms2D.rotateAroundPoint(angleRad, cx, cy);
418
+ }
419
+ return Transforms2D.rotate(angleRad);
420
+ }
421
+
422
+ case 'skewx': {
423
+ const angleDeg = args[0] || 0;
424
+ const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
425
+ const tanVal = Decimal.tan(angleRad);
426
+ return Transforms2D.skew(tanVal, 0);
427
+ }
428
+
429
+ case 'skewy': {
430
+ const angleDeg = args[0] || 0;
431
+ const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
432
+ const tanVal = Decimal.tan(angleRad);
433
+ return Transforms2D.skew(0, tanVal);
434
+ }
435
+
436
+ case 'matrix': {
437
+ // matrix(a, b, c, d, e, f) -> | a c e |
438
+ // | b d f |
439
+ // | 0 0 1 |
440
+ const [a, b, c, d, e, f] = args.map(x => D(x || 0));
441
+ return Matrix.from([
442
+ [a, c, e],
443
+ [b, d, f],
444
+ [D(0), D(0), D(1)]
445
+ ]);
446
+ }
447
+
448
+ default:
449
+ console.warn(`Unknown transform function: ${func}`);
450
+ return Matrix.identity(3);
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Parse an SVG transform attribute string into a combined matrix.
456
+ * Handles multiple transforms: "translate(10,20) rotate(45) scale(2)"
457
+ *
458
+ * @param {string} transformStr - SVG transform attribute value
459
+ * @returns {Matrix} Combined 3x3 transformation matrix
460
+ */
461
+ export function parseTransformAttribute(transformStr) {
462
+ if (!transformStr || transformStr.trim() === '') {
463
+ return Matrix.identity(3);
464
+ }
465
+
466
+ // Regex to match transform functions: name(args)
467
+ const transformRegex = /(\w+)\s*\(([^)]*)\)/g;
468
+ let match;
469
+ let result = Matrix.identity(3);
470
+
471
+ while ((match = transformRegex.exec(transformStr)) !== null) {
472
+ const func = match[1];
473
+ const argsStr = match[2];
474
+
475
+ // Parse arguments (comma or space separated)
476
+ const args = argsStr
477
+ .split(/[\s,]+/)
478
+ .filter(s => s.length > 0)
479
+ .map(s => parseFloat(s));
480
+
481
+ const matrix = parseTransformFunction(func, args);
482
+ // Transforms are applied left-to-right in SVG, so we multiply in order
483
+ result = result.mul(matrix);
484
+ }
485
+
486
+ return result;
487
+ }
488
+
489
+ /**
490
+ * Build the CTM (Current Transform Matrix) for an element by walking up its ancestry.
491
+ *
492
+ * @param {Object[]} transformStack - Array of transform strings from root to element
493
+ * @returns {Matrix} Combined CTM as 3x3 matrix
494
+ */
495
+ export function buildCTM(transformStack) {
496
+ let ctm = Matrix.identity(3);
497
+
498
+ for (const transformStr of transformStack) {
499
+ if (transformStr) {
500
+ const matrix = parseTransformAttribute(transformStr);
501
+ ctm = ctm.mul(matrix);
502
+ }
503
+ }
504
+
505
+ return ctm;
506
+ }
507
+
508
+ /**
509
+ * Apply a CTM to a 2D point.
510
+ *
511
+ * @param {Matrix} ctm - 3x3 transformation matrix
512
+ * @param {number|string|Decimal} x - X coordinate
513
+ * @param {number|string|Decimal} y - Y coordinate
514
+ * @returns {{x: Decimal, y: Decimal}} Transformed coordinates
515
+ */
516
+ export function applyToPoint(ctm, x, y) {
517
+ const [tx, ty] = Transforms2D.applyTransform(ctm, x, y);
518
+ return { x: tx, y: ty };
519
+ }
520
+
521
+ /**
522
+ * Convert a CTM back to SVG matrix() notation.
523
+ *
524
+ * @param {Matrix} ctm - 3x3 transformation matrix
525
+ * @param {number} [precision=6] - Decimal places for output
526
+ * @returns {string} SVG matrix transform string
527
+ */
528
+ export function toSVGMatrix(ctm, precision = 6) {
529
+ const a = ctm.data[0][0].toFixed(precision);
530
+ const b = ctm.data[1][0].toFixed(precision);
531
+ const c = ctm.data[0][1].toFixed(precision);
532
+ const d = ctm.data[1][1].toFixed(precision);
533
+ const e = ctm.data[0][2].toFixed(precision);
534
+ const f = ctm.data[1][2].toFixed(precision);
535
+
536
+ return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
537
+ }
538
+
539
+ /**
540
+ * Check if a matrix is effectively the identity matrix.
541
+ *
542
+ * @param {Matrix} m - 3x3 matrix to check
543
+ * @param {string} [tolerance='1e-10'] - Tolerance for comparison
544
+ * @returns {boolean} True if matrix is identity within tolerance
545
+ */
546
+ export function isIdentity(m, tolerance = '1e-10') {
547
+ const identity = Matrix.identity(3);
548
+ return m.equals(identity, tolerance);
549
+ }
550
+
551
+ /**
552
+ * Transform path data coordinates using a CTM.
553
+ * Handles M, L, C, Q, S, T, A, Z commands (absolute only for now).
554
+ *
555
+ * @param {string} pathData - SVG path d attribute
556
+ * @param {Matrix} ctm - 3x3 transformation matrix
557
+ * @returns {string} Transformed path data
558
+ */
559
+ export function transformPathData(pathData, ctm) {
560
+ // Simple regex-based path parser for common commands
561
+ const result = [];
562
+ const commandRegex = /([MLHVCSQTAZ])([^MLHVCSQTAZ]*)/gi;
563
+ let match;
564
+
565
+ while ((match = commandRegex.exec(pathData)) !== null) {
566
+ const cmd = match[1];
567
+ const argsStr = match[2].trim();
568
+ const args = argsStr
569
+ .split(/[\s,]+/)
570
+ .filter(s => s.length > 0)
571
+ .map(s => parseFloat(s));
572
+
573
+ const cmdUpper = cmd.toUpperCase();
574
+
575
+ switch (cmdUpper) {
576
+ case 'M':
577
+ case 'L':
578
+ case 'T': {
579
+ // Pairs of coordinates
580
+ const transformed = [];
581
+ for (let i = 0; i < args.length; i += 2) {
582
+ const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
583
+ transformed.push(x.toFixed(6), y.toFixed(6));
584
+ }
585
+ result.push(cmd + ' ' + transformed.join(' '));
586
+ break;
587
+ }
588
+
589
+ case 'H': {
590
+ // Horizontal line - becomes L after transform
591
+ const { x, y } = applyToPoint(ctm, args[0], 0);
592
+ result.push('L ' + x.toFixed(6) + ' ' + y.toFixed(6));
593
+ break;
594
+ }
595
+
596
+ case 'V': {
597
+ // Vertical line - becomes L after transform
598
+ const { x, y } = applyToPoint(ctm, 0, args[0]);
599
+ result.push('L ' + x.toFixed(6) + ' ' + y.toFixed(6));
600
+ break;
601
+ }
602
+
603
+ case 'C': {
604
+ // Cubic bezier: 3 pairs of coordinates
605
+ const transformed = [];
606
+ for (let i = 0; i < args.length; i += 2) {
607
+ const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
608
+ transformed.push(x.toFixed(6), y.toFixed(6));
609
+ }
610
+ result.push('C ' + transformed.join(' '));
611
+ break;
612
+ }
613
+
614
+ case 'S': {
615
+ // Smooth cubic: 2 pairs of coordinates
616
+ const transformed = [];
617
+ for (let i = 0; i < args.length; i += 2) {
618
+ const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
619
+ transformed.push(x.toFixed(6), y.toFixed(6));
620
+ }
621
+ result.push('S ' + transformed.join(' '));
622
+ break;
623
+ }
624
+
625
+ case 'Q': {
626
+ // Quadratic bezier: 2 pairs of coordinates
627
+ const transformed = [];
628
+ for (let i = 0; i < args.length; i += 2) {
629
+ const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
630
+ transformed.push(x.toFixed(6), y.toFixed(6));
631
+ }
632
+ result.push('Q ' + transformed.join(' '));
633
+ break;
634
+ }
635
+
636
+ case 'A': {
637
+ // Arc: rx ry x-axis-rotation large-arc-flag sweep-flag x y
638
+ // Transform end point, scale radii (approximate for non-uniform scale)
639
+ const transformed = [];
640
+ for (let i = 0; i < args.length; i += 7) {
641
+ const rx = args[i];
642
+ const ry = args[i + 1];
643
+ const rotation = args[i + 2];
644
+ const largeArc = args[i + 3];
645
+ const sweep = args[i + 4];
646
+ const x = args[i + 5];
647
+ const y = args[i + 6];
648
+
649
+ const { x: tx, y: ty } = applyToPoint(ctm, x, y);
650
+
651
+ // Scale radii approximately (doesn't handle rotation correctly for skew)
652
+ const scaleX = ctm.data[0][0].abs().plus(ctm.data[0][1].abs()).div(2);
653
+ const scaleY = ctm.data[1][0].abs().plus(ctm.data[1][1].abs()).div(2);
654
+
655
+ transformed.push(
656
+ (rx * scaleX.toNumber()).toFixed(6),
657
+ (ry * scaleY.toNumber()).toFixed(6),
658
+ rotation,
659
+ largeArc,
660
+ sweep,
661
+ tx.toFixed(6),
662
+ ty.toFixed(6)
663
+ );
664
+ }
665
+ result.push('A ' + transformed.join(' '));
666
+ break;
667
+ }
668
+
669
+ case 'Z': {
670
+ result.push('Z');
671
+ break;
672
+ }
673
+
674
+ default:
675
+ // Keep unknown commands as-is
676
+ result.push(cmd + ' ' + argsStr);
677
+ }
678
+ }
679
+
680
+ return result.join(' ');
681
+ }
682
+
683
+ /**
684
+ * Information about precision comparison between float and Decimal.
685
+ */
686
+ export const PRECISION_INFO = {
687
+ floatError: 0.0143, // Typical error: 10 -> 9.9857
688
+ decimalPrecision: 80,
689
+ typicalRoundTripError: '2e-79',
690
+ improvementFactor: '1.43e+77'
691
+ };
692
+
693
+ export default {
694
+ // viewBox and preserveAspectRatio
695
+ parseViewBox,
696
+ parsePreserveAspectRatio,
697
+ computeViewBoxTransform,
698
+ SVGViewport,
699
+ buildFullCTM,
700
+ // Unit resolution
701
+ resolveLength,
702
+ resolvePercentages,
703
+ normalizedDiagonal,
704
+ // Object bounding box
705
+ objectBoundingBoxTransform,
706
+ // Transform parsing
707
+ parseTransformFunction,
708
+ parseTransformAttribute,
709
+ buildCTM,
710
+ applyToPoint,
711
+ toSVGMatrix,
712
+ isIdentity,
713
+ transformPathData,
714
+ PRECISION_INFO
715
+ };