@emasoft/svg-matrix 1.0.12 → 1.0.14

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
@@ -111,6 +111,11 @@ const DEFAULT_CONFIG = {
111
111
  resolveMarkers: true, // Instantiate markers as path geometry
112
112
  resolvePatterns: true, // Expand pattern fills to tiled geometry
113
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)
114
119
  };
115
120
 
116
121
  /** @type {CLIConfig} */
@@ -538,6 +543,22 @@ ${colors.bright}FLATTEN OPTIONS:${colors.reset}
538
543
  --no-patterns Skip pattern expansion
539
544
  --no-gradients Skip gradient transform baking
540
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
+
541
562
  ${colors.bright}EXAMPLES:${colors.reset}
542
563
  svg-matrix flatten input.svg -o output.svg
543
564
  svg-matrix flatten ./svgs/ -o ./output/ --transform-only
@@ -626,6 +647,9 @@ function processFlatten(inputPath, outputPath) {
626
647
  const pipelineOptions = {
627
648
  precision: config.precision,
628
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)
629
653
  resolveUse: config.resolveUse,
630
654
  resolveMarkers: config.resolveMarkers,
631
655
  resolvePatterns: config.resolvePatterns,
@@ -634,6 +658,7 @@ function processFlatten(inputPath, outputPath) {
634
658
  flattenTransforms: true, // Always flatten transforms
635
659
  bakeGradients: config.bakeGradients,
636
660
  removeUnusedDefs: true,
661
+ // NOTE: Verification is ALWAYS enabled - precision is non-negotiable
637
662
  };
638
663
 
639
664
  // Run the full flatten pipeline
@@ -655,6 +680,49 @@ function processFlatten(inputPath, outputPath) {
655
680
  logInfo('No transform dependencies found');
656
681
  }
657
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
+ }
711
+
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
+
658
726
  // Report any errors
