@emasoft/svg-matrix 1.0.11 → 1.0.13

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/bin/svg-matrix.js CHANGED
@@ -20,6 +20,7 @@ import { join, dirname, basename, extname, resolve, isAbsolute } from 'path';
20
20
  // Import library modules
21
21
  import * as SVGFlatten from '../src/svg-flatten.js';
22
22
  import * as GeometryToPath from '../src/geometry-to-path.js';
23
+ import * as FlattenPipeline from '../src/flatten-pipeline.js';
23
24
  import { VERSION } from '../src/index.js';
24
25
 
25
26
  // ============================================================================
@@ -81,6 +82,13 @@ let currentOutputFile = null; // Track for cleanup on interrupt
81
82
  * @property {boolean} recursive - Process folders recursively
82
83
  * @property {boolean} overwrite - Overwrite existing files
83
84
  * @property {boolean} dryRun - Show what would be done without doing it
85
+ * @property {boolean} transformOnly - Only flatten transforms (legacy mode)
86
+ * @property {boolean} resolveClipPaths - Apply clipPath boolean operations
87
+ * @property {boolean} resolveMasks - Convert masks to clipped geometry
88
+ * @property {boolean} resolveUse - Expand use/symbol references
89
+ * @property {boolean} resolveMarkers - Expand marker references
90
+ * @property {boolean} resolvePatterns - Expand pattern fills
91
+ * @property {boolean} bakeGradients - Bake gradientTransform into coordinates
84
92
  */
85
93
 
