@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 +12 -28
- package/dist/cli.js +11 -4
- package/dist/src/main.d.ts +4 -3
- package/dist/src/main.d.ts.map +1 -1
- package/dist/src/main.js +27 -11
- package/dist/src/tracing.d.ts +37 -2
- package/dist/src/tracing.d.ts.map +1 -1
- package/dist/src/tracing.js +124 -6
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
112
|
-
const
|
|
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();
|
package/dist/src/main.d.ts
CHANGED
|
@@ -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 {
|
|
8
|
-
export {
|
|
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<
|
|
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
|
package/dist/src/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,
|
|
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 {
|
|
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 {
|
|
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
|
|
36
|
+
return createSceneOutput([]);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
if (!imageFile || !imageFile.dataUrl) {
|
|
40
40
|
console.warn('No valid image file provided.');
|
|
41
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
68
|
+
return createSceneOutput([]);
|
|
69
69
|
}
|
|
70
|
-
|
|
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
|
});
|
package/dist/src/tracing.d.ts
CHANGED
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { CrossSection } from '@cadit-app/manifold-3d/manifoldCAD';
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* A compound path with an outer boundary and optional holes.
|
|
9
|
+
* Used for CADit compound path shapes.
|
|
9
10
|
*/
|
|
10
|
-
export
|
|
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
|
|
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"}
|
package/dist/src/tracing.js
CHANGED
|
@@ -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
|
-
//
|
|
18
|
-
const
|
|
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
|
|
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.
|
|
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
|
|
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
|
},
|