Fix 3mf export

This commit is contained in:
ochafik 2024-12-24 00:51:39 +00:00 • committed by Olivier Chafik
commit 18c1e6e12b
6 changed files with 87 additions and 44 deletions

View file

@ -30,25 +30,32 @@ export default function ExportButton({className, style}: {className?: string, st
] : [
{
data: 'glb',
buttonLabel: 'GLB',
buttonLabel: 'Download GLB',
label: 'GLB (binary glTF)',
icon: 'pi pi-file',
command: () => model!.setFormats(undefined, 'glb'),
},
{
data: 'stl',
buttonLabel: 'STL',
buttonLabel: 'Download STL',
label: 'STL (binary)',
icon: 'pi pi-file',
command: () => model!.setFormats(undefined, 'stl'),
},
{
data: 'off',
buttonLabel: 'OFF',
buttonLabel: 'Download OFF',
label: 'OFF (Object File Format)',
icon: 'pi pi-file',
command: () => model!.setFormats(undefined, 'off'),
},
{
data: '3mf',
buttonLabel: 'Download 3MF',
label: '3MF (3D Manufacturing Format)',
icon: 'pi pi-file',
command: () => model!.setFormats(undefined, '3mf'),
},
];
const exportFormat = state.is2D ? state.params.exportFormat2D : state.params.exportFormat3D;

View file

@ -6,7 +6,6 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { Button } from 'primereact/button';
import { ProgressBar } from 'primereact/progressbar';
import { Badge } from 'primereact/badge';
import { Menu } from 'primereact/menu';
import { Toast } from 'primereact/toast';
import HelpMenu from './HelpMenu';
import ExportButton from './ExportButton';
@ -53,14 +52,35 @@ export default function Footer({style}: {style?: CSSProperties}) {
margin: '5px',
...(style ?? {})
}}>
<Button
{state.output && !state.output.isPreview
? (
<ExportButton />
) : state.previewing ? (
<Button
icon="pi pi-bolt"
disabled
className="p-button-sm"
label="Previewing..."
/>
) : state.output && state.output.isPreview ? (
<Button
icon="pi pi-bolt"
onClick={() => model.render({isPreview: false, now: true})}
className="p-button-sm"
disabled={state.rendering}
label={state.rendering ? 'Rendering...' : 'Render'}
/>
) : undefined
}
{/* <RenderExportButton /> */}
{/* <Button
icon="pi pi-bolt"
onClick={() => model.render({isPreview: false, now: true})}
className="p-button-sm"
label="Render"
/>
<ExportButton />
<ExportButton /> */}
{(state.lastCheckerRun || state.output) &&
<Button type="button"

View file

@ -35,13 +35,6 @@ export function exportOffTo3MF(data: IndexedPolyhedron, extruderColors?: chroma.
getColorMapping(data.colors.map(c => chroma.rgb(...c)), extruderColors).map(i => PAINT_COLOR_MAP[i]);
const archive = {
// _rels/.rels
'[Content Types].xml': new TextEncoder().encode([
'<?xml version="1.0" encoding="utf-8"?>',
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
'<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>',
'</Types>',
].join('\n')),
'3D/3dmodel.model': new TextEncoder().encode([
'<?xml version="1.0" encoding="utf-8"?>',
'<model unit="millimeter" xml:lang="en-US" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06">',
@ -50,7 +43,7 @@ export function exportOffTo3MF(data: IndexedPolyhedron, extruderColors?: chroma.
'<meta name="slic3rpe:MmPaintingVersion" value="1"/>',
'<resources>',
'<basematerials id="2">',
...data.colors.map((color, i) => `<base name="color_${i}" displaycolor="#${chroma.rgb(...color).hex()}"/>`),
...data.colors.map((color, i) => `<base name="color_${i}" displaycolor="${chroma.rgb(...color).hex()}"/>`),
'</basematerials>',
`<object id="1" name="OpenSCAD Model" type="model" p:UUID="${objectUuid}" pid="2" pindex="0">`,
'<mesh>',
@ -80,6 +73,18 @@ export function exportOffTo3MF(data: IndexedPolyhedron, extruderColors?: chroma.
'</build>',
'</model>',
].join('\n')),
'[Content_Types].xml': new TextEncoder().encode([
'<?xml version="1.0" encoding="utf-8"?>',
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
'<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>',
'</Types>',
].join('\n')),
'_rels/.rels': new TextEncoder().encode([
'<?xml version="1.0" encoding="utf-8"?>',
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
'<Relationship Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel" Target="/3D/3dmodel.model" Id="rel0"/>',
'</Relationships>',
].join('\n')),
};
return new Blob([UZIP.encode(archive)], {type: 'model/3mf'});
}

View file

@ -68,7 +68,7 @@ function getGeom(data: IndexedPolyhedron): Geom {
};
}
export async function convertOffToGlb(data: IndexedPolyhedron, defaultColor: Color = DEFAULT_FACE_COLOR): Promise<Blob> {
export async function exportGlb(data: IndexedPolyhedron, defaultColor: Color = DEFAULT_FACE_COLOR): Promise<Blob> {
const doc = new Document();
const lightExt = doc.createExtension(KHRLightsPunctual);
const buffer = doc.createBuffer();

View file

@ -128,7 +128,7 @@ export const render =
if (source.content == null) throw new Error('Source content is null!');
const content = [...prefixLines, source.content].join('\n');
const actualRenderFormat = renderFormat == 'glb' ? 'off' : renderFormat;
const actualRenderFormat = renderFormat == 'glb' || renderFormat == '3mf' ? 'off' : renderFormat;
const outFile = 'out.' + actualRenderFormat;
const args = [
scadPath,

View file

@ -10,7 +10,8 @@ import JSZip from 'jszip';
import { ProcessStreams } from "../runner/openscad-runner";
import { is2DFormatExtension } from "./formats";
import { parseOff } from "../io/import_off";
import { convertOffToGlb } from "../io/export_glb";
import { exportGlb } from "../io/export_glb";
import { exportOffTo3MF as export3MF } from "../io/export_3mf";
export class Model {
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void,
@ -212,37 +213,47 @@ export class Model {
s.exporting = true;
});
const outFile = this.state.output?.outFile;
const outFileURL = this.state.output?.outFileURL;
if (!outFile || !outFileURL) {
if (!this.state.output?.outFile || !this.state.output?.outFileURL) {
throw new Error('No output file to export');
}
const scadPath = '/export.scad'
const sources: Source[] = [
{
path: scadPath,
content: `import("${outFile.name}");`
},
{
path: outFile.name,
url: outFileURL,
}
];
let {features, exportFormat2D, exportFormat3D} = this.state.params;
const {features, exportFormat2D, exportFormat3D} = this.state.params;
const exportFormat = this.state.is2D ? exportFormat2D : exportFormat3D;
const renderArgs: RenderArgs = {
mountArchives: false,
scadPath,
sources,
extraArgs: [], isPreview: false,
features,
renderFormat: this.state.is2D ? exportFormat2D : exportFormat3D,
streamsCallback: ps => console.log('Export', JSON.stringify(ps)),
};
try {
const output = await render(renderArgs)({now: true});
let output: RenderOutput;
if (exportFormat === '3mf') {
const start = performance.now();
const data = parseOff(await this.state.output.outFile.text());
const exportedData = export3MF(data);
const elapsedMillis = performance.now() - start;
output = {
outFile: new File([exportedData], this.state.output.outFile.name.replace('.off', '.3mf')),
elapsedMillis,
logText: '',
markers: [],
};
} else {
output = await render({
mountArchives: false,
scadPath: '/export.scad',
sources: [
{
path: '/export.scad',
content: `import("${this.state.output?.outFile.name}");`
},
{
path: this.state.output?.outFile.name,
url: this.state.output?.outFileURL,
}
],
extraArgs: [], isPreview: false,
features,
renderFormat: exportFormat,
streamsCallback: ps => console.log('Export', JSON.stringify(ps)),
})({now: true});
}
const outFileURL = URL.createObjectURL(output.outFile);
this.mutate(s => {
s.exporting = false;
@ -372,7 +383,7 @@ export class Model {
}
if (displayFile.name.endsWith('.off')) {
const offData = parseOff(await displayFile.text());
displayFile = new File([await convertOffToGlb(offData)], displayFile.name.replace('.off', '.glb'));
displayFile = new File([await exportGlb(offData)], displayFile.name.replace('.off', '.glb'));
}
const outFileURL = URL.createObjectURL(output.outFile);
const displayFileURL = displayFile && await readFileAsDataURL(displayFile);