add mesh:tool 3mf object output
This commit is contained in:
parent
9da2368a7a
commit
f2f5081669
3 changed files with 138 additions and 4 deletions
128
src/load/3mf.js
128
src/load/3mf.js
|
|
@ -212,6 +212,133 @@ function extractItems(records) {
|
||||||
return outItems;
|
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<Blob>} 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 = [` <object id="${objectId}" name="${name}" type="model">`];
|
||||||
|
objectXml.push(' <mesh>');
|
||||||
|
objectXml.push(' <vertices>');
|
||||||
|
|
||||||
|
for (let v of vertices) {
|
||||||
|
objectXml.push(` <vertex x="${v.x}" y="${v.y}" z="${v.z}"/>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
objectXml.push(' </vertices>');
|
||||||
|
objectXml.push(' <triangles>');
|
||||||
|
|
||||||
|
for (let tri of triangles) {
|
||||||
|
objectXml.push(` <triangle v1="${tri[0]}" v2="${tri[1]}" v3="${tri[2]}"/>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
objectXml.push(' </triangles>');
|
||||||
|
objectXml.push(' </mesh>');
|
||||||
|
objectXml.push(' </object>');
|
||||||
|
|
||||||
|
resources.push(objectXml.join('\n'));
|
||||||
|
buildItems.push(` <item objectid="${objectId}"/>`);
|
||||||
|
|
||||||
|
objectId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the complete 3D model XML
|
||||||
|
let modelXml = [
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
`<model unit="${unit}" xml:lang="en-US" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">`,
|
||||||
|
' <resources>',
|
||||||
|
resources.join('\n'),
|
||||||
|
' </resources>',
|
||||||
|
' <build>',
|
||||||
|
buildItems.join('\n'),
|
||||||
|
' </build>',
|
||||||
|
'</model>'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Create [Content_Types].xml
|
||||||
|
let contentTypes = [
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
|
||||||
|
' <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>',
|
||||||
|
' <Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>',
|
||||||
|
'</Types>'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Create _rels/.rels
|
||||||
|
let rels = [
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
|
||||||
|
' <Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>',
|
||||||
|
'</Relationships>'
|
||||||
|
].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
|
* @param {Object} data binary file
|
||||||
* @returns {Array} vertex face array
|
* @returns {Array} vertex face array
|
||||||
|
|
@ -230,3 +357,4 @@ export function parseAsync(data) {
|
||||||
resolve(extractItems(models));
|
resolve(extractItems(models));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1008,9 +1008,10 @@ let file = {
|
||||||
h.input({ id: "filename", value: "mesh_export" })
|
h.input({ id: "filename", value: "mesh_export" })
|
||||||
]),
|
]),
|
||||||
h.div([
|
h.div([
|
||||||
h.button({ _: "download OBJ", onclick() { doit('obj') } }),
|
h.button({ _: "3MF", onclick() { doit('3mf') } }),
|
||||||
h.button({ _: "download STL", onclick() { doit('stl') } }),
|
h.button({ _: "OBJ", onclick() { doit('obj') } }),
|
||||||
h.button({ _: "download STEP", onclick() { doit('step') } }),
|
h.button({ _: "STL", onclick() { doit('stl') } }),
|
||||||
|
h.button({ _: "STEP", onclick() { doit('step') } }),
|
||||||
])
|
])
|
||||||
]) ]
|
]) ]
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { CSG } from '../geo/csg.js';
|
||||||
import { util } from './util.js';
|
import { util } from './util.js';
|
||||||
import { tool as meshTool } from './tool.js';
|
import { tool as meshTool } from './tool.js';
|
||||||
import { meshToSTEPWithFaces } from '../load/step.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 objEncode } from '../load/obj.js';
|
||||||
import { encode as stlEncode } from '../load/stl.js';
|
import { encode as stlEncode } from '../load/stl.js';
|
||||||
|
|
||||||
|
|
@ -580,6 +581,10 @@ const file = {
|
||||||
return objEncode(recs, header);
|
return objEncode(recs, header);
|
||||||
case "stl":
|
case "stl":
|
||||||
return stlEncode(recs, header);
|
return stlEncode(recs, header);
|
||||||
|
case "3mf":
|
||||||
|
send.async();
|
||||||
|
tmfEncode(recs, header).then(data => send.done(data));
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
throw `invalid format "${format}"`;
|
throw `invalid format "${format}"`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue