diff --git a/bindings/wasm/examples/3mf-export.d.ts b/bindings/wasm/examples/3mf-export.d.ts new file mode 100644 index 000000000..2d6cce2c4 --- /dev/null +++ b/bindings/wasm/examples/3mf-export.d.ts @@ -0,0 +1 @@ +declare module '@jscadui/3mf-export'; diff --git a/bindings/wasm/examples/editor.js b/bindings/wasm/examples/editor.js index 80a957351..224965561 100644 --- a/bindings/wasm/examples/editor.js +++ b/bindings/wasm/examples/editor.js @@ -347,7 +347,8 @@ function finishRun() { } const mv = document.querySelector('model-viewer'); -let objectURL = null; +let glbURL = null; +let threeMFURL = null; let manifoldWorker = null; function createWorker() { @@ -370,10 +371,16 @@ function createWorker() { finishRun(); runButton.disabled = true; - URL.revokeObjectURL(objectURL); - objectURL = e.data.objectURL; - mv.src = objectURL; - if (objectURL == null) { + if (threeMFURL != undefined) { + URL.revokeObjectURL(threeMFURL); + threeMFURL = undefined; + } + URL.revokeObjectURL(glbURL); + glbURL = e.data.glbURL; + threeMFURL = e.data.threeMFURL; + threemfButton.disabled = threeMFURL == undefined; + mv.src = glbURL; + if (glbURL == null) { mv.showPoster(); poster.textContent = 'Error'; createWorker(); @@ -411,10 +418,18 @@ runButton.onclick = function() { } }; -const downloadButton = document.querySelector('#download'); -downloadButton.onclick = function() { +const glbButton = document.querySelector('#glb'); +glbButton.onclick = function() { const link = document.createElement('a'); link.download = 'manifold.glb'; - link.href = objectURL; + link.href = glbURL; + link.click(); +}; + +const threemfButton = document.querySelector('#threemf'); +threemfButton.onclick = function() { + const link = document.createElement('a'); + link.download = 'manifold.3mf'; + link.href = threeMFURL; link.click(); }; diff --git a/bindings/wasm/examples/index.html b/bindings/wasm/examples/index.html index cace6dc92..74037e1db 100644 --- a/bindings/wasm/examples/index.html +++ b/bindings/wasm/examples/index.html @@ -48,7 +48,8 @@ - + +
@@ -74,4 +75,4 @@ - \ No newline at end of file + diff --git a/bindings/wasm/examples/package-lock.json b/bindings/wasm/examples/package-lock.json index 7f3128ca9..bf282e629 100644 --- a/bindings/wasm/examples/package-lock.json +++ b/bindings/wasm/examples/package-lock.json @@ -11,6 +11,8 @@ "@gltf-transform/core": "^3.2.1", "@gltf-transform/extensions": "^3.2.1", "@gltf-transform/functions": "^3.2.1", + "@jscadui/3mf-export": "^0.3.0", + "fflate": "^0.8.0", "gl-matrix": "^3.4.3", "simple-dropzone": "0.8.3", "three": "0.151.2" @@ -411,6 +413,11 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, + "node_modules/@jscadui/3mf-export": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@jscadui/3mf-export/-/3mf-export-0.3.0.tgz", + "integrity": "sha512-5NfknQSTO2+rt7m7PGDvIST8V2RNNWBGvQPEmVY2DTMCEZ6y4zyIltxwVbnQ6NRCEeWZseSb3FpmflU1AWE9vw==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -581,6 +588,12 @@ "vitest": ">=0.30.1 <1" } }, + "node_modules/@vitest/ui/node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "dev": true + }, "node_modules/@vitest/utils": { "version": "0.31.1", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.1.tgz", @@ -1000,10 +1013,9 @@ } }, "node_modules/fflate": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", - "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", - "dev": true + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.0.tgz", + "integrity": "sha512-FAdS4qMuFjsJj6XHbBaZeXOgaypXp8iw/Tpyuq/w3XA41jjLHT8NPA+n7czH/DDhdncq0nAyDZmPeWXh2qmdIg==" }, "node_modules/fill-range": { "version": "7.0.1", diff --git a/bindings/wasm/examples/package.json b/bindings/wasm/examples/package.json index 57273b4db..e84f7aa87 100644 --- a/bindings/wasm/examples/package.json +++ b/bindings/wasm/examples/package.json @@ -16,6 +16,8 @@ "@gltf-transform/core": "^3.2.1", "@gltf-transform/extensions": "^3.2.1", "@gltf-transform/functions": "^3.2.1", + "@jscadui/3mf-export": "^0.3.0", + "fflate": "^0.8.0", "gl-matrix": "^3.4.3", "simple-dropzone": "0.8.3", "three": "0.151.2" diff --git a/bindings/wasm/examples/worker.test.js b/bindings/wasm/examples/worker.test.js index 4b870a230..2c8fb0c89 100644 --- a/bindings/wasm/examples/worker.test.js +++ b/bindings/wasm/examples/worker.test.js @@ -39,7 +39,7 @@ function initialized(worker) { }); } -let objectURL = null; +let glbURL = null; async function runExample(name) { const worker = new ManifoldWorker(); @@ -55,12 +55,12 @@ async function runExample(name) { worker.onmessage = async function(e) { try { - URL.revokeObjectURL(objectURL); - objectURL = e.data.objectURL; - if (objectURL == null) { - reject('no objectURL'); + URL.revokeObjectURL(glbURL); + glbURL = e.data.glbURL; + if (glbURL == null) { + reject('no glbURL)'); } - const docIn = await io.read(objectURL); + const docIn = await io.read(glbURL); const nodes = docIn.getRoot().listNodes(); for (const node of nodes) { const mesh = node.getMesh(); diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 31655e65c..bc84b5777 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -14,11 +14,14 @@ import {Document, Material, Node, WebIO} from '@gltf-transform/core'; import {KHRMaterialsUnlit, KHRONOS_EXTENSIONS} from '@gltf-transform/extensions'; +import {fileForContentTypes, to3dmodel} from '@jscadui/3mf-export'; +import {strToU8, Zippable, zipSync} from 'fflate' import * as glMatrix from 'gl-matrix'; import Module from './built/manifold'; //@ts-ignore import {setupIO, writeMesh} from './gltf-io'; + import type {GLTFMaterial, Quat} from './public/editor'; import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './public/manifold'; @@ -457,8 +460,28 @@ async function exportGLB(manifold?: Manifold) { wrapper.addChild(node); } + const results: {glbURL?: string, threeMFURL?: string} = {}; + if (manifold != null) { + const mesh = manifold.getMesh(); + let vertices = new Float32Array(mesh.numVert * 3); + for (let i = 0; i < mesh.numVert; ++i) { + for (let j = 0; j < 3; ++j) + vertices[i * 3 + j] = mesh.vertProperties[i * mesh.numProp + j]; + } + const model = + to3dmodel({simple: [{vertices, indices: mesh.triVerts, id: '1'}]}); + const files: Zippable = {}; + files['3D/3dmodel.model'] = strToU8(model); + files[fileForContentTypes.name] = strToU8(fileForContentTypes.content); + const zipFile = zipSync(files); + results.threeMFURL = URL.createObjectURL(new Blob( + [zipFile], + {type: 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml'})); + } + const glb = await io.writeBinary(doc); const blob = new Blob([glb], {type: 'application/octet-stream'}); - self.postMessage({objectURL: URL.createObjectURL(blob)}); + results.glbURL = URL.createObjectURL(blob); + self.postMessage(results); }