From 6b4a4ed841357f627573181402d3bb979092b5bb Mon Sep 17 00:00:00 2001 From: Emmett Lalish Date: Fri, 10 May 2024 16:43:57 -0700 Subject: [PATCH] Update examples from JS -> TS (#807) * put types back * updated MV example to TS * fixed TS errors * added docs * switch make-manifold to TS * fix TS errors * added docs * update MV * fix TS build * forgot declaration --- bindings/wasm/examples/gltf-io.ts | 20 +-- bindings/wasm/examples/index.html | 4 +- bindings/wasm/examples/make-manifold.html | 129 +-------------- bindings/wasm/examples/make-manifold.ts | 149 ++++++++++++++++++ bindings/wasm/examples/manifold-gltf.ts | 2 +- bindings/wasm/examples/model-viewer-script.ts | 124 +++++++++++++++ bindings/wasm/examples/model-viewer.html | 112 ++----------- bindings/wasm/examples/package-lock.json | 15 +- bindings/wasm/examples/package.json | 2 +- bindings/wasm/examples/simple-dropzone.d.ts | 1 + bindings/wasm/examples/three.ts | 4 +- bindings/wasm/examples/worker.ts | 5 +- 12 files changed, 311 insertions(+), 256 deletions(-) create mode 100644 bindings/wasm/examples/make-manifold.ts create mode 100644 bindings/wasm/examples/model-viewer-script.ts create mode 100644 bindings/wasm/examples/simple-dropzone.d.ts diff --git a/bindings/wasm/examples/gltf-io.ts b/bindings/wasm/examples/gltf-io.ts index 2a47e7e92..2323be05d 100644 --- a/bindings/wasm/examples/gltf-io.ts +++ b/bindings/wasm/examples/gltf-io.ts @@ -107,12 +107,12 @@ export function readMesh(mesh: Mesh, attributes: Attribute[] = []): const manifoldPrimitive = mesh.getExtension('EXT_mesh_manifold') as ManifoldPrimitive; - let vertPropArray: number[] = []; - let triVertArray: number[] = []; + let vertPropArray = Array(); + let triVertArray = Array(); const runIndexArray = [0]; - const mergeFromVertArray = []; - const mergeToVertArray = []; - const runProperties: Properties[] = []; + const mergeFromVertArray = Array(); + const mergeToVertArray = Array(); + const runProperties = Array(); if (manifoldPrimitive != null) { const numVert = primitives[0].getAttribute('POSITION')!.getCount(); const foundAttribute = attributes.map((a) => attributeDefs[a].type == null); @@ -150,7 +150,7 @@ export function readMesh(mesh: Mesh, attributes: Attribute[] = []): } const mergeTriVert = manifoldPrimitive.getMergeIndices()?.getArray() ?? []; const mergeTo = manifoldPrimitive.getMergeValues()?.getArray() ?? []; - const vert2merge = new Map(); + const vert2merge = new Map(); for (const [i, idx] of mergeTriVert.entries()) { vert2merge.set(triVertArray[idx], mergeTo[i]); } @@ -218,8 +218,8 @@ export function writeMesh( const manifoldExtension = doc.createExtension(EXTManifold); const mesh = doc.createMesh(); - const runIndex = []; - const attributeUnion: Attribute[] = []; + const runIndex = Array(); + const attributeUnion = Array(); const primitive2attributes = new Map(); const numRun = manifoldMesh.runIndex.length - 1; let lastID = -1; @@ -323,8 +323,8 @@ export function writeMesh( manifoldPrimitive.setRunIndex(runIndex); const vert2merge = [...Array(manifoldMesh.numVert).keys()]; - const ind = []; - const val = []; + const ind = Array(); + const val = Array(); if (manifoldMesh.mergeFromVert && manifoldMesh.mergeToVert) { for (const [i, from] of manifoldMesh.mergeFromVert.entries()) { vert2merge[from] = manifoldMesh.mergeToVert[i]; diff --git a/bindings/wasm/examples/index.html b/bindings/wasm/examples/index.html index 87b3434c3..18da8e730 100644 --- a/bindings/wasm/examples/index.html +++ b/bindings/wasm/examples/index.html @@ -73,7 +73,7 @@
+ shadow-intensity="1" tone-mapping="neutral" interaction-prompt="none" alt="Editor 3D output">

Loading...

@@ -95,6 +95,6 @@ integrity="sha512-iQEIc0rsSDujsfjtD+lfyJ1W23Bh/lbgriubKDAym6VlEIDRj9rrbSIyJRyshOrl8s0yRcQ0+gyrZfSLyjJGWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"> - + \ No newline at end of file diff --git a/bindings/wasm/examples/make-manifold.html b/bindings/wasm/examples/make-manifold.html index e9856bde7..47bc5664b 100644 --- a/bindings/wasm/examples/make-manifold.html +++ b/bindings/wasm/examples/make-manifold.html @@ -55,134 +55,11 @@ - + Drop a GLB here - - + + \ No newline at end of file diff --git a/bindings/wasm/examples/make-manifold.ts b/bindings/wasm/examples/make-manifold.ts new file mode 100644 index 000000000..66588e88b --- /dev/null +++ b/bindings/wasm/examples/make-manifold.ts @@ -0,0 +1,149 @@ +// Copyright 2024 The Manifold Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {Document, WebIO} from '@gltf-transform/core'; +import {KHRONOS_EXTENSIONS} from '@gltf-transform/extensions'; +import {prune} from '@gltf-transform/functions'; +import {SimpleDropzone} from 'simple-dropzone'; + +import Module from './built/manifold.js'; +import {disposeMesh, Properties, readMesh, setupIO, writeMesh} from './gltf-io'; + +// Set up gltf-transform +const io = setupIO(new WebIO()); +io.registerExtensions(KHRONOS_EXTENSIONS); + +// Set up Manifold WASM library +const wasm = await Module(); +wasm.setup(); +const {Manifold, Mesh} = wasm; + +// UX elements +const mv = document.querySelector('model-viewer'); +const inputEl = document.querySelector('#input') as HTMLInputElement; +const downloadButton = document.querySelector('#download') as HTMLButtonElement; +const checkbox = document.querySelector('#viewFinal') as HTMLInputElement; +const dropCtrl = new SimpleDropzone(mv, inputEl) as any; + +// glTF objects in memory +let inputGLBurl = ''; +let outputGLBurl = ''; +// Status of manifoldness of all meshes +let allManifold = true; +let anyManifold = false; + +// The processing is run when a glTF is drag-and-dropped onto this element. +dropCtrl.on('drop', async ({files}: {files: Map}) => { + for (const [_path, file] of files) { + const filename = file.name.toLowerCase(); + if (filename.match(/\.(gltf|glb)$/)) { + URL.revokeObjectURL(inputGLBurl); + inputGLBurl = URL.createObjectURL(file); + await writeGLB(await readGLB(inputGLBurl)); + updateUI(); + break; + } + } +}); + +// UI functions + +function updateUI() { + if (allManifold) { + checkbox.checked = true; + checkbox.disabled = true; + } else if (anyManifold) { + checkbox.checked = false; + checkbox.disabled = false; + } else { + checkbox.checked = false; + checkbox.disabled = true; + } + onClick(); +} + +checkbox.onclick = onClick; + +function onClick() { + (mv as any).src = checkbox.checked ? outputGLBurl : inputGLBurl; + downloadButton.disabled = !checkbox.checked; +}; + +downloadButton.onclick = () => { + const link = document.createElement('a'); + link.download = 'manifold.glb'; + link.href = outputGLBurl; + link.click(); +}; + +// Write output glTF using gltf-transform, which contains only the meshes that +// are manifold, and using the EXT_mesh_manifold extension. +async function writeGLB(doc: Document): Promise { + URL.revokeObjectURL(outputGLBurl); + if (!anyManifold) { + return; + } + const glb = await io.writeBinary(doc); + + const blob = new Blob([glb], {type: 'application/octet-stream'}); + outputGLBurl = URL.createObjectURL(blob); +} + +// Read the glTF ObjectURL and return a gltf-transform document with all the +// non-manifold meshes stripped out. +async function readGLB(url: string): Promise { + allManifold = false; + anyManifold = false; + updateUI(); + allManifold = true; + const docIn = await io.read(url); + const nodes = docIn.getRoot().listNodes(); + for (const node of nodes) { + const mesh = node.getMesh(); + if (!mesh) continue; + + const tmp = readMesh(mesh); + if (!tmp) continue; + + const id2properties = new Map(); + const numID = tmp.runProperties.length; + const firstID = Manifold.reserveIDs(numID); + tmp.mesh.runOriginalID = new Uint32Array(numID); + for (let i = 0; i < numID; ++i) { + tmp.mesh.runOriginalID[i] = firstID + i; + id2properties.set(firstID + i, tmp.runProperties[i]); + } + const manifoldMesh = new Mesh(tmp.mesh); + disposeMesh(mesh); + // Make the mesh manifold if it's close. + manifoldMesh.merge(); + + try { + // Test manifoldness - will throw if not. + const manifold = new Manifold(manifoldMesh); + // Replace the mesh with a manifold version + node.setMesh(writeMesh(docIn, manifold.getMesh(), id2properties)); + manifold.delete(); + anyManifold = true; + } catch (e) { + console.log(mesh.getName(), e); + allManifold = false; + } + } + + // Prune the leftovers after non-manifold mesh removal. + await docIn.transform(prune()); + + return docIn; +} \ No newline at end of file diff --git a/bindings/wasm/examples/manifold-gltf.ts b/bindings/wasm/examples/manifold-gltf.ts index 079d2c5cb..26cb10749 100644 --- a/bindings/wasm/examples/manifold-gltf.ts +++ b/bindings/wasm/examples/manifold-gltf.ts @@ -104,7 +104,7 @@ export class EXTManifold extends Extension { if (manifoldDef.manifoldPrimitive) { let count = 0; - const runIndex = []; + const runIndex = Array(); runIndex.push(count); for (const primitive of mesh.listPrimitives()) { const indices = primitive.getIndices(); diff --git a/bindings/wasm/examples/model-viewer-script.ts b/bindings/wasm/examples/model-viewer-script.ts new file mode 100644 index 000000000..3dd17be1a --- /dev/null +++ b/bindings/wasm/examples/model-viewer-script.ts @@ -0,0 +1,124 @@ +// Copyright 2024 The Manifold Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {Document, WebIO} from '@gltf-transform/core'; +import {clearNodeTransform, flatten, prune} from '@gltf-transform/functions'; + +import Module, {Manifold, Mesh} from './built/manifold'; +import {disposeMesh, Properties, readMesh, setupIO, writeMesh} from './gltf-io'; + +// Set up gltf-transform +const io = setupIO(new WebIO()); +const doc = new Document(); + + +// Set up Manifold WASM library +const wasm = await Module(); +wasm.setup(); +const {Manifold, Mesh} = wasm; + +// Map of OriginalID to glTF material and attributes +const id2properties = new Map(); + +// Wrapper for gltf-io readMesh() that processes a whole glTF and stores the +// properties in the above map. This function is simplified and intended only +// for reading single-object glTFs, as it simply unions any extra meshes +// together rather than returning a scene hierarchy. +async function readGLB(url: string) { + const manifolds = Array(); + const docIn = await io.read(url); + await docIn.transform(flatten()); + const nodes = docIn.getRoot().listNodes(); + const ids = Array(); + for (const node of nodes) { + clearNodeTransform(node); + const gltfMesh = node.getMesh(); + if (gltfMesh == null) continue; + const tmp = readMesh(gltfMesh); + if (tmp == null) continue; + + const numID = tmp.runProperties.length; + const firstID = Manifold.reserveIDs(numID); + tmp.mesh.runOriginalID = new Uint32Array(numID); + for (let i = 0; i < numID; ++i) { + tmp.mesh.runOriginalID[i] = firstID + i; + ids.push(firstID + i); + id2properties.set(firstID + i, tmp.runProperties[i]); + } + manifolds.push(new Manifold(new Mesh(tmp.mesh))); + } + // pull in materials, TODO: replace with transfer() when available + const startIdx = doc.getRoot().listMaterials().length; + doc.merge(docIn); + doc.getRoot().listScenes().forEach((s) => s.dispose()); + doc.getRoot().listBuffers().forEach((s) => s.dispose()); + doc.getRoot().listAccessors().forEach((s) => s.dispose()); + for (const [i, id] of ids.entries()) { + const material = doc.getRoot().listMaterials()[startIdx + i]; + id2properties.get(id)!.material = material; + } + + return Manifold.union(manifolds); +} + +// Read static input glTFs +const space = await readGLB('/models/space.glb'); +const moon = await readGLB('/models/moon.glb'); + +const node = doc.createNode(); +doc.createScene().addChild(node); + +// Set up UI for operations +type BooleanOp = 'union'|'difference'|'intersection'; + +function csg(operation: BooleanOp) { + push2MV(Manifold[operation](space, moon)); +} + +csg('difference'); +const selectElement = document.querySelector('select')!; +selectElement.onchange = function() { + csg(selectElement.value as BooleanOp); +}; + +// The resulting glTF +let objectURL = ''; + +// Set up download UI +const downloadButton = document.querySelector('#download') as HTMLButtonElement; +downloadButton.onclick = function() { + const link = document.createElement('a'); + link.download = 'manifold.glb'; + link.href = objectURL; + link.click(); +}; + +// element for rendering resulting glTF. +const mv = document.querySelector('model-viewer'); + +// Use gltf-io and gltf-transform to convert the resulting Manifold to a glTF +// and display it with . +async function push2MV(manifold: Manifold) { + disposeMesh(node.getMesh()!); + const mesh = writeMesh(doc, manifold.getMesh(), id2properties); + node.setMesh(mesh); + await doc.transform(prune()); + + const glb = await io.writeBinary(doc); + + const blob = new Blob([glb], {type: 'application/octet-stream'}); + URL.revokeObjectURL(objectURL); + objectURL = URL.createObjectURL(blob); + (mv as any).src = objectURL; +} \ No newline at end of file diff --git a/bindings/wasm/examples/model-viewer.html b/bindings/wasm/examples/model-viewer.html index c01826a95..263bb0d9e 100644 --- a/bindings/wasm/examples/model-viewer.html +++ b/bindings/wasm/examples/model-viewer.html @@ -35,114 +35,24 @@ -

This example demonstrates feeding Manifold's output into <model-viewer> via gltf-transform. It also shows how to pass mesh - properties like UV coordinates through Manifold and how to re-associate textures after operations. A more basic - three.js example is here.

+

This example demonstrates reading and writing glTF models using gltf-transform and using this to feed Manifold's + output into <model-viewer>. It also shows how to pass mesh properties + like UV coordinates through Manifold and how to re-associate textures and materials after operations, made simple + using our `gltf-io.ts` library. The resulting glTF includes the EXT_mesh_manifold extension, using our + `manifold-gltf.ts` extension to gltf-transform, which ensures lossless data transfer.

+

Please open dev tools to inspect our source code - everything is source mapped back to our TS files and commented + thoroughly. A more basic three.js example is here.

- - - + + \ No newline at end of file diff --git a/bindings/wasm/examples/package-lock.json b/bindings/wasm/examples/package-lock.json index 3b318358b..d00c0e5de 100644 --- a/bindings/wasm/examples/package-lock.json +++ b/bindings/wasm/examples/package-lock.json @@ -12,13 +12,13 @@ "@gltf-transform/extensions": "^3.8.0", "@gltf-transform/functions": "^3.8.0", "@jscadui/3mf-export": "^0.5.0", + "@types/three": "^0.164.0", "fflate": "^0.8.0", "gl-matrix": "^3.4.3", "simple-dropzone": "0.8.3", "three": "0.164.1" }, "devDependencies": { - "@types/three": "^0.164.0", "@vitest/ui": "^0.31.1", "@vitest/web-worker": "^0.31.1", "typescript": "5.2.2", @@ -472,8 +472,7 @@ "node_modules/@tweenjs/tween.js": { "version": "23.1.2", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz", - "integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==", - "dev": true + "integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==" }, "node_modules/@types/chai": { "version": "4.3.16", @@ -507,14 +506,12 @@ "node_modules/@types/stats.js": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", - "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==", - "dev": true + "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==" }, "node_modules/@types/three": { "version": "0.164.0", "resolved": "https://registry.npmjs.org/@types/three/-/three-0.164.0.tgz", "integrity": "sha512-SFDofn9dJVrE+1DKta7xj7lc4ru7B3S3yf10NsxOserW57aQlB6GxtAS1UK5To3LfEMN5HUHMu3n5v+M5rApgA==", - "dev": true, "dependencies": { "@tweenjs/tween.js": "~23.1.1", "@types/stats.js": "*", @@ -526,8 +523,7 @@ "node_modules/@types/webxr": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.16.tgz", - "integrity": "sha512-0E0Cl84FECtzrB4qG19TNTqpunw0F1YF0QZZnFMF6pDw1kNKJtrlTKlVB34stGIsHbZsYQ7H0tNjPfZftkHHoA==", - "dev": true + "integrity": "sha512-0E0Cl84FECtzrB4qG19TNTqpunw0F1YF0QZZnFMF6pDw1kNKJtrlTKlVB34stGIsHbZsYQ7H0tNjPfZftkHHoA==" }, "node_modules/@vitest/expect": { "version": "0.31.4", @@ -1287,8 +1283,7 @@ "node_modules/meshoptimizer": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", - "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", - "dev": true + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==" }, "node_modules/micromatch": { "version": "4.0.5", diff --git a/bindings/wasm/examples/package.json b/bindings/wasm/examples/package.json index b32484e2c..746ca05b7 100644 --- a/bindings/wasm/examples/package.json +++ b/bindings/wasm/examples/package.json @@ -17,6 +17,7 @@ "@gltf-transform/extensions": "^3.8.0", "@gltf-transform/functions": "^3.8.0", "@jscadui/3mf-export": "^0.5.0", + "@types/three": "^0.164.0", "fflate": "^0.8.0", "gl-matrix": "^3.4.3", "simple-dropzone": "0.8.3", @@ -25,7 +26,6 @@ "devDependencies": { "@vitest/ui": "^0.31.1", "@vitest/web-worker": "^0.31.1", - "@types/three": "^0.164.0", "typescript": "5.2.2", "vite": "^4.5.0", "vitest": "^0.31.1" diff --git a/bindings/wasm/examples/simple-dropzone.d.ts b/bindings/wasm/examples/simple-dropzone.d.ts new file mode 100644 index 000000000..f62a23b1f --- /dev/null +++ b/bindings/wasm/examples/simple-dropzone.d.ts @@ -0,0 +1 @@ +declare module 'simple-dropzone'; \ No newline at end of file diff --git a/bindings/wasm/examples/three.ts b/bindings/wasm/examples/three.ts index e2b801675..d260ef6d0 100644 --- a/bindings/wasm/examples/three.ts +++ b/bindings/wasm/examples/three.ts @@ -14,7 +14,7 @@ import {BoxGeometry, BufferAttribute, BufferGeometry, IcosahedronGeometry, Mesh as ThreeMesh, MeshLambertMaterial, MeshNormalMaterial, PerspectiveCamera, PointLight, Scene, WebGLRenderer} from 'three'; -import Module, {Mesh} from './built/manifold.js'; +import Module, {Mesh} from './built/manifold'; // Load Manifold WASM library const wasm = await Module(); @@ -82,7 +82,7 @@ function csg(operation: BooleanOp) { } csg('union'); -const selectElement = document.querySelector('select') as HTMLSelectElement; +const selectElement = document.querySelector('select')!; selectElement.onchange = function() { csg(selectElement.value as BooleanOp); }; diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 12c683cdf..8d2ab6105 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -18,10 +18,9 @@ import {fileForContentTypes, FileForRelThumbnail, to3dmodel} from '@jscadui/3mf- import {strToU8, Zippable, zipSync} from 'fflate' import * as glMatrix from 'gl-matrix'; -import Module from './built/manifold'; +import Module, {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './built/manifold'; import {Properties, setupIO, writeMesh} from './gltf-io'; import {GLTFMaterial, Quat} from './public/editor'; -import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './public/manifold'; interface GlobalDefaults { roughness: number; @@ -788,7 +787,7 @@ async function exportModels(defaults: GlobalDefaults, manifold?: Manifold) { const fileForRelThumbnail = new FileForRelThumbnail(); fileForRelThumbnail.add3dModel('3D/3dmodel.model') - const model = to3dmodel(to3mf); + const model = to3dmodel(to3mf as any); const files: Zippable = {}; files['3D/3dmodel.model'] = strToU8(model); files[fileForContentTypes.name] = strToU8(fileForContentTypes.content);