UNPKG

manifold-3d

Version:

Geometry library for topological robustness

230 lines 7.89 kB
// Copyright 2026 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 3MF models into manifoldCAD. * * @packageDocumentation * @group ManifoldCAD * @category Input/Output */ import * as GLTFTransform from '@gltf-transform/core'; import { XMLParser } from 'fast-xml-parser'; import { unzipSync } from 'fflate'; import { euler2quat } from "./math.js"; export const importFormats = [{ extension: '3mf', mimetype: 'model/3mf' }]; /** * Parse a 3MF ArrayBuffer into a gltf-transform Document. * * Note: 3MF files store geometry in millimetres with +Z up, while glTF uses * metres with +Y up. This importer normalizes geometry to glTF conventions so * the shared import pipeline can apply `importTransform()` consistently across * all formats. */ export async function fromArrayBuffer(buffer) { const files = unzipSync(new Uint8Array(buffer)); const modelData = files['3D/3dmodel.model']; if (!modelData) { throw new Error('Invalid 3MF file: missing 3D/3dmodel.model'); } return parse3mfXml(new TextDecoder().decode(modelData)); } const identityMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; function asArray(value) { if (value == null) return []; return Array.isArray(value) ? value : [value]; } function toFiniteNumber(value) { const n = Number(value); return Number.isFinite(n) ? n : null; } function parseUnitMeters(unit) { switch ((unit ?? 'millimeter').toLowerCase()) { case 'micron': return 1e-6; case 'millimeter': return 1e-3; case 'centimeter': return 1e-2; case 'inch': return 0.0254; case 'foot': return 0.3048; case 'meter': return 1.0; default: return 1e-3; } } function parseTransform(value) { if (!value) return [...identityMatrix]; const nums = value.trim().split(/\s+/).map(Number); if (nums.length !== 12 || nums.some((n) => !Number.isFinite(n))) { return [...identityMatrix]; } // 3MF transform attribute stores affine matrix values as: // m00 m01 m02 m10 m11 m12 m20 m21 m22 m30 m31 m32. const m = [...identityMatrix]; m[0] = nums[0]; m[1] = nums[1]; m[2] = nums[2]; m[4] = nums[3]; m[5] = nums[4]; m[6] = nums[5]; m[8] = nums[6]; m[9] = nums[7]; m[10] = nums[8]; m[12] = nums[9]; m[13] = nums[10]; m[14] = nums[11]; return m; } function parseMesh(mesh, doc, buf, defaultMaterial) { if (!mesh) return null; // --- vertices --- const positions = []; for (const vertex of asArray(mesh.vertices?.vertex)) { const x = toFiniteNumber(vertex.x); const y = toFiniteNumber(vertex.y); const z = toFiniteNumber(vertex.z); if (x == null || y == null || z == null) continue; positions.push(x, y, z); } if (!positions.length) return null; // --- triangles --- const indices = []; for (const tri of asArray(mesh.triangles?.triangle)) { const v1 = toFiniteNumber(tri.v1); const v2 = toFiniteNumber(tri.v2); const v3 = toFiniteNumber(tri.v3); if (v1 == null || v2 == null || v3 == null) continue; indices.push(v1, v2, v3); } if (!indices.length) return null; const posAcc = doc.createAccessor() .setBuffer(buf) .setType(GLTFTransform.Accessor.Type.VEC3) .setArray(new Float32Array(positions)); const indAcc = doc.createAccessor() .setBuffer(buf) .setType(GLTFTransform.Accessor.Type.SCALAR) .setArray(new Uint32Array(indices)); const prim = doc.createPrimitive() .setAttribute('POSITION', posAcc) .setIndices(indAcc) .setMaterial(defaultMaterial); return doc.createMesh().addPrimitive(prim); } function parseComponentRefs(object) { const refs = []; for (const component of asArray(object.components?.component)) { const objectID = component.objectid?.toString(); if (!objectID) continue; refs.push({ objectID, transform: parseTransform(component.transform) }); } return refs; } function parseBuildRefs(model) { const refs = []; for (const item of asArray(model.build?.item)) { const objectID = item.objectid?.toString(); if (!objectID) continue; refs.push({ objectID, transform: parseTransform(item.transform) }); } return refs; } function instantiateObjectNode(doc, objects, ref, visiting) { const object = objects.get(ref.objectID); if (!object) return null; if (visiting.has(object.id)) return null; visiting.add(object.id); const node = doc.createNode(object.name); if (object.mesh) node.setMesh(object.mesh); node.setMatrix(ref.transform); for (const childRef of object.children) { const childNode = instantiateObjectNode(doc, objects, childRef, visiting); if (childNode) node.addChild(childNode); } visiting.delete(object.id); return node; } function parse3mfXml(xml) { const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', trimValues: true, parseTagValue: false, parseAttributeValue: false }); const parsed = parser.parse(xml); const model = parsed.model; if (!model) { throw new Error('Invalid 3MF file: missing <model> root'); } const doc = new GLTFTransform.Document(); const buf = doc.createBuffer(); // A default material is required so that gltfDocToManifold can process // the primitives (it calls getMaterial() on each primitive). const defaultMaterial = doc.createMaterial(); const scene = doc.createScene(); const unitToMeters = parseUnitMeters(model.unit); const objects = new Map(); // Keep vertex data untouched and normalize once at the root: // 3MF (unit/+Z-up) -> glTF (m/+Y-up). const rootTransform = doc.createNode('3mf-import-root'); rootTransform.setRotation(euler2quat([-90, 0, 0])); rootTransform.setScale([unitToMeters, unitToMeters, unitToMeters]); scene.addChild(rootTransform); // Parse all resource objects first (mesh objects and component objects). for (const object of asArray(model.resources?.object)) { const id = object.id?.toString(); if (!id) continue; objects.set(id, { id, name: object.name, mesh: parseMesh(object.mesh, doc, buf, defaultMaterial), children: parseComponentRefs(object) }); } // Build roots from the <build> list and instantiate object hierarchies. const buildRefs = parseBuildRefs(model); for (const ref of buildRefs) { const node = instantiateObjectNode(doc, objects, ref, new Set()); if (node) rootTransform.addChild(node); } // Fallback for malformed 3MF files missing <build>: show all mesh objects. if (!buildRefs.length) { for (const object of objects.values()) { if (!object.mesh) continue; rootTransform.addChild(doc.createNode(object.name).setMesh(object.mesh)); } } return doc; } //# sourceMappingURL=import-3mf.js.map