From f2f5081669bc70ae1c47cbc20a2ad523a5a81ae8 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 1 Jan 2026 13:06:44 -0500 Subject: [PATCH] add mesh:tool 3mf object output --- src/load/3mf.js | 130 ++++++++++++++++++++++++++++++++++++++++++++++- src/mesh/api.js | 7 +-- src/mesh/work.js | 5 ++ 3 files changed, 138 insertions(+), 4 deletions(-) diff --git a/src/load/3mf.js b/src/load/3mf.js index bf392988..3a7554ac 100644 --- a/src/load/3mf.js +++ b/src/load/3mf.js @@ -212,6 +212,133 @@ function extractItems(records) { return outItems; } +/** + * Export vertex array(s) to 3MF format + * @param {Array} recs - array of {file: string, varr: Float32Array} records + * @param {Object} options - encoding options + * @param {String} [options.unit='millimeter'] - unit of measurement + * @param {String} [options.title='Model'] - model title + * @returns {Promise} 3MF file as a Blob + */ +export async function encode(recs, options = {}) { + const { + unit = 'millimeter', + title = 'Model' + } = options; + + // Build the 3D model XML + let objectId = 1; + let resources = []; + let buildItems = []; + + for (let rec of recs) { + let { file, varr } = rec; + let name = file || `object-${objectId}`; + + // Extract unique vertices and build triangle indices + let vertices = []; + let triangles = []; + let vertexMap = new Map(); // map "x,y,z" -> vertex index + let vertexIndex = 0; + + // Process triangles (every 9 floats = 3 vertices = 1 triangle) + for (let i = 0; i < varr.length; i += 9) { + let triIndices = []; + + // Process 3 vertices per triangle + for (let j = 0; j < 3; j++) { + let vi = i + j * 3; + let x = varr[vi]; + let y = varr[vi + 1]; + let z = varr[vi + 2]; + + // Create vertex key for deduplication + let key = `${x},${y},${z}`; + let idx = vertexMap.get(key); + + if (idx === undefined) { + idx = vertexIndex++; + vertexMap.set(key, idx); + vertices.push({ x, y, z }); + } + + triIndices.push(idx); + } + + triangles.push(triIndices); + } + + // Generate object XML + let objectXml = [` `]; + objectXml.push(' '); + objectXml.push(' '); + + for (let v of vertices) { + objectXml.push(` `); + } + + objectXml.push(' '); + objectXml.push(' '); + + for (let tri of triangles) { + objectXml.push(` `); + } + + objectXml.push(' '); + objectXml.push(' '); + objectXml.push(' '); + + resources.push(objectXml.join('\n')); + buildItems.push(` `); + + objectId++; + } + + // Construct the complete 3D model XML + let modelXml = [ + '', + ``, + ' ', + resources.join('\n'), + ' ', + ' ', + buildItems.join('\n'), + ' ', + '' + ].join('\n'); + + // Create [Content_Types].xml + let contentTypes = [ + '', + '', + ' ', + ' ', + '' + ].join('\n'); + + // Create _rels/.rels + let rels = [ + '', + '', + ' ', + '' + ].join('\n'); + + // Create ZIP archive + let zip = new JSZip(); + zip.file('[Content_Types].xml', contentTypes); + zip.file('_rels/.rels', rels); + zip.file('3D/3dmodel.model', modelXml); + + // Generate the 3MF file as a blob + return await zip.generateAsync({ + type: "uint8array", + compression: 'DEFLATE', + compressionOptions: { level: 6 }, + streamFiles: true + }); +} + /** * @param {Object} data binary file * @returns {Array} vertex face array @@ -229,4 +356,5 @@ export function parseAsync(data) { } resolve(extractItems(models)); }); -} \ No newline at end of file +} + diff --git a/src/mesh/api.js b/src/mesh/api.js index 080ab478..de17b7ac 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -1008,9 +1008,10 @@ let file = { h.input({ id: "filename", value: "mesh_export" }) ]), h.div([ - h.button({ _: "download OBJ", onclick() { doit('obj') } }), - h.button({ _: "download STL", onclick() { doit('stl') } }), - h.button({ _: "download STEP", onclick() { doit('step') } }), + h.button({ _: "3MF", onclick() { doit('3mf') } }), + h.button({ _: "OBJ", onclick() { doit('obj') } }), + h.button({ _: "STL", onclick() { doit('stl') } }), + h.button({ _: "STEP", onclick() { doit('step') } }), ]) ]) ] }); diff --git a/src/mesh/work.js b/src/mesh/work.js index 091a0faa..ccbd0d54 100644 --- a/src/mesh/work.js +++ b/src/mesh/work.js @@ -17,6 +17,7 @@ import { CSG } from '../geo/csg.js'; import { util } from './util.js'; import { tool as meshTool } from './tool.js'; import { meshToSTEPWithFaces } from '../load/step.js'; +import { encode as tmfEncode } from '../load/3mf.js'; import { encode as objEncode } from '../load/obj.js'; import { encode as stlEncode } from '../load/stl.js'; @@ -580,6 +581,10 @@ const file = { return objEncode(recs, header); case "stl": return stlEncode(recs, header); + case "3mf": + send.async(); + tmfEncode(recs, header).then(data => send.done(data)); + return; default: throw `invalid format "${format}"`; }