Fix 3mf export
This commit is contained in:
parent
7c82459e1f
commit
18c1e6e12b
6 changed files with 87 additions and 44 deletions
|
|
@ -30,25 +30,32 @@ export default function ExportButton({className, style}: {className?: string, st
|
||||||
] : [
|
] : [
|
||||||
{
|
{
|
||||||
data: 'glb',
|
data: 'glb',
|
||||||
buttonLabel: 'GLB',
|
buttonLabel: 'Download GLB',
|
||||||
label: 'GLB (binary glTF)',
|
label: 'GLB (binary glTF)',
|
||||||
icon: 'pi pi-file',
|
icon: 'pi pi-file',
|
||||||
command: () => model!.setFormats(undefined, 'glb'),
|
command: () => model!.setFormats(undefined, 'glb'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: 'stl',
|
data: 'stl',
|
||||||
buttonLabel: 'STL',
|
buttonLabel: 'Download STL',
|
||||||
label: 'STL (binary)',
|
label: 'STL (binary)',
|
||||||
icon: 'pi pi-file',
|
icon: 'pi pi-file',
|
||||||
command: () => model!.setFormats(undefined, 'stl'),
|
command: () => model!.setFormats(undefined, 'stl'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: 'off',
|
data: 'off',
|
||||||
buttonLabel: 'OFF',
|
buttonLabel: 'Download OFF',
|
||||||
label: 'OFF (Object File Format)',
|
label: 'OFF (Object File Format)',
|
||||||
icon: 'pi pi-file',
|
icon: 'pi pi-file',
|
||||||
command: () => model!.setFormats(undefined, 'off'),
|
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;
|
const exportFormat = state.is2D ? state.params.exportFormat2D : state.params.exportFormat3D;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
import { Button } from 'primereact/button';
|
import { Button } from 'primereact/button';
|
||||||
import { ProgressBar } from 'primereact/progressbar';
|
import { ProgressBar } from 'primereact/progressbar';
|
||||||
import { Badge } from 'primereact/badge';
|
import { Badge } from 'primereact/badge';
|
||||||
import { Menu } from 'primereact/menu';
|
|
||||||
import { Toast } from 'primereact/toast';
|
import { Toast } from 'primereact/toast';
|
||||||
import HelpMenu from './HelpMenu';
|
import HelpMenu from './HelpMenu';
|
||||||
import ExportButton from './ExportButton';
|
import ExportButton from './ExportButton';
|
||||||
|
|
@ -53,14 +52,35 @@ export default function Footer({style}: {style?: CSSProperties}) {
|
||||||
margin: '5px',
|
margin: '5px',
|
||||||
...(style ?? {})
|
...(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"
|
icon="pi pi-bolt"
|
||||||
onClick={() => model.render({isPreview: false, now: true})}
|
onClick={() => model.render({isPreview: false, now: true})}
|
||||||
className="p-button-sm"
|
className="p-button-sm"
|
||||||
label="Render"
|
label="Render"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExportButton />
|
<ExportButton /> */}
|
||||||
|
|
||||||
{(state.lastCheckerRun || state.output) &&
|
{(state.lastCheckerRun || state.output) &&
|
||||||
<Button type="button"
|
<Button type="button"
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
getColorMapping(data.colors.map(c => chroma.rgb(...c)), extruderColors).map(i => PAINT_COLOR_MAP[i]);
|
||||||
|
|
||||||
const archive = {
|
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([
|
'3D/3dmodel.model': new TextEncoder().encode([
|
||||||
'<?xml version="1.0" encoding="utf-8"?>',
|
'<?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">',
|
'<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"/>',
|
'<meta name="slic3rpe:MmPaintingVersion" value="1"/>',
|
||||||
'<resources>',
|
'<resources>',
|
||||||
'<basematerials id="2">',
|
'<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>',
|
'</basematerials>',
|
||||||
`<object id="1" name="OpenSCAD Model" type="model" p:UUID="${objectUuid}" pid="2" pindex="0">`,
|
`<object id="1" name="OpenSCAD Model" type="model" p:UUID="${objectUuid}" pid="2" pindex="0">`,
|
||||||
'<mesh>',
|
'<mesh>',
|
||||||
|
|
@ -80,6 +73,18 @@ export function exportOffTo3MF(data: IndexedPolyhedron, extruderColors?: chroma.
|
||||||
'</build>',
|
'</build>',
|
||||||
'</model>',
|
'</model>',
|
||||||
].join('\n')),
|
].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'});
|
return new Blob([UZIP.encode(archive)], {type: 'model/3mf'});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 doc = new Document();
|
||||||
const lightExt = doc.createExtension(KHRLightsPunctual);
|
const lightExt = doc.createExtension(KHRLightsPunctual);
|
||||||
const buffer = doc.createBuffer();
|
const buffer = doc.createBuffer();
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ export const render =
|
||||||
if (source.content == null) throw new Error('Source content is null!');
|
if (source.content == null) throw new Error('Source content is null!');
|
||||||
const content = [...prefixLines, source.content].join('\n');
|
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 outFile = 'out.' + actualRenderFormat;
|
||||||
const args = [
|
const args = [
|
||||||
scadPath,
|
scadPath,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ import JSZip from 'jszip';
|
||||||
import { ProcessStreams } from "../runner/openscad-runner";
|
import { ProcessStreams } from "../runner/openscad-runner";
|
||||||
import { is2DFormatExtension } from "./formats";
|
import { is2DFormatExtension } from "./formats";
|
||||||
import { parseOff } from "../io/import_off";
|
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 {
|
export class Model {
|
||||||
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void,
|
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void,
|
||||||
|
|
@ -212,37 +213,47 @@ export class Model {
|
||||||
s.exporting = true;
|
s.exporting = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const outFile = this.state.output?.outFile;
|
if (!this.state.output?.outFile || !this.state.output?.outFileURL) {
|
||||||
const outFileURL = this.state.output?.outFileURL;
|
|
||||||
if (!outFile || !outFileURL) {
|
|
||||||
throw new Error('No output file to export');
|
throw new Error('No output file to export');
|
||||||
}
|
}
|
||||||
|
|
||||||
const scadPath = '/export.scad'
|
const {features, exportFormat2D, exportFormat3D} = this.state.params;
|
||||||
const sources: Source[] = [
|
const exportFormat = this.state.is2D ? exportFormat2D : exportFormat3D;
|
||||||
{
|
|
||||||
path: scadPath,
|
|
||||||
content: `import("${outFile.name}");`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: outFile.name,
|
|
||||||
url: outFileURL,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
let {features, exportFormat2D, exportFormat3D} = this.state.params;
|
|
||||||
|
|
||||||
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 {
|
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);
|
const outFileURL = URL.createObjectURL(output.outFile);
|
||||||
this.mutate(s => {
|
this.mutate(s => {
|
||||||
s.exporting = false;
|
s.exporting = false;
|
||||||
|
|
@ -372,7 +383,7 @@ export class Model {
|
||||||
}
|
}
|
||||||
if (displayFile.name.endsWith('.off')) {
|
if (displayFile.name.endsWith('.off')) {
|
||||||
const offData = parseOff(await displayFile.text());
|
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 outFileURL = URL.createObjectURL(output.outFile);
|
||||||
const displayFileURL = displayFile && await readFileAsDataURL(displayFile);
|
const displayFileURL = displayFile && await readFileAsDataURL(displayFile);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue