@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 +13 -12
- package/src/crossSectionUtils.ts +0 -30
- package/src/main.ts +0 -77
- package/src/makeCrossSection.ts +0 -57
- package/src/manifoldUtils.ts +0 -13
- package/src/params.ts +0 -70
- package/src/resvg.ts +0 -79
- package/src/threeMfExport.ts +0 -67
- package/src/tracing.ts +0 -98
- package/src/utils.ts +0 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cadit-app/image-extrude",
|
|
3
|
-
"version": "0.3.
|
|
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
|
-
"
|
|
19
|
-
"
|
|
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
|
+
}
|
package/src/crossSectionUtils.ts
DELETED
|
@@ -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
|
-
});
|
package/src/makeCrossSection.ts
DELETED
|
@@ -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
|
-
}
|
package/src/manifoldUtils.ts
DELETED
|
@@ -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
|
-
};
|
package/src/threeMfExport.ts
DELETED
|
@@ -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
|
-
}
|