659
727
  if (stats.errors.length > 0) {
660
728
  for (const err of stats.errors.slice(0, 5)) {
@@ -944,6 +1012,35 @@ function parseArgs(args) {
944
1012
  case '--no-markers': cfg.resolveMarkers = false; break;
945
1013
  case '--no-patterns': cfg.resolvePatterns = false; break;
946
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
947
1044
  default:
948
1045
  if (arg.startsWith('-')) { logError(`Unknown option: ${arg}`); process.exit(CONSTANTS.EXIT_ERROR); }
949
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.12",
3
+ "version": "1.0.14",
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",
@@ -1,15 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * @fileoverview Post-install welcome message for @emasoft/svg-matrix
4
- * Displays a compact summary of available features after npm install.
5
- *
6
- * Design principles:
7
- * - Fail silently: npm install must never fail because of postinstall.
8
- * Any error is caught and swallowed with exit(0).
9
- * - Be respectful of terminal real estate: Keep the message compact.
10
- * Users want to continue working, not read documentation.
11
- * - Cross-platform: Handle Windows legacy terminals that don't support ANSI.
12
- * - Standards-compliant: Respect NO_COLOR environment variable.
4
+ * Displays a summary of CLI commands and API functions after npm install.
13
5
  *
14
6
  * @module scripts/postinstall
15
7
  * @license MIT
@@ -19,234 +11,124 @@ import { readFileSync } from 'fs';
19
11
  import { fileURLToPath } from 'url';
20
12
  import { dirname, join } from 'path';
21
13
 
22
- // ============================================================================
23
- // VERSION DETECTION
24
- // ============================================================================
25
- /**
26
- * Read version from package.json dynamically.
27
- *
28
- * Why dynamic reading instead of hardcoding:
29
- * - Avoids version mismatch bugs where postinstall shows wrong version.
30
- * - Single source of truth in package.json.
31
- *
32
- * Why wrapped in try-catch:
33
- * - postinstall scripts must NEVER fail - this would break npm install.
34
- * - Return 'unknown' gracefully if file read fails for any reason.
35
- *
36
- * @returns {string} Version string or 'unknown' on error
37
- */
38
14
  function getVersion() {
39
15
  try {
40
- // Why: ESM doesn't have __dirname, must derive from import.meta.url
41
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
42
- // Why: Go up one level because this script is in scripts/ subfolder
43
17
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
44
18
  return pkg.version || 'unknown';
45
19
  } catch {
46
- // Why: Return safe default - never throw from postinstall
47
20
  return 'unknown';
48
21
  }
49
22
  }
50
23
 
51
- // ============================================================================
52
- // COLOR SUPPORT DETECTION
53
- // ============================================================================
54
- /**
55
- * Check if colors should be disabled.
56
- *
57
- * Why this function:
58
- * - Not all terminals support ANSI escape codes.
59
- * - Showing raw escape codes is worse than no colors.
60
- * - NO_COLOR is an emerging standard: https://no-color.org
61
- *
62
- * @returns {boolean} True if colors should be disabled
63
- */
64
24
  function shouldDisableColors() {
65
- // Why: NO_COLOR standard - presence (any value) means disable colors
66
- // Check for presence, not truthiness, per spec
67
- if (process.env.NO_COLOR !== undefined) {
68
- return true;
69
- }
70
-
71
- // Why: Windows cmd.exe (not PowerShell/Terminal) doesn't support ANSI by default
72
- // Check for known ANSI-capable Windows terminals
25
+ if (process.env.NO_COLOR !== undefined) return true;
73
26
  if (process.platform === 'win32') {
74
- const supportsAnsi =
75
- process.env.WT_SESSION || // Windows Terminal sets this
76
- process.env.ConEmuANSI === 'ON' || // ConEmu explicitly signals support
77
- process.env.TERM_PROGRAM || // VS Code, Hyper, etc. set this
78
- process.env.ANSICON; // ANSICON utility for legacy cmd
79
- // Why: Disable colors unless we detect ANSI support
80
- return !supportsAnsi;
27
+ return !(process.env.WT_SESSION || process.env.ConEmuANSI === 'ON' ||
28
+ process.env.TERM_PROGRAM || process.env.ANSICON);
81
29
  }
82
-
83
- // Why: Unix terminals generally support ANSI, so enable by default
84
30
  return false;
85
31
  }
86
32
 
87
- /**
88
- * ANSI color codes for terminal output.
89
- *
90
- * Why function instead of constant:
91
- * - Need to check color support at runtime, not module load time.
92
- * - Environment variables may change between import and execution.
93
- *
94
- * @param {boolean} disabled - Whether colors are disabled
95
- * @returns {Object} Color code object
96
- */
97
33
  function getColors(disabled) {
98
- // Why: Return empty strings instead of undefined to avoid string concat issues
99
34
  if (disabled) {
100
- return {
101
- reset: '', bright: '', dim: '',
102
- cyan: '', green: '', yellow: '',
103
- magenta: '', blue: '', white: '',
104
- };
35
+ return { reset: '', bright: '', dim: '', cyan: '', green: '',
36
+ yellow: '', magenta: '', blue: '', white: '' };
105
37
  }
106
- // Why: Use standard ANSI SGR (Select Graphic Rendition) codes
107
- // Format: ESC[<code>m where ESC is \x1b
108
38
  return {
109
- reset: '\x1b[0m', // Reset all attributes
110
- bright: '\x1b[1m', // Bold/bright
111
- dim: '\x1b[2m', // Dim/faint
112
- cyan: '\x1b[36m', // Cyan foreground
113
- green: '\x1b[32m', // Green foreground
114
- yellow: '\x1b[33m', // Yellow foreground
115
- magenta: '\x1b[35m', // Magenta foreground
116
- blue: '\x1b[34m', // Blue foreground
117
- white: '\x1b[37m', // White foreground
39
+ reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m',
40
+ cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m',
41
+ magenta: '\x1b[35m', blue: '\x1b[34m', white: '\x1b[37m',
118
42
  };
119
43
  }
120
44
 
121
- // ============================================================================
122
- // UNICODE SUPPORT DETECTION
123
- // ============================================================================
124
- /**
125
- * Check if terminal likely supports Unicode box drawing characters.
126
- *
127
- * Why this check:
128
- * - Some older terminals or SSH sessions may not render Unicode correctly.
129
- * - Windows cmd.exe before Windows 10 has limited Unicode support.
130
- * - Better to show ASCII fallback than broken box characters.
131
- *
132
- * @returns {boolean} True if Unicode is likely supported
133
- */
134
45
  function supportsUnicode() {
135
- // Why: Check common environment indicators for Unicode support
136
-
137
- // Windows legacy cmd.exe typically has codepage issues
138
46
  if (process.platform === 'win32') {
139
- // Modern Windows Terminal supports Unicode
140
- if (process.env.WT_SESSION) return true;
141
- // Check for UTF-8 codepage
142
- if (process.env.CHCP === '65001') return true;
143
- // ConEmu and other modern terminals
144
- if (process.env.ConEmuANSI === 'ON') return true;
145
- if (process.env.TERM_PROGRAM) return true;
146
- // Default to ASCII on Windows for safety
147
- return false;
47
+ return !!(process.env.WT_SESSION || process.env.CHCP === '65001' ||
48
+ process.env.ConEmuANSI === 'ON' || process.env.TERM_PROGRAM);
148
49
  }
149
-
150
- // Why: Unix terminals generally support Unicode if LANG/LC_CTYPE mentions UTF-8
151
- const lang = process.env.LANG || process.env.LC_CTYPE || '';
152
- if (lang.toLowerCase().includes('utf')) return true;
153
-
154
- // Why: Modern terminal emulators typically support Unicode
155
- if (process.env.TERM_PROGRAM) return true;
156
-
157
- // Default to Unicode on Unix (most modern systems support it)
158
50
  return true;
159
51
  }
160
52
 
161
- // ============================================================================
162
- // CI DETECTION
163
- // ============================================================================
164
- /**
165
- * Check if running in CI environment.
166
- *
167
- * Why skip in CI:
168
- * - CI logs should be clean and focused on build/test output.
169
- * - Welcome messages just add noise to CI logs.
170
- * - CI systems can set CI=true to suppress this and similar messages.
171
- *
172
- * @returns {boolean} True if in CI
173
- */
174
53
  function isCI() {
175
- // Why: Check multiple CI indicators since there's no universal standard
176
- // Each CI system sets different environment variables
177
- return !!(
178
- process.env.CI || // GitHub Actions, GitLab CI, CircleCI
179
- process.env.CONTINUOUS_INTEGRATION || // Travis CI
180
- process.env.GITHUB_ACTIONS || // GitHub Actions (redundant with CI but explicit)
181
- process.env.GITLAB_CI || // GitLab CI
182
- process.env.CIRCLECI || // CircleCI
183
- process.env.TRAVIS || // Travis CI
184
- process.env.JENKINS_URL || // Jenkins
185
- process.env.BUILD_ID // Various CI systems
186
- );
54
+ return !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION ||
55
+ process.env.GITHUB_ACTIONS || process.env.GITLAB_CI ||
56
+ process.env.CIRCLECI || process.env.TRAVIS ||
57
+ process.env.JENKINS_URL || process.env.BUILD_ID);
187
58
  }
188
59
 
189
- /**
190
- * Display the welcome message.
191
- * Main entry point for the postinstall script.
192
- *
193
- * Why this design:
194
- * - Skip silently in CI: CI environments don't need welcome messages, and
195
- * they clutter build logs. Most CI systems set CI=true or similar.
196
- * - Skip in non-TTY: If stdout isn't a terminal (e.g., piped to file),
197
- * ANSI codes would be visible as garbage characters.
198
- * - Compact message: Users just installed the package and want to continue
199
- * their work. A brief message with the essentials respects their time.
200
- * Full documentation belongs in README, not terminal output.
201
- */
202
- function showWelcome() {
203
- // Why: CI builds don't need welcome messages, they just add noise to logs
204
- if (isCI()) {
205
- process.exit(0);
206
- }
207
-
208
- // Why: Non-TTY means output is being piped/redirected - ANSI codes would be garbage
209
- if (!process.stdout.isTTY) {
210
- process.exit(0);
211
- }
212
-
213
- const version = getVersion();
214
- const colorsDisabled = shouldDisableColors();
215
- const c = getColors(colorsDisabled);
216
- const unicode = supportsUnicode();
217
-
218
- // Why: Use ASCII box characters as fallback for terminals without Unicode
219
- const box = unicode ? {
220
- tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│'
221
- } : {
222
- tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|'
223
- };
60
+ // Strip ANSI escape codes for accurate length calculation
61
+ function stripAnsi(s) {
62
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
63
+ }
224
64
 
225
- // Why: Keep message compact - users want to continue their work, not read a manual.
226
- // Link to docs for those who want more. Use box drawing for visual structure.
227
- const message = `
228
- ${c.cyan}${box.tl}${box.h.repeat(45)}${box.tr}${c.reset}
229
- ${c.cyan}${box.v}${c.reset} ${c.bright}@emasoft/svg-matrix${c.reset} v${version} ${c.cyan}${box.v}${c.reset}
230
- ${c.cyan}${box.v}${c.reset} High-precision SVG matrix transforms ${c.cyan}${box.v}${c.reset}
231
- ${c.cyan}${box.bl}${box.h.repeat(45)}${box.br}${c.reset}
65
+ // Pad string to width W (accounting for ANSI codes)
66
+ function pad(s, w) {
67
+ const visible = stripAnsi(s).length;
68
+ return s + ' '.repeat(Math.max(0, w - visible));
69
+ }
232
70
 
233
- ${c.green}CLI:${c.reset} npx svg-matrix flatten input.svg -o out.svg
234
- ${c.green}API:${c.reset} import { Matrix, Transforms2D } from '@emasoft/svg-matrix'
235
- ${c.green}Docs:${c.reset} ${c.blue}https://github.com/Emasoft/SVG-MATRIX#readme${c.reset}
236
- `;
71
+ function showWelcome() {
72
+ if (isCI()) process.exit(0);
73
+ if (!process.stdout.isTTY) process.exit(0);
237
74
 
238
- console.log(message);
75
+ const version = getVersion();
76
+ const c = getColors(shouldDisableColors());
77
+ const u = supportsUnicode();
78
+
79
+ const B = u
80
+ ? { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│', dot: '•' }
81
+ : { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|', dot: '*' };
82
+
83
+ const W = 66;
84
+ const hr = B.h.repeat(W);
85
+ const R = (s) => pad(s, W);
86
+
87
+ console.log(`
88
+ ${c.cyan}${B.tl}${hr}${B.tr}${c.reset}
89
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.bright}@emasoft/svg-matrix${c.reset} v${version}`)}${c.cyan}${B.v}${c.reset}
90
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.dim}Arbitrary-precision SVG transforms with decimal.js${c.reset}`)}${c.cyan}${B.v}${c.reset}
91
+ ${c.cyan}${B.v}${hr}${B.v}${c.reset}
92
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
93
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.yellow}CLI Commands:${c.reset}`)}${c.cyan}${B.v}${c.reset}
94
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
95
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}svg-matrix flatten${c.reset} input.svg -o output.svg`)}${c.cyan}${B.v}${c.reset}
96
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.dim}Bake all transforms into path coordinates${c.reset}`)}${c.cyan}${B.v}${c.reset}
97
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
98
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}Options:${c.reset}`)}${c.cyan}${B.v}${c.reset}
99
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--precision N${c.reset} Output decimal places (default: 6)`)}${c.cyan}${B.v}${c.reset}
100
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--clip-segments N${c.reset} Polygon sampling for clips (default: 64)`)}${c.cyan}${B.v}${c.reset}
101
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--bezier-arcs N${c.reset} Bezier arcs for curves (default: 8)`)}${c.cyan}${B.v}${c.reset}
102
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--e2e-tolerance N${c.reset} Verification tolerance (default: 1e-10)`)}${c.cyan}${B.v}${c.reset}
103
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--verbose${c.reset} Show processing details`)}${c.cyan}${B.v}${c.reset}
104
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
105
+ ${c.cyan}${B.v}${hr}${B.v}${c.reset}
106
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
107
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.yellow}JavaScript API:${c.reset}`)}${c.cyan}${B.v}${c.reset}
108
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
109
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.magenta}import${c.reset} { Matrix, Vector, Transforms2D } ${c.magenta}from${c.reset} '@emasoft/svg-matrix'`)}${c.cyan}${B.v}${c.reset}
110
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
111
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot} Matrix${c.reset} Arbitrary-precision matrix operations`)}${c.cyan}${B.v}${c.reset}
112
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot} Vector${c.reset} High-precision vector math`)}${c.cyan}${B.v}${c.reset}
113
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot} Transforms2D${c.reset} rotate, scale, translate, skew, reflect`)}${c.cyan}${B.v}${c.reset}
114
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot} Transforms3D${c.reset} 3D affine transformations`)}${c.cyan}${B.v}${c.reset}
115
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
116
+ ${c.cyan}${B.v}${hr}${B.v}${c.reset}
117
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
118
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.yellow}New in v1.0.13:${c.reset}`)}${c.cyan}${B.v}${c.reset}
119
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot}${c.reset} High-precision Bezier circle/ellipse approximation`)}${c.cyan}${B.v}${c.reset}
120
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot}${c.reset} E2E verification for clip-path operations`)}${c.cyan}${B.v}${c.reset}
121
+ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot}${c.reset} Configurable: --clip-segments, --bezier-arcs, --e2e-tolerance`)}${c.cyan}${B.v}${c.reset}
122
+ ${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
123
+ ${c.cyan}${B.bl}${hr}${B.br}${c.reset}
124
+
125
+ ${c.blue}Docs:${c.reset} https://github.com/Emasoft/SVG-MATRIX#readme
126
+ ${c.blue}Help:${c.reset} svg-matrix --help
127
+ `);
239
128
  }
240
129
 
241
- // ============================================================================
242
- // MAIN ENTRY
243
- // ============================================================================
244
- // Why try-catch: postinstall scripts must NEVER fail or throw.
245
- // A failed postinstall would break `npm install` for users, which is
246
- // absolutely unacceptable. Catch everything and exit cleanly.
247
130
  try {
248
131
  showWelcome();
249
132
  } catch {
250
- // Why: Silent exit - don't break npm install for a welcome message
251
133
  process.exit(0);
252
134
  }
@@ -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),