@emasoft/svg-matrix 1.0.3 → 1.0.4

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,142 @@ 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
+ | `parseTransformFunction(func, args)` | Parse a single SVG transform function |
282
+ | `parseTransformAttribute(str)` | Parse a full SVG transform attribute string |
283
+ | `buildCTM(transformStack)` | Build CTM from array of transform strings |
284
+ | `applyToPoint(ctm, x, y)` | Apply CTM to a 2D point |
285
+ | `toSVGMatrix(ctm, precision?)` | Convert CTM back to SVG matrix() notation |
286
+ | `isIdentity(m, tolerance?)` | Check if matrix is effectively identity |
287
+ | `transformPathData(pathD, ctm)` | Transform path data coordinates |
288
+ | `PRECISION_INFO` | Object with precision comparison data |
289
+
290
+ ## SVG Transform Flattening
291
+
292
+ The `SVGFlatten` module provides tools for parsing SVG transform attributes, building CTMs (Current Transform Matrices), and flattening nested transforms with arbitrary precision.
293
+
294
+ ### Why Use SVGFlatten?
295
+
296
+ 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:
297
+
298
+ ```
299
+ Original coordinates: (10, 10)
300
+ After round-trip (float): (9.9857, 9.9857) // Error: 0.0143
301
+ ```
302
+
303
+ With `@emasoft/svg-matrix` using 80-digit Decimal precision:
304
+
305
+ ```
306
+ Original coordinates: (10, 10)
307
+ After round-trip: (10.00000000000000000000000000000000000000,
308
+ 9.999999999999999999999999999999999999999999999999999999999999999999999999999999998)
309
+ Round-trip error: X=0, Y=2e-79
310
+ ```
311
+
312
+ **Improvement: 10^77 times better precision than JavaScript floats.**
313
+
314
+ ### Parsing SVG Transforms
315
+
316
+ ```js
317
+ import { SVGFlatten } from '@emasoft/svg-matrix';
318
+
319
+ // Parse individual transforms
320
+ const m1 = SVGFlatten.parseTransformAttribute('translate(10, 20)');
321
+ const m2 = SVGFlatten.parseTransformAttribute('rotate(45)');
322
+ const m3 = SVGFlatten.parseTransformAttribute('scale(2, 0.5)');
323
+ const m4 = SVGFlatten.parseTransformAttribute('skewX(15)');
324
+ const m5 = SVGFlatten.parseTransformAttribute('matrix(0.866, 0.5, -0.5, 0.866, 0, 0)');
325
+
326
+ // Parse chained transforms
327
+ const combined = SVGFlatten.parseTransformAttribute('translate(50,50) rotate(45) scale(2)');
328
+ ```
329
+
330
+ ### Building CTM from Nested Elements
331
+
332
+ ```js
333
+ import { Decimal, SVGFlatten } from '@emasoft/svg-matrix';
334
+
335
+ Decimal.set({ precision: 80 });
336
+
337
+ // Simulate a 6-level SVG hierarchy:
338
+ // <svg viewBox="..."> <!-- viewBox scaling -->
339
+ // <g transform="translate(-13.6,-10.2)"> <!-- g1 -->
340
+ // <g transform="translate(-1144.8,517.6)"> <!-- g2 -->
341
+ // <g transform="rotate(15)"> <!-- g3 -->
342
+ // <g transform="scale(1.2, 0.8)"> <!-- g4 -->
343
+ // <path transform="matrix(...)"/> <!-- element -->
344
+
345
+ const transformStack = [
346
+ 'scale(1.5)', // viewBox scaling
347
+ 'translate(-13.613145,-10.209854)', // g1
348
+ 'translate(-1144.8563,517.64642)', // g2
349
+ 'rotate(15)', // g3
350
+ 'scale(1.2, 0.8)', // g4
351
+ 'matrix(0.71577068,0,0,1.3970955,0,0)' // element
352
+ ];
353
+
354
+ // Build combined CTM
355
+ const ctm = SVGFlatten.buildCTM(transformStack);
356
+
357
+ // Transform a point from local to viewport coordinates
358
+ const local = { x: new Decimal('10'), y: new Decimal('10') };
359
+ const viewport = SVGFlatten.applyToPoint(ctm, local.x, local.y);
360
+
361
+ // Transform back to local coordinates
362
+ const inverseCTM = ctm.inverse();
363
+ const recovered = SVGFlatten.applyToPoint(inverseCTM, viewport.x, viewport.y);
364
+
365
+ // Verify precision
366
+ const errorX = recovered.x.minus(local.x).abs();
367
+ const errorY = recovered.y.minus(local.y).abs();
368
+ console.log('Round-trip error X:', errorX.toString()); // 0
369
+ console.log('Round-trip error Y:', errorY.toString()); // ~2e-79
370
+ ```
371
+
372
+ ### Transforming Path Data
373
+
374
+ ```js
375
+ import { SVGFlatten } from '@emasoft/svg-matrix';
376
+
377
+ const pathD = 'M 100 100 L 200 100 L 200 200 L 100 200 Z';
378
+ const ctm = SVGFlatten.parseTransformAttribute('translate(50, 50) scale(2)');
379
+ const transformed = SVGFlatten.transformPathData(pathD, ctm);
380
+
381
+ console.log(transformed);
382
+ // M 250.000000 250.000000 L 450.000000 250.000000 L 450.000000 450.000000 L 250.000000 450.000000 Z
383
+ ```
384
+
385
+ ### Flattening All Transforms
386
+
387
+ To flatten an SVG (remove all transform attributes and apply them directly to coordinates):
388
+
389
+ ```js
390
+ import { SVGFlatten } from '@emasoft/svg-matrix';
391
+
392
+ // 1. For each element, collect transforms from root to element
393
+ // 2. Build CTM: const ctm = SVGFlatten.buildCTM(transformStack);
394
+ // 3. Transform path data: const newD = SVGFlatten.transformPathData(d, ctm);
395
+ // 4. Remove transform attribute, update path d attribute
396
+ // 5. Convert CTM back to SVG: SVGFlatten.toSVGMatrix(ctm, 6)
397
+ ```
398
+
399
+ ### CDN Usage
400
+
401
+ ```html
402
+ <script type="module">
403
+ import { Decimal, SVGFlatten } from 'https://esm.sh/@emasoft/svg-matrix';
404
+
405
+ Decimal.set({ precision: 80 });
406
+
407
+ const ctm = SVGFlatten.parseTransformAttribute('rotate(45, 100, 100)');
408
+ const point = SVGFlatten.applyToPoint(ctm, 50, 50);
409
+ console.log('Transformed:', point.x.toFixed(6), point.y.toFixed(6));
410
+ </script>
411
+ ```
412
+
275
413
  ## License
