@cadit-app/image-extrude 0.3.1 → 0.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cadit-app/image-extrude",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Image Extrude for CADit - Extrude shapes from SVG or bitmap images",
5
5
  "type": "module",
6
6
  "main": "dist/bundle.js",
@@ -15,15 +15,8 @@
15
15
  "types": "./dist/src/main.d.ts"
16
16
  }
17
17
  },
18
- "scripts": {
19
- "typecheck": "tsc --noEmit",
20
- "build:types": "tsc",
21
- "build:bundle": "vite build",
22
- "build": "npm run build:types && npm run build:bundle",
23
- "prepublishOnly": "npm run build",
24
- "generate": "npx tsx cli.ts",
25
- "build:glb": "npx tsx cli.ts output.glb",
26
- "build:3mf": "npx tsx cli.ts output.3mf"
18
+ "cadit": {
19
+ "browserBundle": "./dist/bundle.js"
27
20
  },
28
21
  "dependencies": {
29
22
  "@cadit-app/script-params": "0.4.1",
@@ -59,12 +52,20 @@
59
52
  "license": "MIT",
60
53
  "files": [
61
54
  "dist/**/*",
62
- "src/**/*",
63
55
  "images/**/*",
64
56
  "cadit.json",
65
57
  "README.md"
66
58
  ],
67
59
  "publishConfig": {
68
60
  "access": "public"
61
+ },
62
+ "scripts": {
63
+ "typecheck": "tsc --noEmit",
64
+ "build:types": "tsc",
65
+ "build:bundle": "vite build",
66
+ "build": "npm run build:types && npm run build:bundle",
67
+ "generate": "npx tsx cli.ts",
68
+ "build:glb": "npx tsx cli.ts output.glb",
69
+ "build:3mf": "npx tsx cli.ts output.3mf"
69
70
  }
70
- }
71
+ }
@@ -1,30 +0,0 @@
1
- /**
2
- * CrossSection utility functions
3
- */
4
-
5
- import { CrossSection } from '@cadit-app/manifold-3d/manifoldCAD';
6
-
7
- /**
8
- * Centers a CrossSection at the origin based on its bounding box
9
- */
10
- export function centerCrossSection(crossSection: CrossSection): CrossSection {
11
- const bounds = crossSection.bounds();
12
- const centerX = (bounds.min[0] + bounds.max[0]) / 2;
13
- const centerY = (bounds.min[1] + bounds.max[1]) / 2;
14
- return crossSection.translate([-centerX, -centerY]);
15
- }
16
-
17
- /**
18
- * Scales a CrossSection to fit within a maximum size while maintaining aspect ratio
19
- */
20
- export function scaleToMaxSize(crossSection: CrossSection, maxSize: number): CrossSection {
21
- const bounds = crossSection.bounds();
22
- const width = bounds.max[0] - bounds.min[0];
23
- const height = bounds.max[1] - bounds.min[1];
24
- const maxDim = Math.max(width, height);
25
-
26
- if (maxDim <= 0) return crossSection;
27
-
28
- const scale = maxSize / maxDim;
29
- return crossSection.scale([scale, scale]);
30
- }
package/src/main.ts DELETED
@@ -1,77 +0,0 @@
1
- /**
2
- * @cadit-app/image-extrude
3
- *
4
- * Extrude 3D shapes from SVG or bitmap images.
5
- * Uses the defineParams API from @cadit-app/script-params.
6
- */
7
-
8
- import { defineParams } from '@cadit-app/script-params';
9
- import type { Manifold, CrossSection } from '@cadit-app/manifold-3d/manifoldCAD';
10
- import { imageExtrudeParamsSchema, ImageExtrudeParams, ImageFileValue } from './params';
11
- import { sampleSvg, traceImage } from './tracing';
12
- import { renderSvgToBitmapDataUrl } from './resvg';
13
- import { fetchImageAsDataUrl } from './utils';
14
- import { createEmptyManifold } from './manifoldUtils';
15
-
16
- // Re-export for external use
17
- export { sampleSvg, traceImage } from './tracing';
18
- export { renderSvgToBitmapDataUrl } from './resvg';
19
- export { makeCrossSection } from './makeCrossSection';
20
-
21
- /**
22
- * Main entry point using defineParams
23
- */
24
- export default defineParams({
25
- params: imageExtrudeParamsSchema as any,
26
- main: async (params): Promise<Manifold> => {
27
- const typedParams = params as unknown as ImageExtrudeParams;
28
- let { mode, height } = typedParams;
29
- let imageFile: ImageFileValue | undefined = typedParams.imageFile;
30
-
31
- // If imageFile has imageUrl but not dataUrl, fetch and convert to dataUrl
32
- if (imageFile && !imageFile.dataUrl && imageFile.imageUrl) {
33
- try {
34
- imageFile = {
35
- ...imageFile,
36
- dataUrl: await fetchImageAsDataUrl(imageFile.imageUrl)
37
- };
38
- } catch (err) {
39
- console.warn('Failed to fetch imageUrl:', err);
40
- return createEmptyManifold();
41
- }
42
- }
43
-
44
- if (!imageFile || !imageFile.dataUrl) {
45
- console.warn('No valid image file provided.');
46
- return createEmptyManifold();
47
- }
48
-
49
- // Adjust mode if sample is selected for non-SVG
50
- if (mode === 'sample' && !imageFile.fileType?.includes('svg')) {
51
- console.warn('Sample mode selected for non-SVG file. Defaulting to Trace mode.');
52
- mode = 'trace';
53
- }
54
-
55
- let crossSection: CrossSection;
56
- try {
57
- if (mode === 'trace') {
58
- // if svg, render svg to bitmap and then trace
59
- const isSvg = imageFile.fileType?.includes('svg');
60
- const dataUrl = isSvg ? await renderSvgToBitmapDataUrl(imageFile.dataUrl) : imageFile.dataUrl;
61
-
62
- crossSection = await traceImage(dataUrl, {
63
- maxWidth: typedParams.maxWidth,
64
- despeckleSize: typedParams.despeckleSize
65
- });
66
- } else {
67
- // mode is 'sample', and fileType is guaranteed to be svg+xml
68
- crossSection = await sampleSvg(imageFile.dataUrl, typedParams.maxWidth);
69
- }
70
- } catch (error) {
71
- console.error(`Error during image processing (mode: ${mode}):`, error);
72
- return createEmptyManifold();
73
- }
74
-
75
- return crossSection.extrude(height);
76
- },
77
- });
@@ -1,57 +0,0 @@
1
- /**
2
- * Create a CrossSection from image parameters
3
- * Exported for external use (e.g., embedding in other makers)
4
- */
5
-
6
- import type { CrossSection } from '@cadit-app/manifold-3d/manifoldCAD';
7
- import { ImageExtrudeParams } from './params';
8
- import { sampleSvg, traceImage } from './tracing';
9
- import { renderSvgToBitmapDataUrl } from './resvg';
10
- import { fetchImageAsDataUrl } from './utils';
11
-
12
- export type MakeCrossSectionOptions = {
13
- imageFile: ImageExtrudeParams['imageFile'];
14
- mode: 'trace' | 'sample';
15
- maxWidth?: number;
16
- despeckleSize?: number;
17
- };
18
-
19
- /**
20
- * Creates a CrossSection from image data
21
- * This is the main function for embedding in other makers
22
- */
23
- export async function makeCrossSection(options: MakeCrossSectionOptions): Promise<CrossSection> {
24
- let { imageFile, mode, maxWidth, despeckleSize } = options;
25
-
26
- // If imageFile has imageUrl but not dataUrl, fetch and convert
27
- if (imageFile && !imageFile.dataUrl && imageFile.imageUrl) {
28
- imageFile = {
29
- ...imageFile,
30
- dataUrl: await fetchImageAsDataUrl(imageFile.imageUrl)
31
- };
32
- }
33
-
34
- if (!imageFile?.dataUrl) {
35
- throw new Error('No valid image file provided');
36
- }
37
-
38
- // Adjust mode if sample is selected for non-SVG
39
- if (mode === 'sample' && !imageFile.fileType?.includes('svg')) {
40
- console.warn('Sample mode selected for non-SVG file. Defaulting to Trace mode.');
41
- mode = 'trace';
42
- }
43
-
44
- if (mode === 'trace') {
45
- // if svg, render svg to bitmap and then trace
46
- const isSvg = imageFile.fileType?.includes('svg');
47
- const dataUrl = isSvg ? await renderSvgToBitmapDataUrl(imageFile.dataUrl) : imageFile.dataUrl;
48
-
49
- return traceImage(dataUrl, {
50
- maxWidth,
51
- despeckleSize
52
- });
53
- } else {
54
- // mode is 'sample', and fileType is guaranteed to be svg+xml
55
- return sampleSvg(imageFile.dataUrl, maxWidth);
56
- }
57
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Manifold utility functions
3
- */
4
-
5
- import { Manifold, CrossSection } from '@cadit-app/manifold-3d/manifoldCAD';
6
-
7
- /**
8
- * Creates an empty manifold (a very small cube that will be invisible)
9
- */
10
- export function createEmptyManifold(): Manifold {
11
- // Create a tiny box that's essentially invisible
12
- return CrossSection.square([0.001, 0.001], true).extrude(0.001);
13
- }
package/src/params.ts DELETED
@@ -1,70 +0,0 @@
1
- /**
2
- * Parameter schema for the Image Extrude generator.
3
- */
4
-
5
- /**
6
- * Value type for image parameters.
7
- * Matches the ImageFileValue type from @cadit-app/script-params.
8
- * TODO: Import from script-params once version with ImageFileValue is published.
9
- */
10
- export interface ImageFileValue {
11
- /** Remote URL to fetch the image from (e.g., HTTP URL or relative path). */
12
- imageUrl?: string;
13
- /** Base64-encoded data URL of the image content. */
14
- dataUrl?: string;
15
- /** MIME type of the image (e.g., 'image/svg+xml', 'image/png'). */
16
- fileType?: string;
17
- /** Original filename of the image. */
18
- fileName?: string;
19
- }
20
-
21
- // Default image - CookieCAD logo from GitHub via jsdelivr CDN
22
- const defaultImageUrl = 'https://cdn.jsdelivr.net/gh/CADit-app/image-extrude@master/images/cookiecad-logo-dark.svg';
23
-
24
- export const imageExtrudeParamsSchema = {
25
- mode: {
26
- type: 'choice' as const,
27
- label: 'Mode',
28
- options: [
29
- { value: 'trace', label: 'Trace' },
30
- { value: 'sample', label: 'Sample (SVG only)' },
31
- ],
32
- default: 'trace',
33
- },
34
- imageFile: {
35
- type: 'image' as const,
36
- label: 'Image File',
37
- default: {
38
- imageUrl: defaultImageUrl,
39
- dataUrl: '',
40
- fileType: 'image/svg+xml',
41
- fileName: 'cookiecad-logo-dark.svg'
42
- } as ImageFileValue,
43
- },
44
- height: {
45
- type: 'number' as const,
46
- label: 'Extrusion Height (mm)',
47
- default: 1,
48
- min: 0.1,
49
- },
50
- maxWidth: {
51
- type: 'number' as const,
52
- label: 'Maximum Width (mm)',
53
- default: 50,
54
- min: 0.1,
55
- },
56
- despeckleSize: {
57
- type: 'number' as const,
58
- label: 'Despeckle Size (Tracing only)',
59
- default: 2,
60
- min: 0.1,
61
- },
62
- };
63
-
64
- export type ImageExtrudeParams = {
65
- mode: 'trace' | 'sample';
66
- imageFile: ImageFileValue;
67
- height: number;
68
- maxWidth: number;
69
- despeckleSize: number;
70
- };
package/src/resvg.ts DELETED
@@ -1,79 +0,0 @@
1
- /**
2
- * SVG to bitmap rendering using resvg-wasm
3
- *
4
- * Note: When running as a dynamically loaded script in CADit, the WASM module
5
- * is loaded from esm.sh CDN since Vite's ?url import syntax isn't available.
6
- */
7
-
8
- import * as resvg from '@resvg/resvg-wasm';
9
- import { svgDataUrlToString } from './utils';
10
-
11
- // Hardcoded version for WASM URL - update when upgrading @resvg/resvg-wasm dependency
12
- const RESVG_VERSION = '2.6.2';
13
-
14
- let wasmInitialized = false;
15
- let wasmInitPromise: Promise<void> | null = null;
16
-
17
- /**
18
- * Initialize resvg WASM module.
19
- * Fetches the WASM binary from esm.sh CDN to work in dynamically loaded scripts.
20
- *
21
- * Safe to call multiple times - subsequent calls return immediately.
22
- * If initialization fails, subsequent calls will retry.
23
- */
24
- async function initializeResvgWasm(): Promise<void> {
25
- if (wasmInitialized) return;
26
- if (wasmInitPromise) return wasmInitPromise;
27
-
28
- wasmInitPromise = (async () => {
29
- try {
30
- // Fetch WASM from esm.sh CDN
31
- const wasmUrl = `https://esm.sh/@resvg/resvg-wasm@${RESVG_VERSION}/index_bg.wasm`;
32
- await resvg.initWasm(fetch(wasmUrl));
33
- wasmInitialized = true;
34
- } catch (error) {
35
- // Reset promise so retry is possible
36
- wasmInitPromise = null;
37
- throw error;
38
- }
39
- })();
40
-
41
- return wasmInitPromise;
42
- }
43
-
44
- /**
45
- * Renders an SVG Data URL to a PNG Data URL using resvg-wasm.
46
- */
47
- export const renderSvgToBitmapDataUrl = async (
48
- svgDataUrl: string,
49
- options?: { maxWidth?: number }
50
- ): Promise<string> => {
51
- await initializeResvgWasm();
52
-
53
- const svgString = svgDataUrlToString(svgDataUrl);
54
- if (!svgString) {
55
- throw new Error("Could not decode SVG Data URL");
56
- }
57
-
58
- const opts: Record<string, unknown> = {};
59
- if (options?.maxWidth && options.maxWidth > 0 && isFinite(options.maxWidth)) {
60
- opts.fitTo = {
61
- mode: 'width',
62
- value: options.maxWidth
63
- };
64
- }
65
-
66
- const resvgInstance = new resvg.Resvg(svgString, opts);
67
- const pngData = resvgInstance.render();
68
- const pngBuffer = pngData.asPng(); // Uint8Array
69
-
70
- // Convert Uint8Array to proper ArrayBuffer for Blob
71
- const arrayBuffer = pngBuffer.buffer.slice(
72
- pngBuffer.byteOffset,
73
- pngBuffer.byteOffset + pngBuffer.byteLength
74
- ) as ArrayBuffer;
75
-
76
- // Return blob URL (browser-compatible and more efficient)
77
- const blob = new Blob([arrayBuffer], { type: 'image/png' });
78
- return URL.createObjectURL(blob);
79
- };
@@ -1,67 +0,0 @@
1
- /**
2
- * 3MF Export for image-extrude
3
- */
4
-
5
- import type { Manifold } from '@cadit-app/manifold-3d/manifoldCAD';
6
- // @ts-ignore - No type declarations available
7
- import { to3dmodel, fileForContentTypes, FileForRelThumbnail } from '@jscadui/3mf-export';
8
- import { strToU8, zipSync, Zippable } from 'fflate';
9
-
10
- /**
11
- * Creates a 3MF ArrayBuffer from manifolds
12
- */
13
- export async function create3mfArrayBuffer(manifolds: Manifold[]): Promise<ArrayBuffer> {
14
- const meshes = manifolds.map((m, index) => {
15
- const mesh = m.getMesh();
16
- return {
17
- id: String(index + 1),
18
- vertices: mesh.vertProperties,
19
- indices: mesh.triVerts,
20
- name: `Part-${index + 1}`,
21
- };
22
- });
23
-
24
- // Generate a single component with all meshes as children
25
- const components = [
26
- {
27
- id: meshes.length + 1,
28
- children: meshes.map((mesh) => ({ objectID: mesh.id })),
29
- name: 'ImageExtrude-Assembly',
30
- },
31
- ];
32
-
33
- // The main item should reference the component
34
- const items = components.map((component) => ({
35
- objectID: component.id,
36
- }));
37
-
38
- const header = {
39
- unit: 'millimeter',
40
- title: 'CADit Image Extrude',
41
- description: 'Image Extrude 3MF export',
42
- application: 'CADit',
43
- };
44
-
45
- const to3mf = {
46
- meshes,
47
- components,
48
- items,
49
- precision: 7,
50
- header,
51
- };
52
-
53
- // Generate the 3D model XML
54
- const model = to3dmodel(to3mf as any);
55
-
56
- // Package the 3MF file using fflate
57
- const fileForRelThumbnail = new FileForRelThumbnail();
58
- fileForRelThumbnail.add3dModel('3D/3dmodel.model');
59
-
60
- const files: Zippable = {};
61
- files['3D/3dmodel.model'] = strToU8(model);
62
- files[fileForContentTypes.name] = strToU8(fileForContentTypes.content);
63
- files[fileForRelThumbnail.name] = strToU8(fileForRelThumbnail.content);
64
-
65
- const zipData = zipSync(files);
66
- return zipData.buffer as ArrayBuffer;
67
- }
package/src/tracing.ts DELETED
@@ -1,98 +0,0 @@
1
- /**
2
- * Image tracing and SVG sampling utilities
3
- *
4
- * Uses potrace for bitmap tracing. When running in CADit, potrace is exposed
5
- * as an importable library by the code.worker, which handles the jimp dependency.
6
- */
7
-
8
- import { svgToPolygons } from '@cadit-app/svg-sampler';
9
- import { CrossSection } from '@cadit-app/manifold-3d/manifoldCAD';
10
- import { Potrace } from 'potrace';
11
- import { svgDataUrlToString } from './utils';
12
- import { centerCrossSection } from './crossSectionUtils';
13
-
14
- /**
15
- * Converts SVG content to polygons
16
- */
17
- export const svgContentToPolygons = async (
18
- svgContent: string,
19
- maxError: number
20
- ) => {
21
- // Sample the SVG into polygons
22
- const polygons = await svgToPolygons(svgContent, { maxError });
23
-
24
- // Flip the Y-axis for SVG paths (SVG uses Y-down, but 3D modeling uses Y-up)
25
- const flippedPolygons = polygons.map((polygon) => {
26
- return polygon.points.map(([x, y]) => [x, -y]) as [number, number][];
27
- });
28
-
29
- return flippedPolygons;
30
- };
31
-
32
- /**
33
- * Converts an SVG string to a CrossSection with optional scaling
34
- */
35
- export const svgStringToCrossSection = async (
36
- svgContent: string,
37
- maxWidth?: number,
38
- maxError: number = 0.01
39
- ): Promise<CrossSection> => {
40
- const polygons = await svgContentToPolygons(svgContent, maxError);
41
-
42
- const crossSection = new CrossSection(polygons, 'EvenOdd').simplify(maxError);
43
- if (!maxWidth) return crossSection;
44
-
45
- // Check the width of the resulting CrossSection
46
- const boundingBox = crossSection.bounds();
47
- const width = boundingBox.max[0] - boundingBox.min[0];
48
- const scaleFactor = maxWidth / width;
49
- const scaledError = maxError / scaleFactor;
50
-
51
- // Sample again with new error
52
- const newPolygons = await svgContentToPolygons(svgContent, scaledError);
53
- const newCrossSection = new CrossSection(newPolygons, 'EvenOdd').simplify(scaledError);
54
- return newCrossSection.scale([scaleFactor, scaleFactor]);
55
- };
56
-
57
- /**
58
- * Samples an SVG data URL and returns a centered CrossSection
59
- */
60
- export const sampleSvg = async (svgDataUrl: string, maxWidth?: number): Promise<CrossSection> => {
61
- const svgContent = svgDataUrlToString(svgDataUrl);
62
- if (!svgContent) {
63
- throw new Error('Failed to parse SVG data URL');
64
- }
65
- const crossSection = await svgStringToCrossSection(svgContent, maxWidth);
66
- return centerCrossSection(crossSection);
67
- };
68
-
69
- /**
70
- * Traces a bitmap image and returns a centered CrossSection
71
- * Uses potrace with jimp for image loading (works in web workers).
72
- */
73
- export const traceImage = async (
74
- imageDataUrl: string,
75
- options: {
76
- maxWidth?: number;
77
- despeckleSize?: number;
78
- }
79
- ): Promise<CrossSection> => {
80
- const tracer = new Potrace({
81
- turdSize: options.despeckleSize || 2,
82
- });
83
-
84
- // Promisify tracer.loadImage
85
- await new Promise<void>((resolve, reject) => {
86
- tracer.loadImage(imageDataUrl, (_potrace, err) => {
87
- if (err) {
88
- reject(err);
89
- } else {
90
- resolve();
91
- }
92
- });
93
- });
94
-
95
- const svgContent = tracer.getSVG();
96
- const crossSection = await svgStringToCrossSection(svgContent, options.maxWidth);
97
- return centerCrossSection(crossSection);
98
- };
package/src/utils.ts DELETED
@@ -1,55 +0,0 @@
1
- /**
2
- * Utility functions for image processing
3
- */
4
-
5
- /**
6
- * Extracts the plain SVG XML string from a Data URL.
7
- * Handles both Base64 encoded and URL-encoded SVG data URLs.
8
- */
9
- export function svgDataUrlToString(dataUrl: string): string | null {
10
- if (!dataUrl || !dataUrl.startsWith('data:image/svg+xml')) {
11
- console.error("Invalid SVG Data URL format.");
12
- return null;
13
- }
14
-
15
- const commaIndex = dataUrl.indexOf(',');
16
- if (commaIndex === -1) {
17
- console.error("Invalid Data URL: Missing comma separator.");
18
- return null;
19
- }
20
-
21
- const header = dataUrl.substring(0, commaIndex);
22
- const encodedData = dataUrl.substring(commaIndex + 1);
23
-
24
- if (header.includes(';base64')) {
25
- try {
26
- // Decode Base64
27
- return atob(encodedData);
28
- } catch (e) {
29
- console.error("Error decoding Base64 SVG data:", e);
30
- return null;
31
- }
32
- } else {
33
- try {
34
- // Decode URL-encoded string
35
- return decodeURIComponent(encodedData);
36
- } catch (e) {
37
- console.error("Error decoding URL-encoded SVG data:", e);
38
- return null;
39
- }
40
- }
41
- }
42
-
43
- /**
44
- * Fetches an image URL and converts it to a data URL
45
- */
46
- export async function fetchImageAsDataUrl(imageUrl: string): Promise<string> {
47
- const response = await fetch(imageUrl);
48
- const blob = await response.blob();
49
- return new Promise<string>((resolve, reject) => {
50
- const reader = new FileReader();
51
- reader.onloadend = () => resolve(reader.result as string);
52
- reader.onerror = reject;
53
- reader.readAsDataURL(blob);
54
- });
55
- }