UNPKG

manifold-3d

Version:

Geometry library for topological robustness

216 lines 8.63 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. /** * Serialize in memory glTF-transform documents to 3MF. * @packageDocumentation * @group ManifoldCAD * @category Input/Output * @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 { fileForContentTypes, FileForRelThumbnail, to3dmodel } from '@jscadui/3mf-export'; import { strToU8, zipSync } from 'fflate'; import { ManifoldPrimitive } from "./manifold-gltf.js"; const supportedFormat = { extension: '3mf', mimetype: 'model/3mf' }; /** * @group Export */ export const exportFormats = [supportedFormat]; const defaultHeader = { unit: 'millimeter', title: 'ManifoldCAD.org model', description: 'ManifoldCAD.org model', application: 'ManifoldCAD.org', }; /** * Sort 3MF components topologically. * * Some 3MF parsers (like PrusaSlicer and descendants) expect child nodes to be * defined before their parents. This function sorts the component list * accordingly. * * This is a version of Kahn's algorithm -- a stripped down breadth first * search. It finds root nodes, adds them to the result, then removes them from * the graph. It moves on to their children, which are now root nodes * themselves. * * Rinse, lather, repeat, and the final result is a list of nodes ordered by * generation. Order within a generation is not guaranteed, and is not required * in this particular case. * * @hidden * Exported for unit tests. */ export const toposort = (unsorted) => { // Create a local graph. We will destroy it by removing edges and do not want // to introduce any side effects. let graph = []; for (const parent of unsorted) { for (const { objectID } of parent.children) { const child = unsorted.find(c => c.id === objectID); if (child) graph.push([parent, child]); } } const isRoot = (child) => graph.filter(([, c]) => c.id === child.id).length === 0; const children = (parent) => graph.filter(([p]) => p.id === parent.id).map(([, c]) => c); const disown = (parent, child) => graph = graph.filter(([p, c]) => !(p.id === parent.id && c.id === child.id)); const roots = unsorted.filter(isRoot); const sorted = []; let root; while (root = roots.shift()) { // For each root node... sorted.unshift(root); // ...insert before potential ancestors. for (const child of children(root)) { // For each child.... disown(root, child); // ...remove the parent-child edge from the graph. if (isRoot(child)) roots.push(child); // Enqueue new root nodes. } } return sorted; }; /** * Convert a GLTF-Transform document to a 3MF model. * * 3MF files are more sophisticated than STL files; they can encode meshes, * components and build items. * * 3MF components are like a scene graph. Each component can have multiple * children, and does have its own transformation matrix. * This is flexible enough to allow putting several parts in the same file * (multiple components, each with a mesh) as well as multi-material files (a * tree containing multiple meshes, each for a particular material). * * Finally, build items define what the slicer software actually sees. * ManifoldCAD doesn't have an equivalent comprehension. We assume that top * level objects -- nodes with no parents -- are build objects. * * @param doc The GLTF document to convert. * @returns A blob containing the converted model. * @group Export */ export async function toArrayBuffer(doc, options) { const to3mf = { meshes: [], components: [], items: [], precision: 7, header: { ...defaultHeader, ...(options?.header ?? {}) } }; // GLTF references by array index. // 3MF references by ID. let nextGlobalID = 1; const object2globalID = new Map(); const getObjectID = (obj) => `${object2globalID.get(obj)}`; const getMeshID = (mesh) => { // If a mesh has been cloned with a different material, find // the original mesh. This isn't a general GLTF feature; this is set // by the ManifoldCAD GLTF exporter. const { clonedFrom } = mesh.getExtras(); if (clonedFrom) { return object2globalID.get(clonedFrom); } return object2globalID.get(mesh); }; const setObjectID = (obj) => { const objectID = `${nextGlobalID++}`; object2globalID.set(obj, objectID); return objectID; }; // Get meshes in place first. for (const mesh of doc.getRoot().listMeshes()) { const manifoldPrimitive = mesh.getExtension('EXT_mesh_manifold'); if (manifoldPrimitive) { // This mesh has a list of triangle vertices already. const indices = manifoldPrimitive.getIndices(); const positionAccessor = mesh.listPrimitives()[0].getAttribute('POSITION'); const objectID = setObjectID(mesh); to3mf.meshes.push({ vertices: positionAccessor.getArray(), indices: indices.getArray(), id: objectID }); } const { clonedFrom } = mesh.getExtras(); if (!manifoldPrimitive && clonedFrom) { // GLTF Mesh, instance of another mesh. // getMeshID will find this when adding it to components. continue; } if (!manifoldPrimitive && !clonedFrom) { // GLTF Mesh, no manifold primitive, // not an instance of another mesh. // We should handle this case, but for now we do not. console.log('skipping non-ManifoldCAD mesh'); } } // Create our components... const components = []; for (const node of doc.getRoot().listNodes()) { const meshID = node.getMesh() && getMeshID(node.getMesh()); components.push({ id: setObjectID(node), name: node.getName(), children: meshID ? [{ objectID: meshID }] : [], transform: node.getMatrix().map(n => n.toFixed(to3mf.precision)) }); } // ...work out our component hierarchy... for (const node of doc.getRoot().listNodes()) { const objectID = getObjectID(node); if (!objectID) { console.log(`Could not find object ID for ${node.getName()}`); continue; } const child = { objectID, // Most 3MF parsers will not accept a number in scientific notation. // Transforms are serialized to a string, containing 12 numbers // separated by spaces. If we force a number to a string here, // 3mf-export passes it through. transform: node.getMatrix().map(n => n.toFixed(to3mf?.precision ?? 7)) }; const parent = node.getParentNode(); if (parent) { // This is a child node, add it to its parent. const parentID = getObjectID(parent); const parent3mf = components.find((comp) => comp.id == parentID); parent3mf.children.push(child); } else { // This is a root node. // Add it to the build list. to3mf.items.push({ objectID }); } } // ...and sort it so that parents always follow children. to3mf.components = toposort(components); const fileForRelThumbnail = new FileForRelThumbnail(); fileForRelThumbnail.add3dModel('3D/3dmodel.model'); const model = to3dmodel(to3mf); const files = {}; files['3D/3dmodel.model'] = strToU8(model); files[fileForContentTypes.name] = strToU8(fileForContentTypes.content); files[fileForRelThumbnail.name] = strToU8(fileForRelThumbnail.content); const zipFile = zipSync(files); return zipFile.buffer; } ; //# sourceMappingURL=export-3mf.js.map