UNPKG

manifold-3d

Version:

Geometry library for topological robustness

460 lines 19 kB
// Copyright 2023-2025 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. /** * Convert between in-memory glTF-transform documents and their serialized * formats. This module also includes some utilities for conversion between glTF * meshes and manifold meshes. * * @packageDocumentation * @group ManifoldCAD * @category Input/Output * @groupDescription Import * These properties implement the {@link lib/import-model!Importer | Importer} * interface. Through this interface, manifoldCAD can determine when to use this * module to import a model. * @groupDescription Export * These properties implement the {@link lib/export-model!Exporter | Exporter} * interface. Through this interface, manifoldCAD can determine when to use this * module to export a model. */ import * as GLTFTransform from '@gltf-transform/core'; import { KHRONOS_EXTENSIONS } from '@gltf-transform/extensions'; import { EXTManifold, ManifoldPrimitive } from "./manifold-gltf.js"; import { fetchWithRetry } from "./util.js"; const binaryFormat = { extension: 'glb', mimetype: 'model/gltf-binary' }; /** * @group Import * @readonly */ export const importFormats = [binaryFormat]; /** * @group Export * @readonly */ export const exportFormats = [binaryFormat]; export const attributeDefs = { 'POSITION': { type: GLTFTransform.Accessor.Type.VEC3, components: 3 }, 'NORMAL': { type: GLTFTransform.Accessor.Type.VEC3, components: 3 }, 'TANGENT': { type: GLTFTransform.Accessor.Type.VEC4, components: 4 }, 'TEXCOORD_0': { type: GLTFTransform.Accessor.Type.VEC2, components: 2 }, 'TEXCOORD_1': { type: GLTFTransform.Accessor.Type.VEC2, components: 2 }, 'COLOR_0': { type: GLTFTransform.Accessor.Type.VEC3, components: 3 }, 'JOINTS_0': { type: GLTFTransform.Accessor.Type.VEC4, components: 4 }, 'WEIGHTS_0': { type: GLTFTransform.Accessor.Type.VEC4, components: 4 }, 'SKIP_1': { type: null, components: 1 }, 'SKIP_2': { type: null, components: 2 }, 'SKIP_3': { type: null, components: 3 }, 'SKIP_4': { type: null, components: 4 }, }; /** * Call this first to register the manifold extension so that readMesh and * writeMesh will work. */ export function setupIO(io) { return io.registerExtensions([EXTManifold]); } /** * Read an input mesh into Manifold-compatible data structures, whether it * contains the EXT_mesh_manifold extension or not. * * @param mesh The Mesh to read. * @param attributes An array of attributes representing the order of desired * properties returned in the vertProperties array of the output mesh. If * omitted, this will be populated with the union of all attributes defined * in the primitives of the input mesh. If present, the first entry must be * 'POSITION', and any attributes in the primitives that are not included in * this list will be ignored, while those in the list but not defined in a * primitive will be populated with zeros. * @returns The returned mesh is suitable for initializing a Manifold or Mesh of * the Manifold library if desired. See Manifold documentation if you prefer * to use these GL arrays in a different library. The runProperties array * gives the Material and attributes list associated with each triangle run, * which in turn corresponds to a primitive of the input mesh. These * attributes are the intersection of the attributes present on the * primitive and those requested in the attributes input. */ export function readMesh(mesh, attributes = []) { const primitives = mesh.listPrimitives(); if (primitives.length === 0) { return null; } if (attributes.length === 0) { const attributeSet = new Set(); for (const primitive of primitives) { const semantics = primitive.listSemantics(); for (const semantic of semantics) { attributeSet.add(semantic); } } let semantic; for (semantic in attributeDefs) { if (attributeSet.has(semantic)) { attributes.push(semantic); attributeSet.delete(semantic); } } for (const semantic of attributeSet.keys()) { attributes.push(semantic); } } if (attributes.length < 1 || attributes[0] !== 'POSITION') throw new Error('First attribute must be "POSITION".'); let numProp = 0; const attributeOffsets = attributes.map((numProp = 0, def => { const last = numProp; numProp += attributeDefs[def].components; return last; })); const manifoldPrimitive = mesh.getExtension('EXT_mesh_manifold'); let vertPropArray = Array(); let triVertArray = Array(); const runIndexArray = [0]; 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); vertPropArray = new Array(numProp * numVert); for (const primitive of primitives) { const indices = primitive.getIndices(); if (!indices) { console.log('Skipping non-indexed primitive ', primitive.getName()); continue; } const attributesIn = primitive.listSemantics(); attributes.forEach((attributeOut, idx) => { if (foundAttribute[idx]) { return; } for (const attributeIn of attributesIn) { if (attributeIn === attributeOut) { foundAttribute[idx] = true; const accessor = primitive.getAttribute(attributeIn); writeProperties(vertPropArray, accessor, numProp, attributeOffsets[idx]); } } }); triVertArray = [...triVertArray, ...indices.getArray()]; runIndexArray.push(triVertArray.length); runProperties.push({ material: primitive.getMaterial(), attributes: attributesIn.filter(b => attributes.some(a => a == b)) }); } const mergeTriVert = manifoldPrimitive.getMergeIndices()?.getArray() ?? []; const mergeTo = manifoldPrimitive.getMergeValues()?.getArray() ?? []; const vert2merge = new Map(); for (const [i, idx] of mergeTriVert.entries()) { vert2merge.set(triVertArray[idx], mergeTo[i]); } for (const [from, to] of vert2merge.entries()) { mergeFromVertArray.push(from); mergeToVertArray.push(to); } } else { for (const primitive of primitives) { const indices = primitive.getIndices(); if (!indices) { console.log('Skipping non-indexed primitive ', primitive.getName()); continue; } const numVert = vertPropArray.length / numProp; vertPropArray = [...vertPropArray, ...readPrimitive(primitive, numProp, attributes)]; triVertArray = [...triVertArray, ...indices.getArray().map((i) => i + numVert)]; runIndexArray.push(triVertArray.length); const attributesIn = primitive.listSemantics(); runProperties.push({ material: primitive.getMaterial(), attributes: attributesIn.filter(b => attributes.some(a => a == b)) }); } } const vertProperties = new Float32Array(vertPropArray); const triVerts = new Uint32Array(triVertArray); const runIndex = new Uint32Array(runIndexArray); const mergeFromVert = new Uint32Array(mergeFromVertArray); const mergeToVert = new Uint32Array(mergeToVertArray); const meshOut = { numProp, triVerts, vertProperties, runIndex, mergeFromVert, mergeToVert }; return { mesh: meshOut, runProperties }; } /** * Write a Manifold Mesh into a glTF Mesh object, using the EXT_mesh_manifold * extension to allow for lossless roundtrip of the manifold mesh through the * glTF file. * * @param doc The glTF Document to which this Mesh will be added. * @param manifoldMesh The Manifold Mesh to convert to glTF. * @param id2properties A map from originalID to Properties that include the * glTF Material and the set of attributes to output. All triangle runs with * the same originalID will be combined into a single output primitive. Any * originalIDs not found in the map will have the glTF default material and * no attributes beyond 'POSITION'. Each attributes array must correspond to * the manifoldMesh vertProperties, thus the first attribute must always be * 'POSITION'. Any properties that should not be output for a given * primitive must use the 'SKIP_*' attributes. * @param EXT_mesh_manifold If false, emit a plain glTF mesh. In this case, * the mesh is not required to be fully manifold when written. Use this * to write explicitly non-manifold meshes, e.g.: CrossSections. * @returns The glTF Mesh to add to the Document. */ export function writeMesh(doc, manifoldMesh, id2properties, EXT_mesh_manifold = true) { if (doc.getRoot().listBuffers().length === 0) { doc.createBuffer(); } const mesh = doc.createMesh(); writePrimitiveAttributes(doc, mesh, manifoldMesh, id2properties); if (EXT_mesh_manifold) { writeExtMeshManifoldIndices(doc, mesh, manifoldMesh); } else { writePlainIndices(doc, mesh, manifoldMesh); } return mesh; } /** * Create the necessary primitives and their attributes needed to represent a * ManifoldMesh object as a glTF mesh, and add those to an existing glTF * transform mesh node. * * This does not create or populate indices. After this call these primitives * will exist and have positions, but will not have visible geometry. */ function writePrimitiveAttributes(doc, mesh, manifoldMesh, id2properties) { const attributeUnion = Array(); const primitive2attributes = new Map(); // For each run, create a primitive, set material and collate attributes. const buffer = doc.getRoot().listBuffers()[0]; for (let run = 0; run < (manifoldMesh.runIndex.length - 1); run++) { const id = manifoldMesh.runOriginalID[run]; const primitive = doc.createPrimitive(); const properties = id2properties.get(id); if (properties) { const { material, attributes } = properties; if (attributes.length < 1 || attributes[0] !== 'POSITION') throw new Error('First attribute must be "POSITION".'); primitive.setMaterial(material); primitive2attributes.set(primitive, attributes); properties.attributes.forEach((attribute, i) => { if (i >= attributeUnion.length) { attributeUnion.push(attribute); } else { const size = attributeDefs[attribute].components; const unionSize = attributeDefs[attributeUnion[i]].components; if (size != unionSize) { throw new Error('Attribute sizes do not correspond: ' + attribute + ' and ' + attributeUnion[i]); } if (attributeDefs[attributeUnion[i]].type == null) { attributeUnion[i] = attribute; } } }); } else { primitive2attributes.set(primitive, ['POSITION']); } mesh.addPrimitive(primitive); } // For each primitive, create accessors for each attribute and populate those // attributes. const numVert = manifoldMesh.numVert; const numProp = manifoldMesh.numProp; let offset = 0; attributeUnion.forEach((attribute, aIdx) => { const def = attributeDefs[attribute]; if (def == null) throw new Error(attribute + ' is not a recognized attribute.'); if (def.type == null) { ++offset; return; } const n = def.components; if (offset + n > numProp) throw new Error('Too many attribute channels.'); const array = new Float32Array(n * numVert); for (let v = 0; v < numVert; ++v) { for (let i = 0; i < n; ++i) { let x = manifoldMesh.vertProperties[numProp * v + offset + i]; if (attribute == 'COLOR_0') { x = Math.max(0, Math.min(1, x)); } array[n * v + i] = x; } } const accessor = doc.createAccessor(attribute) .setBuffer(buffer) .setType(def.type) .setArray(array); for (const primitive of mesh.listPrimitives()) { const attributes = primitive2attributes.get(primitive); if (attributes.length > aIdx && attributeDefs[attributes[aIdx]].type != null) { primitive.setAttribute(attribute, accessor); } } offset += n; }); } function writeExtMeshManifoldIndices(doc, mesh, manifoldMesh) { const manifoldExtension = doc.createExtension(EXTManifold); const manifoldPrimitive = manifoldExtension.createManifoldPrimitive(); mesh.setExtension('EXT_mesh_manifold', manifoldPrimitive); const buffer = doc.getRoot().listBuffers()[0]; const { runIndex } = manifoldMesh; mesh.listPrimitives().forEach((primitive, n) => { // These indices will be populated by `manifold-gtlf` when the // document is written out by glTF transform. const indices = doc.createAccessor('primitive indices of ID ' + runIndex[n]) .setBuffer(buffer) .setType(GLTFTransform.Accessor.Type.SCALAR) .setArray(new Uint32Array(1)); primitive.setIndices(indices); }); const indices = doc.createAccessor('manifold indices') .setBuffer(buffer) .setType(GLTFTransform.Accessor.Type.SCALAR) .setArray(manifoldMesh.triVerts); manifoldPrimitive.setIndices(indices); manifoldPrimitive.setRunIndex(runIndex); const vert2merge = [...Array(manifoldMesh.numVert).keys()]; const ind = Array(); const val = Array(); if (manifoldMesh.mergeFromVert && manifoldMesh.mergeToVert) { for (const [i, from] of manifoldMesh.mergeFromVert.entries()) { vert2merge[from] = manifoldMesh.mergeToVert[i]; } for (const [i, vert] of manifoldMesh.triVerts.entries()) { const newVert = vert2merge[vert]; if (vert !== newVert) { ind.push(i); val.push(newVert); } } } if (ind.length > 0) { const indicesAccessor = doc.createAccessor('merge from') .setBuffer(buffer) .setType(GLTFTransform.Accessor.Type.SCALAR) .setArray(new Uint32Array(ind)); const valuesAccessor = doc.createAccessor('merge to') .setBuffer(buffer) .setType(GLTFTransform.Accessor.Type.SCALAR) .setArray(new Uint32Array(val)); manifoldPrimitive.setMerge(indicesAccessor, valuesAccessor); } } function writePlainIndices(doc, mesh, manifoldMesh) { const buffer = doc.getRoot().listBuffers()[0]; const { runIndex } = manifoldMesh; mesh.listPrimitives().forEach((primitive, n) => { const indices = doc.createAccessor('primitive indices of ID ' + runIndex[n]) .setBuffer(buffer) .setType(GLTFTransform.Accessor.Type.SCALAR) .setArray(manifoldMesh.triVerts.slice(runIndex[n], runIndex[n + 1])); primitive.setIndices(indices); }); } /** * Helper function to dispose of a Mesh, useful when replacing an existing Mesh * with one from writeMesh. */ export function disposeMesh(mesh) { if (!mesh) return; const primitives = mesh.listPrimitives(); for (const primitive of primitives) { primitive.getIndices()?.dispose(); for (const accessor of primitive.listAttributes()) { accessor.dispose(); } } const manifoldPrimitive = mesh.getExtension('EXT_mesh_manifold'); if (manifoldPrimitive) { manifoldPrimitive.getIndices()?.dispose(); manifoldPrimitive.getMergeIndices()?.dispose(); manifoldPrimitive.getMergeValues()?.dispose(); } mesh.dispose(); } /** * Helper function to download an image and apply it to the given texture. * * @param texture The texture to update * @param uri The location of the image to download */ export async function loadTexture(texture, uri) { const response = await fetchWithRetry(uri); const blob = await response.blob(); texture.setMimeType(blob.type); texture.setImage(new Uint8Array(await blob.arrayBuffer())); } function writeProperties(vertProperties, accessor, numProp, offset) { const array = accessor.getArray(); const size = accessor.getElementSize(); const numVert = accessor.getCount(); for (let i = 0; i < numVert; ++i) { for (let j = 0; j < size; ++j) { vertProperties[numProp * i + offset + j] = array[i * size + j]; } } } function readPrimitive(primitive, numProp, attributes) { const vertProperties = []; let offset = 0; for (const attribute of attributes) { const size = attributeDefs[attribute].components; if (attributeDefs[attribute].type == null) { offset += size; continue; } const accessor = primitive.getAttribute(attribute); if (accessor) { writeProperties(vertProperties, accessor, numProp, offset); } offset += size; } return vertProperties; } let _io = null; /** * Return an appropriate PlatformIO object. */ const getIO = () => { if (!_io) { _io = new GLTFTransform.WebIO(); _io.registerExtensions([EXTManifold, ...KHRONOS_EXTENSIONS]); } return _io; }; /** * @group Export */ export async function toArrayBuffer(doc) { return (await getIO().writeBinary(doc)).buffer; } /** * @group Import */ export async function fromArrayBuffer(buffer) { return await getIO().readBinary(new Uint8Array(buffer)); } //# sourceMappingURL=gltf-io.js.map