@cadit-app/image-extrude 0.4.4 → 0.5.0

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
@@ -10,10 +10,7 @@ Extrude 3D shapes from SVG or bitmap images. Supports both tracing (for raster i
10
10
  - **Sample mode**: High-fidelity conversion of SVG files to 3D
11
11
  - **Configurable size**: Set maximum width to control output dimensions
12
12
  - **Despeckle**: Remove small artifacts during tracing
13
- - **CLI support**: Generate GLB and 3MF files from command line (SVG sample mode)
14
-
15
- > **Note:** Trace mode for bitmap images currently only works in the browser (CADit).
16
- > The CLI only supports SVG files with sample mode.
13
+ - **CLI support**: Generate GLB and 3MF files from command line
17
14
 
18
15
  ## Installation
19
16
 
@@ -25,12 +22,12 @@ npm install @cadit-app/image-extrude
25
22
 
26
23
  ### As a CADit Script
27
24
 
28
- Import this script in [CADit](https://cadit.app) by adding the GitHub repository URL.
25
+ Open this script in [CADit](https://cadit.app/design/ohQ58mpwMpdX5qC) and edit parameters inside the browser.
29
26
 
30
27
  ### CLI Usage
31
28
 
32
29
  ```bash
33
- # Generate GLB from default star shape
30
+ # Generate GLB from default Cookiecad logo
34
31
  npx tsx cli.ts output.glb
35
32
 
36
33
  # Generate from a specific image
@@ -44,7 +41,7 @@ npx tsx cli.ts output.3mf --image=photo.png --mode=trace
44
41
 
45
42
  | Option | Description | Default |
46
43
  |--------|-------------|---------|
47
- | `--image=<path>` | Path to image file (SVG, PNG, JPG) | Built-in star |
44
+ | `--image=<path>` | Path to image file (SVG, PNG, JPG) | Built-in Cookiecad logo |
48
45
  | `--height=<mm>` | Extrusion height in mm | 1 |
49
46
  | `--maxWidth=<mm>` | Maximum width in mm | 50 |
50
47
  | `--mode=<trace\|sample>` | Processing mode | trace |
@@ -55,7 +52,7 @@ npx tsx cli.ts output.3mf --image=photo.png --mode=trace
55
52
  ```typescript
56
53
  import imageExtrude from '@cadit-app/image-extrude';
57
54
 
58
- // Use the defineParams API
55
+ // Use the defineParams API - returns 2D shapes (SceneOutput)
59
56
  const result = await imageExtrude.main({
60
57
  mode: 'trace',
61
58
  imageFile: {
@@ -65,8 +62,13 @@ const result = await imageExtrude.main({
65
62
  },
66
63
  height: 2,
67
64
  maxWidth: 50,
68
- despeckleSize: 2
65
+ despeckleSize: 2,
66
+ threshold: 0,
67
+ invert: false
69
68
  });
69
+
70
+ // result is a SceneOutput containing polygon shapes
71
+ console.log(result.objects); // Array of polygon shapes with holes
70
72
  ```
71
73
 
72
74
  ### Creating CrossSections for Embedding
@@ -80,7 +82,7 @@ const crossSection = await makeCrossSection({
80
82
  maxWidth: 30
81
83
  });
82
84
 
83
- // Use in your own maker
85
+ // Use in your own maker - extrude to get a Manifold
84
86
  const manifold = crossSection.extrude(5);
85
87
  ```
86
88
 
@@ -94,24 +96,6 @@ const manifold = crossSection.extrude(5);
94
96
  | `maxWidth` | `number` | Maximum width in mm |
95
97
  | `despeckleSize` | `number` | Remove spots smaller than this (trace only) |
96
98
 
97
- ## Build & Bundle
98
-
99
- This package ships as a **pre-bundled** ES module for browser use.
100
-
101
- ### Why we bundle
102
-
103
- CADit's script runtime uses esbuild to bundle external scripts at runtime, fetching dependencies from CDN (esm.sh). Some dependencies like `potrace` (which uses `jimp` for image processing) don't work correctly when fetched from CDN due to complex browser shims.
104
-
105
- By pre-bundling at publish time, we ensure:
106
- - All dependencies are resolved correctly at build time
107
- - The bundle works reliably in CADit's browser environment
108
- - No CDN resolution issues at runtime
109
-
110
- ### Exports
111
-
112
- - **Default** (`@cadit-app/image-extrude`): Pre-bundled version for CADit/browser use
113
- - **Unbundled** (`@cadit-app/image-extrude/unbundled`): TypeScript-compiled modules for Node.js/CLI use
114
-
115
99
  ## License
116
100
 
117
101
  MIT
package/dist/cli.js CHANGED
@@ -58,8 +58,8 @@ async function main() {
58
58
  // Initialize manifold-3d
59
59
  const manifold = await import('@cadit-app/manifold-3d');
60
60
  await manifold.default();
61
- // Now import the maker
62
- const { default: imageExtrudeMaker } = await import('./src/main');
61
+ // Now import the cross section maker for CLI (we need a Manifold for mesh export)
62
+ const { makeCrossSection } = await import('./src/makeCrossSection');
63
63
  const { imageExtrudeParamsSchema } = await import('./src/params');
64
64
  console.log('Generating image extrusion...');
65
65
  // Build params from defaults and CLI options
@@ -108,8 +108,15 @@ async function main() {
108
108
  despeckleSize: defaultParams.despeckleSize,
109
109
  hasImage: !!defaultParams.imageFile?.dataUrl
110
110
  });
111
- // Generate the model
112
- const result = await imageExtrudeMaker(defaultParams);
111
+ // Generate the cross section using makeCrossSection (returns CrossSection for mesh export)
112
+ const crossSection = await makeCrossSection({
113
+ imageFile: defaultParams.imageFile,
114
+ mode: defaultParams.mode,
115
+ maxWidth: defaultParams.maxWidth,
116
+ despeckleSize: defaultParams.despeckleSize,
117
+ });
118
+ // Extrude to get a Manifold for mesh export
119
+ const result = crossSection.extrude(defaultParams.height);
113
120
  if (ext === '.glb') {
114
121
  // Export as GLB
115
122
  const mesh = result.getMesh();
@@ -4,13 +4,14 @@
4
4
  * Extrude 3D shapes from SVG or bitmap images.
5
5
  * Uses the defineParams API from @cadit-app/script-params.
6
6
  */
7
- import type { Manifold } from '@cadit-app/manifold-3d/manifoldCAD';
8
- export { sampleSvg, traceImage } from './tracing';
7
+ import type { SceneOutput } from '@cadit-app/script-params';
8
+ export { sampleSvgToPolygons, traceImageToPolygons, CompoundPolygon } from './tracing';
9
9
  export { renderSvgToBitmapDataUrl } from './resvg';
10
10
  export { makeCrossSection } from './makeCrossSection';
11
11
  /**
12
12
  * Main entry point using defineParams
13
+ * Returns 2D shapes (SceneOutput) that CADit will extrude
13
14
  */
14
- declare const _default: import("@cadit-app/script-params").ScriptModule<any, Promise<Manifold>>;
15
+ declare const _default: import("@cadit-app/script-params").ScriptModule<any, Promise<SceneOutput>>;
15
16
  export default _default;
16
17
  //# sourceMappingURL=main.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAgB,MAAM,oCAAoC,CAAC;AAQjF,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD;;GAEG;;AACH,wBAuDG"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,WAAW,EAA6B,MAAM,0BAA0B,CAAC;AAOvF,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD;;;GAGG;;AACH,wBA0EG"}
package/dist/src/main.js CHANGED
@@ -4,18 +4,18 @@
4
4
  * Extrude 3D shapes from SVG or bitmap images.
5
5
  * Uses the defineParams API from @cadit-app/script-params.
6
6
  */
7
- import { defineParams } from '@cadit-app/script-params';
7
+ import { defineParams, createSceneOutput, polygon } from '@cadit-app/script-params';
8
8
  import { imageExtrudeParamsSchema } from './params';
9
- import { sampleSvg, traceImage } from './tracing';
9
+ import { sampleSvgToPolygons, traceImageToPolygons } from './tracing';
10
10
  import { renderSvgToBitmapDataUrl } from './resvg';
11
11
  import { fetchImageAsDataUrl } from './utils';
12
- import { createEmptyManifold } from './manifoldUtils';
13
12
  // Re-export for external use
14
- export { sampleSvg, traceImage } from './tracing';
13
+ export { sampleSvgToPolygons, traceImageToPolygons } from './tracing';
15
14
  export { renderSvgToBitmapDataUrl } from './resvg';
16
15
  export { makeCrossSection } from './makeCrossSection';
17
16
  /**
18
17
  * Main entry point using defineParams
18
+ * Returns 2D shapes (SceneOutput) that CADit will extrude
19
19
  */
20
20
  export default defineParams({
21
21
  params: imageExtrudeParamsSchema,
@@ -33,25 +33,25 @@ export default defineParams({
33
33
  }
34
34
  catch (err) {
35
35
  console.warn('Failed to fetch imageUrl:', err);
36
- return createEmptyManifold();
36
+ return createSceneOutput([]);
37
37
  }
38
38
  }
39
39
  if (!imageFile || !imageFile.dataUrl) {
40
40
  console.warn('No valid image file provided.');
41
- return createEmptyManifold();
41
+ return createSceneOutput([]);
42
42
  }
43
43
  // Adjust mode if sample is selected for non-SVG
44
44
  if (mode === 'sample' && !imageFile.fileType?.includes('svg')) {
45
45
  console.warn('Sample mode selected for non-SVG file. Defaulting to Trace mode.');
46
46
  mode = 'trace';
47
47
  }
48
- let crossSection;
48
+ let compoundPolygons;
49
49
  try {
50
50
  if (mode === 'trace') {
51
51
  // if svg, render svg to bitmap and then trace
52
52
  const isSvg = imageFile.fileType?.includes('svg');
53
53
  const dataUrl = isSvg ? await renderSvgToBitmapDataUrl(imageFile.dataUrl) : imageFile.dataUrl;
54
- crossSection = await traceImage(dataUrl, {
54
+ compoundPolygons = await traceImageToPolygons(dataUrl, {
55
55
  maxWidth: typedParams.maxWidth,
56
56
  despeckleSize: typedParams.despeckleSize,
57
57
  threshold: typedParams.threshold || undefined, // 0 means auto
@@ -60,13 +60,29 @@ export default defineParams({
60
60
  }
61
61
  else {
62
62
  // mode is 'sample', and fileType is guaranteed to be svg+xml
63
- crossSection = await sampleSvg(imageFile.dataUrl, typedParams.maxWidth);
63
+ compoundPolygons = await sampleSvgToPolygons(imageFile.dataUrl, typedParams.maxWidth);
64
64
  }
65
65
  }
66
66
  catch (error) {
67
67
  console.error(`Error during image processing (mode: ${mode}):`, error);
68
- return createEmptyManifold();
68
+ return createSceneOutput([]);
69
69
  }
70
- return crossSection.extrude(height);
70
+ if (!compoundPolygons || compoundPolygons.length === 0) {
71
+ console.error('No polygons generated');
72
+ return createSceneOutput([]);
73
+ }
74
+ // Convert compound polygons to CADit polygon shapes with holes
75
+ const shapes = compoundPolygons.map((compound) => {
76
+ const points = compound.outer.map(([x, y]) => ({ x, y }));
77
+ const holes = compound.holes.length > 0
78
+ ? compound.holes.map(hole => hole.map(([x, y]) => ({ x, y })))
79
+ : undefined;
80
+ return polygon(points, {
81
+ height,
82
+ fill: true,
83
+ holes,
84
+ });
85
+ });
86
+ return createSceneOutput(shapes);
71
87
  },
72
88
  });
@@ -5,9 +5,20 @@
5
5
  */
6
6
  import { CrossSection } from '@cadit-app/manifold-3d/manifoldCAD';
7
7
  /**
8
- * Converts SVG content to polygons
8
+ * A compound path with an outer boundary and optional holes.
9
+ * Used for CADit compound path shapes.
9
10
  */
10
- export declare const svgContentToPolygons: (svgContent: string, maxError: number) => Promise<[number, number][][]>;
11
+ export type CompoundPolygon = {
12
+ outer: [number, number][];
13
+ holes: [number, number][][];
14
+ };
15
+ /**
16
+ * Converts SVG content to polygons.
17
+ * @param svgContent The SVG content string
18
+ * @param maxError Maximum error for polygon sampling
19
+ * @param flipY Whether to flip the Y axis. Set to true for 3D (Y-up), false for 2D (Y-down like SVG)
20
+ */
21
+ export declare const svgContentToPolygons: (svgContent: string, maxError: number, flipY?: boolean) => Promise<[number, number][][]>;
11
22
  /**
12
23
  * Converts an SVG string to a CrossSection with optional scaling
13
24
  */
@@ -33,4 +44,28 @@ export declare const traceImage: (imageDataUrl: string, options?: {
33
44
  */
34
45
  invert?: boolean;
35
46
  }) => Promise<CrossSection>;
47
+ /**
48
+ * Converts SVG content to compound polygons with proper hole detection.
49
+ * Uses Manifold's CrossSection with even-odd fill rule, then decompose() to
50
+ * get individual shapes where each has an outer boundary and its holes.
51
+ *
52
+ * This is the correct way to handle text and complex shapes where internal
53
+ * paths should be holes (like the inside of letters A, B, D, O, etc.)
54
+ */
55
+ export declare const svgContentToCompoundPolygons: (svgContent: string, maxWidth?: number, maxError?: number) => Promise<CompoundPolygon[]>;
56
+ /**
57
+ * Samples an SVG data URL and returns compound polygons with proper hole detection.
58
+ * Uses Manifold's CrossSection.decompose() to correctly identify outer shapes and holes.
59
+ */
60
+ export declare const sampleSvgToPolygons: (svgDataUrl: string, maxWidth?: number) => Promise<CompoundPolygon[]>;
61
+ /**
62
+ * Traces a bitmap image and returns compound polygons with proper hole detection.
63
+ * Uses Manifold's CrossSection.decompose() to correctly identify outer shapes and holes.
64
+ */
65
+ export declare const traceImageToPolygons: (imageDataUrl: string, options: {
66
+ maxWidth?: number;
67
+ despeckleSize?: number;
68
+ threshold?: number;
69
+ invert?: boolean;
70
+ }) => Promise<CompoundPolygon[]>;
36
71
  //# sourceMappingURL=tracing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/tracing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAKlE;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAC/B,YAAY,MAAM,EAClB,UAAU,MAAM,kCAWjB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAClC,YAAY,MAAM,EAClB,WAAW,MAAM,EACjB,WAAU,MAAa,KACtB,OAAO,CAAC,YAAY,CAgBtB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS,GAAU,YAAY,MAAM,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,YAAY,CAO3F,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GACrB,cAAc,MAAM,EACpB,UAAS;IACP,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACb,KACL,OAAO,CAAC,YAAY,CAsBtB,CAAC"}
1
+ {"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/tracing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAKlE;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC1B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;CAC7B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC/B,YAAY,MAAM,EAClB,UAAU,MAAM,EAChB,QAAO,OAAc,KACpB,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAU9B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAClC,YAAY,MAAM,EAClB,WAAW,MAAM,EACjB,WAAU,MAAa,KACtB,OAAO,CAAC,YAAY,CAgBtB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS,GAAU,YAAY,MAAM,EAAE,WAAW,MAAM,KAAG,OAAO,CAAC,YAAY,CAO3F,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GACrB,cAAc,MAAM,EACpB,UAAS;IACP,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACb,KACL,OAAO,CAAC,YAAY,CAsBtB,CAAC;AA2CF;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,GACvC,YAAY,MAAM,EAClB,WAAW,MAAM,EACjB,WAAU,MAAa,KACtB,OAAO,CAAC,eAAe,EAAE,CAkD3B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC9B,YAAY,MAAM,EAClB,WAAW,MAAM,KAChB,OAAO,CAAC,eAAe,EAAE,CAM3B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAU,cAAc,MAAM,EAAE,SAAS;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,KAAG,OAAO,CAAC,eAAe,EAAE,CAoB5B,CAAC"}
@@ -9,16 +9,19 @@ import { traceDataUrl, getSVG, THRESHOLD_AUTO } from '@cadit-app/potrace-ts';
9
9
  import { svgDataUrlToString } from './utils';
10
10
  import { centerCrossSection } from './crossSectionUtils';
11
11
  /**
12
- * Converts SVG content to polygons
12
+ * Converts SVG content to polygons.
13
+ * @param svgContent The SVG content string
14
+ * @param maxError Maximum error for polygon sampling
15
+ * @param flipY Whether to flip the Y axis. Set to true for 3D (Y-up), false for 2D (Y-down like SVG)
13
16
  */
14
- export const svgContentToPolygons = async (svgContent, maxError) => {
17
+ export const svgContentToPolygons = async (svgContent, maxError, flipY = true) => {
15
18
  // Sample the SVG into polygons
16
19
  const polygons = await svgToPolygons(svgContent, { maxError });
17
- // Flip the Y-axis for SVG paths (SVG uses Y-down, but 3D modeling uses Y-up)
18
- const flippedPolygons = polygons.map((polygon) => {
19
- return polygon.points.map(([x, y]) => [x, -y]);
20
+ // Optionally flip Y-axis: SVG uses Y-down, 3D modeling uses Y-up, CADit 2D uses Y-down
21
+ const processedPolygons = polygons.map((polygon) => {
22
+ return polygon.points.map(([x, y]) => [x, flipY ? -y : y]);
20
23
  });
21
- return flippedPolygons;
24
+ return processedPolygons;
22
25
  };
23
26
  /**
24
27
  * Converts an SVG string to a CrossSection with optional scaling
@@ -76,3 +79,118 @@ export const traceImage = async (imageDataUrl, options = {}) => {
76
79
  const crossSection = await svgStringToCrossSection(svgContent, options.maxWidth);
77
80
  return centerCrossSection(crossSection);
78
81
  };
82
+ // =============================================================================
83
+ // Compound polygon functions (for scene output shapes with holes)
84
+ // =============================================================================
85
+ /**
86
+ * Center compound polygons around origin.
87
+ */
88
+ function centerCompoundPolygons(compounds) {
89
+ if (compounds.length === 0)
90
+ return compounds;
91
+ // Find bounding box across all polygons
92
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
93
+ for (const { outer, holes } of compounds) {
94
+ for (const [x, y] of outer) {
95
+ minX = Math.min(minX, x);
96
+ minY = Math.min(minY, y);
97
+ maxX = Math.max(maxX, x);
98
+ maxY = Math.max(maxY, y);
99
+ }
100
+ for (const hole of holes) {
101
+ for (const [x, y] of hole) {
102
+ minX = Math.min(minX, x);
103
+ minY = Math.min(minY, y);
104
+ maxX = Math.max(maxX, x);
105
+ maxY = Math.max(maxY, y);
106
+ }
107
+ }
108
+ }
109
+ const centerX = (minX + maxX) / 2;
110
+ const centerY = (minY + maxY) / 2;
111
+ // Translate all polygons
112
+ return compounds.map(({ outer, holes }) => ({
113
+ outer: outer.map(([x, y]) => [x - centerX, y - centerY]),
114
+ holes: holes.map(hole => hole.map(([x, y]) => [x - centerX, y - centerY])),
115
+ }));
116
+ }
117
+ /**
118
+ * Converts SVG content to compound polygons with proper hole detection.
119
+ * Uses Manifold's CrossSection with even-odd fill rule, then decompose() to
120
+ * get individual shapes where each has an outer boundary and its holes.
121
+ *
122
+ * This is the correct way to handle text and complex shapes where internal
123
+ * paths should be holes (like the inside of letters A, B, D, O, etc.)
124
+ */
125
+ export const svgContentToCompoundPolygons = async (svgContent, maxWidth, maxError = 0.01) => {
126
+ // Get polygons with Y flipped for Manifold (Y-up coordinate system)
127
+ let polygons = await svgContentToPolygons(svgContent, maxError, true);
128
+ if (polygons.length === 0) {
129
+ return [];
130
+ }
131
+ // Create CrossSection with even-odd fill rule - this handles overlapping paths correctly
132
+ let crossSection = new CrossSection(polygons, "EvenOdd").simplify(maxError);
133
+ // Scale if needed
134
+ if (maxWidth) {
135
+ const boundingBox = crossSection.bounds();
136
+ const width = boundingBox.max[0] - boundingBox.min[0];
137
+ if (width > 0) {
138
+ const scaleFactor = maxWidth / width;
139
+ const scaledError = maxError / scaleFactor;
140
+ // Re-sample with adjusted error for better quality at target size
141
+ polygons = await svgContentToPolygons(svgContent, scaledError, true);
142
+ crossSection = new CrossSection(polygons, "EvenOdd").simplify(scaledError);
143
+ crossSection = crossSection.scale([scaleFactor, scaleFactor]);
144
+ }
145
+ }
146
+ // Decompose into individual connected components (each outer shape with its holes)
147
+ const decomposed = crossSection.decompose();
148
+ // Convert each decomposed CrossSection to CompoundPolygon
149
+ // toPolygons() returns [outerPath, ...holes] for each decomposed section
150
+ const compounds = decomposed.map(section => {
151
+ const sectionPolygons = section.toPolygons();
152
+ if (sectionPolygons.length === 0) {
153
+ return { outer: [], holes: [] };
154
+ }
155
+ // First polygon is the outer boundary, rest are holes
156
+ // Flip Y back to CADit 2D coordinate system (Y-down)
157
+ const outer = sectionPolygons[0].map(([x, y]) => [x, -y]);
158
+ const holes = sectionPolygons.slice(1).map(hole => hole.map(([x, y]) => [x, -y]));
159
+ return { outer, holes };
160
+ }).filter(c => c.outer.length > 0);
161
+ // Center the compound polygons
162
+ return centerCompoundPolygons(compounds);
163
+ };
164
+ /**
165
+ * Samples an SVG data URL and returns compound polygons with proper hole detection.
166
+ * Uses Manifold's CrossSection.decompose() to correctly identify outer shapes and holes.
167
+ */
168
+ export const sampleSvgToPolygons = async (svgDataUrl, maxWidth) => {
169
+ const svgContent = svgDataUrlToString(svgDataUrl);
170
+ if (!svgContent) {
171
+ throw new Error('Failed to decode SVG data URL');
172
+ }
173
+ return svgContentToCompoundPolygons(svgContent, maxWidth);
174
+ };
175
+ /**
176
+ * Traces a bitmap image and returns compound polygons with proper hole detection.
177
+ * Uses Manifold's CrossSection.decompose() to correctly identify outer shapes and holes.
178
+ */
179
+ export const traceImageToPolygons = async (imageDataUrl, options) => {
180
+ const paths = traceDataUrl(imageDataUrl, {
181
+ threshold: options.threshold ?? THRESHOLD_AUTO,
182
+ invert: options.invert,
183
+ turnpolicy: 'black',
184
+ turdsize: options.despeckleSize ?? 2,
185
+ optcurve: true,
186
+ alphamax: 1,
187
+ opttolerance: 0.2
188
+ });
189
+ if (paths.length === 0) {
190
+ throw new Error('Potrace produced no paths. Check threshold or image contrast.');
191
+ }
192
+ // Generate SVG from traced paths
193
+ const svgContent = getSVG(paths, 1);
194
+ // Convert SVG to compound polygons with hole detection
195
+ return svgContentToCompoundPolygons(svgContent, options.maxWidth);
196
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cadit-app/image-extrude",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Image Extrude for CADit - Extrude shapes from SVG or bitmap images",
5
5
  "type": "module",
6
6
  "main": "dist/src/main.js",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@cadit-app/potrace-ts": "^1.3.0",
16
- "@cadit-app/script-params": "0.4.1",
16
+ "@cadit-app/script-params": "^0.5.4",
17
17
  "@cadit-app/svg-sampler": "^0.1.0",
18
18
  "@resvg/resvg-wasm": "^2.6.2"
19
19
  },