86
94
  const DEFAULT_CONFIG = {
@@ -95,6 +103,19 @@ const DEFAULT_CONFIG = {
95
103
  recursive: false,
96
104
  overwrite: false,
97
105
  dryRun: false,
106
+ // Full flatten options - all enabled by default for TRUE flattening
107
+ transformOnly: false, // If true, skip all resolvers (legacy behavior)
108
+ resolveClipPaths: true, // Apply clipPath boolean intersection
109
+ resolveMasks: true, // Convert masks to clipped geometry
110
+ resolveUse: true, // Expand use/symbol elements inline
111
+ resolveMarkers: true, // Instantiate markers as path geometry
112
+ resolvePatterns: true, // Expand pattern fills to tiled geometry
113
+ bakeGradients: true, // Bake gradientTransform into gradient coords
114
+ // NOTE: Verification is ALWAYS enabled - precision is non-negotiable
115
+ // E2E verification precision controls
116
+ clipSegments: 64, // Polygon samples for clip operations (higher = more precise)
117
+ bezierArcs: 8, // Bezier arcs for circles/ellipses (multiple of 4; 8=π/4 optimal)
118
+ e2eTolerance: '1e-10', // E2E verification tolerance (tighter with more segments)
98
119
  };
99
120
 
100
121
  /** @type {CLIConfig} */
@@ -487,7 +508,14 @@ ${colors.bright}USAGE:${colors.reset}
487
508
  svg-matrix <command> [options] <input> [-o <output>]
488
509
 
489
510
  ${colors.bright}COMMANDS:${colors.reset}
490
- flatten Flatten SVG transforms into path data
511
+ flatten TRUE flatten: resolve ALL transform dependencies
512
+ - Bakes transform attributes into coordinates
513
+ - Applies clipPath boolean operations
514
+ - Converts masks to clipped geometry
515
+ - Expands use/symbol references inline
516
+ - Instantiates markers as path geometry
517
+ - Expands pattern fills to tiled geometry
518
+ - Bakes gradientTransform into coordinates
491
519
  convert Convert shapes (rect, circle, etc.) to paths
492
520
  normalize Convert paths to absolute cubic Bezier curves
493
521
  info Show SVG file information
@@ -506,10 +534,35 @@ ${colors.bright}OPTIONS:${colors.reset}
506
534
  --log-file <path> Write log to file
507
535
  -h, --help Show help
508
536
 
537
+ ${colors.bright}FLATTEN OPTIONS:${colors.reset}
538
+ --transform-only Only flatten transforms (skip resolvers)
539
+ --no-clip-paths Skip clipPath boolean operations
540
+ --no-masks Skip mask to clip conversion
541
+ --no-use Skip use/symbol expansion
542
+ --no-markers Skip marker instantiation
543
+ --no-patterns Skip pattern expansion
544
+ --no-gradients Skip gradient transform baking
545
+
546
+ ${colors.bright}E2E VERIFICATION OPTIONS:${colors.reset}
547
+ --clip-segments <n> Polygon samples for clipping (default: 64)
548
+ Higher = better curve approximation, tighter tolerance
549
+ Recommended: 64 (balanced), 128 (high), 256 (very high)
550
+ --bezier-arcs <n> Bezier arcs for circles/ellipses (default: 8)
551
+ Must be multiple of 4. Multiples of 8 are optimal (π/4).
552
+ 8: ~0.0004% error (π/4 optimal base)
553
+ 16: ~0.000007% error (high precision)
554
+ 32: ~0.0000004% error, 64: ~0.00000001% error
555
+ --e2e-tolerance <exp> E2E verification tolerance exponent (default: 1e-10)
556
+ Examples: 1e-8, 1e-10, 1e-12, 1e-14
557
+ Tighter tolerance requires more clip-segments
558
+
559
+ ${colors.dim}Note: Mathematical verification is ALWAYS enabled.${colors.reset}
560
+ ${colors.dim}Precision is non-negotiable in this library.${colors.reset}
561
+
509
562
  ${colors.bright}EXAMPLES:${colors.reset}
510
563
  svg-matrix flatten input.svg -o output.svg
511
- svg-matrix flatten ./svgs/ -o ./output/
512
- svg-matrix flatten --list files.txt -o ./output/
564
+ svg-matrix flatten ./svgs/ -o ./output/ --transform-only
565
+ svg-matrix flatten --list files.txt -o ./output/ --no-patterns
513
566
  svg-matrix convert input.svg -o output.svg --precision 10
514
567
  svg-matrix info input.svg
515
568
 
@@ -563,8 +616,19 @@ function replacePathD(attrs, newD) {
563
616
  }
564
617
 
565
618
  /**
566
- * Flatten transforms in SVG content by baking transforms into path data.
567
- * Handles: path elements, shape elements (converted to paths), and nested groups.
619
+ * Flatten SVG completely - no transform dependencies remain.
620
+ *
621
+ * TRUE flattening resolves ALL transform-dependent elements:
622
+ * - Bakes transform attributes into path coordinates
623
+ * - Applies clipPath boolean operations
624
+ * - Converts masks to clipped geometry
625
+ * - Expands use/symbol references inline
626
+ * - Instantiates markers as path geometry
627
+ * - Expands pattern fills to tiled geometry
628
+ * - Bakes gradientTransform into gradient coordinates
629
+ *
630
+ * Use --transform-only flag for legacy behavior (transforms only).
631
+ *
568
632
  * @param {string} inputPath - Input file path
569
633
  * @param {string} outputPath - Output file path
570
634
  * @returns {boolean} True if successful
@@ -572,152 +636,247 @@ function replacePathD(attrs, newD) {
572
636
  function processFlatten(inputPath, outputPath) {
573
637
  try {
574
638
  logDebug(`Processing: ${inputPath}`);
575
- let result = readFileSync(inputPath, 'utf8');
576
- let transformCount = 0;
577
- let pathCount = 0;
578
- let shapeCount = 0;
639
+ const svgContent = readFileSync(inputPath, 'utf8');
579
640
 
580
- // Step 1: Flatten transforms on path elements
581
- // Note: regex captures attrs without the closing /> or >
582
- result = result.replace(/<path\s+([^>]*?)\s*\/?>/gi, (match, attrs) => {
583
- const transform = extractTransform(attrs);
584
- const pathD = extractPathD(attrs);
641
+ // Use legacy transform-only mode if requested
642
+ if (config.transformOnly) {
643
+ return processFlattenLegacy(inputPath, outputPath, svgContent);
644
+ }
645
+
646
+ // Build pipeline options from config
647
+ const pipelineOptions = {
648
+ precision: config.precision,
649
+ curveSegments: 20,
650
+ clipSegments: config.clipSegments, // Higher segments for clip accuracy (default 64)
651
+ bezierArcs: config.bezierArcs, // Bezier arcs for circles/ellipses (default 16)
652
+ e2eTolerance: config.e2eTolerance, // Configurable E2E tolerance (default 1e-10)
653
+ resolveUse: config.resolveUse,
654
+ resolveMarkers: config.resolveMarkers,
655
+ resolvePatterns: config.resolvePatterns,
656
+ resolveMasks: config.resolveMasks,
657
+ resolveClipPaths: config.resolveClipPaths,
658
+ flattenTransforms: true, // Always flatten transforms
659
+ bakeGradients: config.bakeGradients,
660
+ removeUnusedDefs: true,
661
+ // NOTE: Verification is ALWAYS enabled - precision is non-negotiable
662
+ };
663
+
664
+ // Run the full flatten pipeline
665
+ const { svg: flattenedSvg, stats } = FlattenPipeline.flattenSVG(svgContent, pipelineOptions);
666
+
667
+ // Report statistics
668
+ const parts = [];
669
+ if (stats.transformsFlattened > 0) parts.push(`${stats.transformsFlattened} transforms`);
670
+ if (stats.useResolved > 0) parts.push(`${stats.useResolved} use`);
671
+ if (stats.markersResolved > 0) parts.push(`${stats.markersResolved} markers`);
672
+ if (stats.patternsResolved > 0) parts.push(`${stats.patternsResolved} patterns`);
673
+ if (stats.masksResolved > 0) parts.push(`${stats.masksResolved} masks`);
674
+ if (stats.clipPathsApplied > 0) parts.push(`${stats.clipPathsApplied} clipPaths`);
675
+ if (stats.gradientsProcessed > 0) parts.push(`${stats.gradientsProcessed} gradients`);
676
+
677
+ if (parts.length > 0) {
678
+ logInfo(`Flattened: ${parts.join(', ')}`);
679
+ } else {
680
+ logInfo('No transform dependencies found');
681
+ }
682
+
683
+ // Report verification results (ALWAYS - precision is non-negotiable)
684
+ if (stats.verifications) {
685
+ const v = stats.verifications;
686
+ const total = v.passed + v.failed;
687
+ if (total > 0) {
688
+ const verifyStatus = v.allPassed
689
+ ? `${colors.green}VERIFIED${colors.reset}`
690
+ : `${colors.red}${v.failed} FAILED${colors.reset}`;
691
+ logInfo(`Verification: ${v.passed}/${total} - ${verifyStatus}`);
692
+
693
+ // Show detailed results in verbose mode
694
+ if (config.verbose) {
695
+ if (v.matrices.length > 0) {
696
+ logDebug(` Matrix verifications: ${v.matrices.filter(m => m.valid).length}/${v.matrices.length} passed`);
697
+ }
698
+ if (v.transforms.length > 0) {
699
+ logDebug(` Transform round-trips: ${v.transforms.filter(t => t.valid).length}/${v.transforms.length} passed`);
700
+ }
701
+ if (v.polygons.length > 0) {
702
+ logDebug(` Polygon intersections: ${v.polygons.filter(p => p.valid).length}/${v.polygons.length} passed`);
703
+ }
704
+ if (v.gradients.length > 0) {
705
+ logDebug(` Gradient transforms: ${v.gradients.filter(g => g.valid).length}/${v.gradients.length} passed`);
706
+ }
707
+ if (v.e2e && v.e2e.length > 0) {
708
+ logDebug(` E2E area conservation: ${v.e2e.filter(e => e.valid).length}/${v.e2e.length} passed`);
709
+ }
710
+ }
585
711
 
586
- if (!transform || !pathD) {
587
- return match; // No transform or no path data, skip
712
+ // Always show failed verifications (not just in verbose mode)
713
+ const allVerifications = [...v.matrices, ...v.transforms, ...v.polygons, ...v.gradients, ...(v.e2e || [])];
714
+ const failed = allVerifications.filter(vr => !vr.valid);
715
+ if (failed.length > 0) {
716
+ for (const f of failed.slice(0, 3)) {
717
+ logError(`${colors.red}VERIFICATION FAILED:${colors.reset} ${f.message}`);
718
+ }
719
+ if (failed.length > 3) {
720
+ logError(`...and ${failed.length - 3} more failed verifications`);
721
+ }
722
+ }
723
+ }
724
+ }
725
+
726
+ // Report any errors
727
+ if (stats.errors.length > 0) {
728
+ for (const err of stats.errors.slice(0, 5)) {
729
+ logWarn(err);
730
+ }
731
+ if (stats.errors.length > 5) {
732
+ logWarn(`...and ${stats.errors.length - 5} more errors`);
733
+ }
734
+ }
735
+
736
+ if (!config.dryRun) {
737
+ ensureDir(dirname(outputPath));
738
+ writeFileSync(outputPath, flattenedSvg, 'utf8');
739
+ }
740
+ logSuccess(`${basename(inputPath)} -> ${basename(outputPath)}`);
741
+ return true;
742
+ } catch (error) {
743
+ logError(`Failed: ${inputPath}: ${error.message}`);
744
+ return false;
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Legacy flatten mode - only bakes transform attributes into coordinates.
750
+ * Does NOT resolve clipPaths, masks, use, markers, patterns, or gradients.
751
+ * Use this when you only need transform flattening without boolean operations.
752
+ * @private
753
+ */
754
+ function processFlattenLegacy(inputPath, outputPath, svgContent) {
755
+ let result = svgContent;
756
+ let transformCount = 0;
757
+ let pathCount = 0;
758
+ let shapeCount = 0;
759
+
760
+ // Step 1: Flatten transforms on path elements
761
+ result = result.replace(/<path\s+([^>]*?)\s*\/?>/gi, (match, attrs) => {
762
+ const transform = extractTransform(attrs);
763
+ const pathD = extractPathD(attrs);
764
+
765
+ if (!transform || !pathD) {
766
+ return match;
767
+ }
768
+
769
+ try {
770
+ const ctm = SVGFlatten.parseTransformAttribute(transform);
771
+ const transformedD = SVGFlatten.transformPathData(pathD, ctm, { precision: config.precision });
772
+ const newAttrs = removeTransform(replacePathD(attrs, transformedD));
773
+ transformCount++;
774
+ pathCount++;
775
+ logDebug(`Flattened path transform: ${transform}`);
776
+ return `<path ${newAttrs.trim()}${match.endsWith('/>') ? '/>' : '>'}`;
777
+ } catch (e) {
778
+ logWarn(`Failed to flatten path: ${e.message}`);
779
+ return match;
780
+ }
781
+ });
782
+
783
+ // Step 2: Convert shapes with transforms to flattened paths
784
+ const shapeTypes = ['rect', 'circle', 'ellipse', 'line', 'polygon', 'polyline'];
785
+
786
+ for (const shapeType of shapeTypes) {
787
+ const shapeRegex = new RegExp(`<${shapeType}([^>]*)\\/>`, 'gi');
788
+
789
+ result = result.replace(shapeRegex, (match, attrs) => {
790
+ const transform = extractTransform(attrs);
791
+ if (!transform) {
792
+ return match;
588
793
  }
589
794
 
590
795
  try {
591
- // Parse the transform and build CTM
796
+ const pathD = extractShapeAsPath(shapeType, attrs, config.precision);
797
+ if (!pathD) {
798
+ return match;
799
+ }
800
+
592
801
  const ctm = SVGFlatten.parseTransformAttribute(transform);
593
- // Transform the path data
594
802
  const transformedD = SVGFlatten.transformPathData(pathD, ctm, { precision: config.precision });
595
- // Remove transform and update path data
596
- const newAttrs = removeTransform(replacePathD(attrs, transformedD));
803
+ const attrsToRemove = getShapeSpecificAttrs(shapeType);
804
+ const styleAttrs = removeShapeAttrs(removeTransform(attrs), attrsToRemove);
597
805
  transformCount++;
598
- pathCount++;
599
- logDebug(`Flattened path transform: ${transform}`);
600
- return `<path ${newAttrs.trim()}${match.endsWith('/>') ? '/>' : '>'}`;
806
+ shapeCount++;
807
+ logDebug(`Flattened ${shapeType} transform: ${transform}`);
808
+ return `<path d="${transformedD}"${styleAttrs ? ' ' + styleAttrs : ''}/>`;
601
809
  } catch (e) {
602
- logWarn(`Failed to flatten path: ${e.message}`);
810
+ logWarn(`Failed to flatten ${shapeType}: ${e.message}`);
603
811
  return match;
604
812
  }
605
813
  });
814
+ }
606
815
 
607
- // Step 2: Convert shapes with transforms to flattened paths
608
- const shapeTypes = ['rect', 'circle', 'ellipse', 'line', 'polygon', 'polyline'];
609
-
610
- for (const shapeType of shapeTypes) {
611
- const shapeRegex = new RegExp(`<${shapeType}([^>]*)\\/>`, 'gi');
612
-
613
- result = result.replace(shapeRegex, (match, attrs) => {
614
- const transform = extractTransform(attrs);
615
- if (!transform) {
616
- return match; // No transform, skip
617
- }
816
+ // Step 3: Handle group transforms
817
+ let groupIterations = 0;
818
+ while (groupIterations < CONSTANTS.MAX_GROUP_ITERATIONS) {
819
+ const beforeResult = result;
618
820
 
821
+ result = result.replace(
822
+ /<g([^>]*transform\s*=\s*["']([^"']+)["'][^>]*)>([\s\S]*?)<\/g>/gi,
823
+ (match, gAttrs, groupTransform, content) => {
619
824
  try {
620
- // Extract shape attributes and convert to path using helper
621
- const pathD = extractShapeAsPath(shapeType, attrs, config.precision);
622
-
623
- if (!pathD) {
624
- return match; // Couldn't convert to path
625
- }
626
-
627
- // Parse the transform and build CTM
628
- const ctm = SVGFlatten.parseTransformAttribute(transform);
629
- // Transform the path data
630
- const transformedD = SVGFlatten.transformPathData(pathD, ctm, { precision: config.precision });
631
- // Build new path element, preserving style attributes
632
- const attrsToRemove = getShapeSpecificAttrs(shapeType);
633
- const styleAttrs = removeShapeAttrs(removeTransform(attrs), attrsToRemove);
634
- transformCount++;
635
- shapeCount++;
636
- logDebug(`Flattened ${shapeType} transform: ${transform}`);
637
- return `<path d="${transformedD}"${styleAttrs ? ' ' + styleAttrs : ''}/>`;
638
- } catch (e) {
639
- logWarn(`Failed to flatten ${shapeType}: ${e.message}`);
640
- return match;
641
- }
642
- });
643
- }
825
+ const groupCtm = SVGFlatten.parseTransformAttribute(groupTransform);
826
+ let modifiedContent = content;
827
+ let childrenModified = false;
644
828
 
645
- // Step 3: Handle group transforms by propagating to children
646
- // This is a simplified approach - for full support, we'd need DOM parsing
647
- // For now, we handle the case where a <g> has a transform and contains paths/shapes
648
- let groupIterations = 0;
829
+ modifiedContent = modifiedContent.replace(/<path\s+([^>]*?)\s*\/?>/gi, (pathMatch, pathAttrs) => {
830
+ const pathD = extractPathD(pathAttrs);
831
+ if (!pathD) return pathMatch;
649
832
 
650
- while (groupIterations < CONSTANTS.MAX_GROUP_ITERATIONS) {
651
- const beforeResult = result;
833
+ try {
834
+ const childTransform = extractTransform(pathAttrs);
835
+ let combinedCtm = groupCtm;
652
836
 
653
- // Find groups with transforms
654
- result = result.replace(
655
- /<g([^>]*transform\s*=\s*["']([^"']+)["'][^>]*)>([\s\S]*?)<\/g>/gi,
656
- (match, gAttrs, groupTransform, content) => {
657
- try {
658
- const groupCtm = SVGFlatten.parseTransformAttribute(groupTransform);
659
- let modifiedContent = content;
660
- let childrenModified = false;
661
-
662
- // Apply group transform to child paths
663
- modifiedContent = modifiedContent.replace(/<path\s+([^>]*?)\s*\/?>/gi, (pathMatch, pathAttrs) => {
664
- const pathD = extractPathD(pathAttrs);
665
- if (!pathD) return pathMatch;
666
-
667
- try {
668
- const childTransform = extractTransform(pathAttrs);
669
- let combinedCtm = groupCtm;
670
-
671
- // If child has its own transform, compose them
672
- if (childTransform) {
673
- const childCtm = SVGFlatten.parseTransformAttribute(childTransform);
674
- combinedCtm = groupCtm.mul(childCtm);
675
- }
676
-
677
- const transformedD = SVGFlatten.transformPathData(pathD, combinedCtm, { precision: config.precision });
678
- const newAttrs = removeTransform(replacePathD(pathAttrs, transformedD));
679
- childrenModified = true;
680
- transformCount++;
681
- return `<path ${newAttrs.trim()}${pathMatch.endsWith('/>') ? '/>' : '>'}`;
682
- } catch (e) {
683
- logWarn(`Failed to apply group transform to path: ${e.message}`);
684
- return pathMatch;
837
+ if (childTransform) {
838
+ const childCtm = SVGFlatten.parseTransformAttribute(childTransform);
839
+ combinedCtm = groupCtm.mul(childCtm);
685
840
  }
686
- });
687
841
 
688
- if (childrenModified) {
689
- // Remove transform from group
690
- const newGAttrs = removeTransform(gAttrs);
691
- logDebug(`Propagated group transform to children: ${groupTransform}`);
692
- return `<g${newGAttrs}>${modifiedContent}</g>`;
842
+ const transformedD = SVGFlatten.transformPathData(pathD, combinedCtm, { precision: config.precision });
843
+ const newAttrs = removeTransform(replacePathD(pathAttrs, transformedD));
844
+ childrenModified = true;
845
+ transformCount++;
846
+ return `<path ${newAttrs.trim()}${pathMatch.endsWith('/>') ? '/>' : '>'}`;
847
+ } catch (e) {
848
+ logWarn(`Failed to apply group transform to path: ${e.message}`);
849
+ return pathMatch;
693
850
  }
694
- return match;
695
- } catch (e) {
696
- logWarn(`Failed to process group: ${e.message}`);
697
- return match;
851
+ });
852
+
853
+ if (childrenModified) {
854
+ const newGAttrs = removeTransform(gAttrs);
855
+ logDebug(`Propagated group transform to children: ${groupTransform}`);
856
+ return `<g${newGAttrs}>${modifiedContent}</g>`;
698
857
  }
858
+ return match;
859
+ } catch (e) {
860
+ logWarn(`Failed to process group: ${e.message}`);
861
+ return match;
699
862
  }
700
- );
701
-
702
- // Check if anything changed
703
- if (result === beforeResult) {
704
- break;
705
863
  }
706
- groupIterations++;
864
+ );
865
+
866
+ if (result === beforeResult) {
867
+ break;
707
868
  }
869
+ groupIterations++;
870
+ }
708
871
 
709
- logInfo(`Flattened ${transformCount} transforms (${pathCount} paths, ${shapeCount} shapes)`);
872
+ logInfo(`Flattened ${transformCount} transforms (${pathCount} paths, ${shapeCount} shapes) [legacy mode]`);
710
873
 
711
- if (!config.dryRun) {
712
- ensureDir(dirname(outputPath));
713
- writeFileSync(outputPath, result, 'utf8');
714
- }
715
- logSuccess(`${basename(inputPath)} -> ${basename(outputPath)}`);
716
- return true;
717
- } catch (error) {
718
- logError(`Failed: ${inputPath}: ${error.message}`);
719
- return false;
874
+ if (!config.dryRun) {
875
+ ensureDir(dirname(outputPath));
876
+ writeFileSync(outputPath, result, 'utf8');
720
877
  }
878
+ logSuccess(`${basename(inputPath)} -> ${basename(outputPath)}`);
879
+ return true;
721
880
  }
722
881
 
723
882
  function processConvert(inputPath, outputPath) {
@@ -845,6 +1004,43 @@ function parseArgs(args) {
845
1004
  case '--log-file': cfg.logFile = args[++i]; break;
846
1005
  case '-h': case '--help': cfg.command = 'help'; break;
847
1006
  case '--version': cfg.command = 'version'; break;
1007
+ // Full flatten pipeline options
1008
+ case '--transform-only': cfg.transformOnly = true; break;
1009
+ case '--no-clip-paths': cfg.resolveClipPaths = false; break;
1010
+ case '--no-masks': cfg.resolveMasks = false; break;
1011
+ case '--no-use': cfg.resolveUse = false; break;
1012
+ case '--no-markers': cfg.resolveMarkers = false; break;
1013
+ case '--no-patterns': cfg.resolvePatterns = false; break;
1014
+ case '--no-gradients': cfg.bakeGradients = false; break;
1015
+ // E2E verification precision options
1016
+ case '--clip-segments': {
1017
+ const segs = parseInt(args[++i], 10);
1018
+ if (isNaN(segs) || segs < 8 || segs > 512) {
1019
+ logError('clip-segments must be between 8 and 512');
1020
+ process.exit(CONSTANTS.EXIT_ERROR);
1021
+ }
1022
+ cfg.clipSegments = segs;
1023
+ break;
1024
+ }
1025
+ case '--bezier-arcs': {
1026
+ const arcs = parseInt(args[++i], 10);
1027
+ if (isNaN(arcs) || arcs < 4 || arcs > 128) {
1028
+ logError('bezier-arcs must be between 4 and 128');
1029
+ process.exit(CONSTANTS.EXIT_ERROR);
1030
+ }
1031
+ cfg.bezierArcs = arcs;
1032
+ break;
1033
+ }
1034
+ case '--e2e-tolerance': {
1035
+ const tol = args[++i];
1036
+ if (!/^1e-\d+$/.test(tol)) {
1037
+ logError('e2e-tolerance must be in format 1e-N (e.g., 1e-10, 1e-12)');
1038
+ process.exit(CONSTANTS.EXIT_ERROR);
1039
+ }
1040
+ cfg.e2eTolerance = tol;
1041
+ break;
1042
+ }
1043
+ // NOTE: --verify removed - verification is ALWAYS enabled
848
1044
  default:
849
1045
  if (arg.startsWith('-')) { logError(`Unknown option: ${arg}`); process.exit(CONSTANTS.EXIT_ERROR); }
850
1046
  if (['flatten', 'convert', 'normalize', 'info', 'help', 'version'].includes(arg) && cfg.command === 'help') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emasoft/svg-matrix",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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",
@@ -41,6 +41,10 @@ import {
41
41
  parseTransformAttribute,
42
42
  transformPathData
43
43
  } from './svg-flatten.js';
44
+ import {
45
+ circleToPathDataHP,
46
+ ellipseToPathDataHP
47
+ } from './geometry-to-path.js';
44
48
  import { Logger } from './logger.js';
45
49
 
46
50
  // Alias for cleaner code
@@ -414,14 +418,32 @@ function removeDuplicateConsecutive(points) {
414
418
  * const ctm = Transforms2D.translation(10, 20);
415
419
  * const polygon = shapeToPolygon(rect, ctm, 30);
416
420
  */
417
- export function shapeToPolygon(element, ctm = null, samples = DEFAULT_CURVE_SAMPLES) {
421
+ /**
422
+ * Convert SVG shape to polygon.
423
+ *
424
+ * @param {Object} element - Shape element (circle, ellipse, rect, etc.)
425
+ * @param {Matrix} ctm - Current transform matrix (optional)
426
+ * @param {number} samples - Samples per curve for polygon conversion
427
+ * @param {number} bezierArcs - Number of Bezier arcs for circles/ellipses (4=standard, 16 or 64=HP)
428
+ */
429
+ export function shapeToPolygon(element, ctm = null, samples = DEFAULT_CURVE_SAMPLES, bezierArcs = 4) {
418
430
  let pathData;
419
431
  switch (element.type) {
420
432
  case 'circle':
421
- pathData = circleToPath(D(element.cx || 0), D(element.cy || 0), D(element.r || 0));
433
+ // Use high-precision Bezier arcs for better curve approximation
434
+ if (bezierArcs > 4) {
435
+ pathData = circleToPathDataHP(element.cx || 0, element.cy || 0, element.r || 0, bezierArcs, 10);
436
+ } else {
437
+ pathData = circleToPath(D(element.cx || 0), D(element.cy || 0), D(element.r || 0));
438
+ }
422
439
  break;
423
440
  case 'ellipse':
424
- pathData = ellipseToPath(D(element.cx || 0), D(element.cy || 0), D(element.rx || 0), D(element.ry || 0));
441
+ // Use high-precision Bezier arcs for better curve approximation
442
+ if (bezierArcs > 4) {
443
+ pathData = ellipseToPathDataHP(element.cx || 0, element.cy || 0, element.rx || 0, element.ry || 0, bezierArcs, 10);
444
+ } else {
445
+ pathData = ellipseToPath(D(element.cx || 0), D(element.cy || 0), D(element.rx || 0), D(element.ry || 0));
446
+ }
425
447
  break;
426
448
  case 'rect':
427
449
  pathData = rectToPath(D(element.x || 0), D(element.y || 0), D(element.width || 0), D(element.height || 0),