276
414
 
277
415
  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.4",
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,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,335 @@
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
+ * Parse a single SVG transform function and return a 3x3 matrix.
19
+ * Supports: translate, scale, rotate, skewX, skewY, matrix
20
+ *
21
+ * @param {string} func - Transform function name
22
+ * @param {number[]} args - Numeric arguments
23
+ * @returns {Matrix} 3x3 transformation matrix
24
+ */
25
+ export function parseTransformFunction(func, args) {
26
+ const D = x => new Decimal(x);
27
+
28
+ switch (func.toLowerCase()) {
29
+ case 'translate': {
30
+ const tx = args[0] || 0;
31
+ const ty = args[1] || 0;
32
+ return Transforms2D.translation(tx, ty);
33
+ }
34
+
35
+ case 'scale': {
36
+ const sx = args[0] || 1;
37
+ const sy = args[1] !== undefined ? args[1] : sx;
38
+ return Transforms2D.scale(sx, sy);
39
+ }
40
+
41
+ case 'rotate': {
42
+ // SVG rotate is in degrees, can have optional cx, cy
43
+ const angleDeg = args[0] || 0;
44
+ const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
45
+
46
+ if (args.length >= 3) {
47
+ // rotate(angle, cx, cy) - rotation around point
48
+ const cx = args[1];
49
+ const cy = args[2];
50
+ return Transforms2D.rotateAroundPoint(angleRad, cx, cy);
51
+ }
52
+ return Transforms2D.rotate(angleRad);
53
+ }
54
+
55
+ case 'skewx': {
56
+ const angleDeg = args[0] || 0;
57
+ const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
58
+ const tanVal = Decimal.tan(angleRad);
59
+ return Transforms2D.skew(tanVal, 0);
60
+ }
61
+
62
+ case 'skewy': {
63
+ const angleDeg = args[0] || 0;
64
+ const angleRad = D(angleDeg).mul(D(Math.PI)).div(180);
65
+ const tanVal = Decimal.tan(angleRad);
66
+ return Transforms2D.skew(0, tanVal);
67
+ }
68
+
69
+ case 'matrix': {
70
+ // matrix(a, b, c, d, e, f) -> | a c e |
71
+ // | b d f |
72
+ // | 0 0 1 |
73
+ const [a, b, c, d, e, f] = args.map(x => D(x || 0));
74
+ return Matrix.from([
75
+ [a, c, e],
76
+ [b, d, f],
77
+ [D(0), D(0), D(1)]
78
+ ]);
79
+ }
80
+
81
+ default:
82
+ console.warn(`Unknown transform function: ${func}`);
83
+ return Matrix.identity(3);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Parse an SVG transform attribute string into a combined matrix.
89
+ * Handles multiple transforms: "translate(10,20) rotate(45) scale(2)"
90
+ *
91
+ * @param {string} transformStr - SVG transform attribute value
92
+ * @returns {Matrix} Combined 3x3 transformation matrix
93
+ */
94
+ export function parseTransformAttribute(transformStr) {
95
+ if (!transformStr || transformStr.trim() === '') {
96
+ return Matrix.identity(3);
97
+ }
98
+
99
+ // Regex to match transform functions: name(args)
100
+ const transformRegex = /(\w+)\s*\(([^)]*)\)/g;
101
+ let match;
102
+ let result = Matrix.identity(3);
103
+
104
+ while ((match = transformRegex.exec(transformStr)) !== null) {
105
+ const func = match[1];
106
+ const argsStr = match[2];
107
+
108
+ // Parse arguments (comma or space separated)
109
+ const args = argsStr
110
+ .split(/[\s,]+/)
111
+ .filter(s => s.length > 0)
112
+ .map(s => parseFloat(s));
113
+
114
+ const matrix = parseTransformFunction(func, args);
115
+ // Transforms are applied left-to-right in SVG, so we multiply in order
116
+ result = result.mul(matrix);
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /**
123
+ * Build the CTM (Current Transform Matrix) for an element by walking up its ancestry.
124
+ *
125
+ * @param {Object[]} transformStack - Array of transform strings from root to element
126
+ * @returns {Matrix} Combined CTM as 3x3 matrix
127
+ */
128
+ export function buildCTM(transformStack) {
129
+ let ctm = Matrix.identity(3);
130
+
131
+ for (const transformStr of transformStack) {
132
+ if (transformStr) {
133
+ const matrix = parseTransformAttribute(transformStr);
134
+ ctm = ctm.mul(matrix);
135
+ }
136
+ }
137
+
138
+ return ctm;
139
+ }
140
+
141
+ /**
142
+ * Apply a CTM to a 2D point.
143
+ *
144
+ * @param {Matrix} ctm - 3x3 transformation matrix
145
+ * @param {number|string|Decimal} x - X coordinate
146
+ * @param {number|string|Decimal} y - Y coordinate
147
+ * @returns {{x: Decimal, y: Decimal}} Transformed coordinates
148
+ */
149
+ export function applyToPoint(ctm, x, y) {
150
+ const [tx, ty] = Transforms2D.applyTransform(ctm, x, y);
151
+ return { x: tx, y: ty };
152
+ }
153
+
154
+ /**
155
+ * Convert a CTM back to SVG matrix() notation.
156
+ *
157
+ * @param {Matrix} ctm - 3x3 transformation matrix
158
+ * @param {number} [precision=6] - Decimal places for output
159
+ * @returns {string} SVG matrix transform string
160
+ */
161
+ export function toSVGMatrix(ctm, precision = 6) {
162
+ const a = ctm.data[0][0].toFixed(precision);
163
+ const b = ctm.data[1][0].toFixed(precision);
164
+ const c = ctm.data[0][1].toFixed(precision);
165
+ const d = ctm.data[1][1].toFixed(precision);
166
+ const e = ctm.data[0][2].toFixed(precision);
167
+ const f = ctm.data[1][2].toFixed(precision);
168
+
169
+ return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
170
+ }
171
+
172
+ /**
173
+ * Check if a matrix is effectively the identity matrix.
174
+ *
175
+ * @param {Matrix} m - 3x3 matrix to check
176
+ * @param {string} [tolerance='1e-10'] - Tolerance for comparison
177
+ * @returns {boolean} True if matrix is identity within tolerance
178
+ */
179
+ export function isIdentity(m, tolerance = '1e-10') {
180
+ const identity = Matrix.identity(3);
181
+ return m.equals(identity, tolerance);
182
+ }
183
+
184
+ /**
185
+ * Transform path data coordinates using a CTM.
186
+ * Handles M, L, C, Q, S, T, A, Z commands (absolute only for now).
187
+ *
188
+ * @param {string} pathData - SVG path d attribute
189
+ * @param {Matrix} ctm - 3x3 transformation matrix
190
+ * @returns {string} Transformed path data
191
+ */
192
+ export function transformPathData(pathData, ctm) {
193
+ // Simple regex-based path parser for common commands
194
+ const result = [];
195
+ const commandRegex = /([MLHVCSQTAZ])([^MLHVCSQTAZ]*)/gi;
196
+ let match;
197
+
198
+ while ((match = commandRegex.exec(pathData)) !== null) {
199
+ const cmd = match[1];
200
+ const argsStr = match[2].trim();
201
+ const args = argsStr
202
+ .split(/[\s,]+/)
203
+ .filter(s => s.length > 0)
204
+ .map(s => parseFloat(s));
205
+
206
+ const cmdUpper = cmd.toUpperCase();
207
+
208
+ switch (cmdUpper) {
209
+ case 'M':
210
+ case 'L':
211
+ case 'T': {
212
+ // Pairs of coordinates
213
+ const transformed = [];
214
+ for (let i = 0; i < args.length; i += 2) {
215
+ const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
216
+ transformed.push(x.toFixed(6), y.toFixed(6));
217
+ }
218
+ result.push(cmd + ' ' + transformed.join(' '));
219
+ break;
220
+ }
221
+
222
+ case 'H': {
223
+ // Horizontal line - becomes L after transform
224
+ const { x, y } = applyToPoint(ctm, args[0], 0);
225
+ result.push('L ' + x.toFixed(6) + ' ' + y.toFixed(6));
226
+ break;
227
+ }
228
+
229
+ case 'V': {
230
+ // Vertical line - becomes L after transform
231
+ const { x, y } = applyToPoint(ctm, 0, args[0]);
232
+ result.push('L ' + x.toFixed(6) + ' ' + y.toFixed(6));
233
+ break;
234
+ }
235
+
236
+ case 'C': {
237
+ // Cubic bezier: 3 pairs of coordinates
238
+ const transformed = [];
239
+ for (let i = 0; i < args.length; i += 2) {
240
+ const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
241
+ transformed.push(x.toFixed(6), y.toFixed(6));
242
+ }
243
+ result.push('C ' + transformed.join(' '));
244
+ break;
245
+ }
246
+
247
+ case 'S': {
248
+ // Smooth cubic: 2 pairs of coordinates
249
+ const transformed = [];
250
+ for (let i = 0; i < args.length; i += 2) {
251
+ const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
252
+ transformed.push(x.toFixed(6), y.toFixed(6));
253
+ }
254
+ result.push('S ' + transformed.join(' '));
255
+ break;
256
+ }
257
+
258
+ case 'Q': {
259
+ // Quadratic bezier: 2 pairs of coordinates
260
+ const transformed = [];
261
+ for (let i = 0; i < args.length; i += 2) {
262
+ const { x, y } = applyToPoint(ctm, args[i], args[i + 1]);
263
+ transformed.push(x.toFixed(6), y.toFixed(6));
264
+ }
265
+ result.push('Q ' + transformed.join(' '));
266
+ break;
267
+ }
268
+
269
+ case 'A': {
270
+ // Arc: rx ry x-axis-rotation large-arc-flag sweep-flag x y
271
+ // Transform end point, scale radii (approximate for non-uniform scale)
272
+ const transformed = [];
273
+ for (let i = 0; i < args.length; i += 7) {
274
+ const rx = args[i];
275
+ const ry = args[i + 1];
276
+ const rotation = args[i + 2];
277
+ const largeArc = args[i + 3];
278
+ const sweep = args[i + 4];
279
+ const x = args[i + 5];
280
+ const y = args[i + 6];
281
+
282
+ const { x: tx, y: ty } = applyToPoint(ctm, x, y);
283
+
284
+ // Scale radii approximately (doesn't handle rotation correctly for skew)
285
+ const scaleX = ctm.data[0][0].abs().plus(ctm.data[0][1].abs()).div(2);
286
+ const scaleY = ctm.data[1][0].abs().plus(ctm.data[1][1].abs()).div(2);
287
+
288
+ transformed.push(
289
+ (rx * scaleX.toNumber()).toFixed(6),
290
+ (ry * scaleY.toNumber()).toFixed(6),
291
+ rotation,
292
+ largeArc,
293
+ sweep,
294
+ tx.toFixed(6),
295
+ ty.toFixed(6)
296
+ );
297
+ }
298
+ result.push('A ' + transformed.join(' '));
299
+ break;
300
+ }
301
+
302
+ case 'Z': {
303
+ result.push('Z');
304
+ break;
305
+ }
306
+
307
+ default:
308
+ // Keep unknown commands as-is
309
+ result.push(cmd + ' ' + argsStr);
310
+ }
311
+ }
312
+
313
+ return result.join(' ');
314
+ }
315
+
316
+ /**
317
+ * Information about precision comparison between float and Decimal.
318
+ */
319
+ export const PRECISION_INFO = {
320
+ floatError: 0.0143, // Typical error: 10 -> 9.9857
321
+ decimalPrecision: 80,
322
+ typicalRoundTripError: '2e-79',
323
+ improvementFactor: '1.43e+77'
324
+ };
325
+
326
+ export default {
327
+ parseTransformFunction,
328
+ parseTransformAttribute,
329
+ buildCTM,
330
+ applyToPoint,
331
+ toSVGMatrix,
332
+ isIdentity,
333
+ transformPathData,
334
+ PRECISION_INFO
335
+ };