UNPKG

three-3mf-exporter

Version:
328 lines (325 loc) 11.9 kB
import { XMLBuilder } from 'fast-xml-parser'; import JSZip from 'jszip'; import { Color, Vector3 } from 'three'; const defaultPrintConfig = { printer_name: "Bambu Lab A1", filament: "Bambu PLA Basic @BBL A1", printableWidth: 256, printableDepth: 256, printableHeight: 256, printableArea: ["0x0", "256x0", "256x256", "0x256"], printerSettingsId: "Bambu Lab A1 0.4 nozzle", printSettingsId: "0.20mm Standard @BBL A1" }; async function exportTo3MF(object, printJobConfig) { const objectId = 1; const zip = new JSZip(); const printConfig = Object.assign({}, defaultPrintConfig, printJobConfig); const components = []; const materials = []; collectComponents(object, components, materials); const boundingBox = calculateBoundingBox(components); const modelCenter = calculateModelCenter(boundingBox); const transform = calculateCenteringTransform(modelCenter, boundingBox, printConfig); const mainModelXml = createMainModelXML(objectId, components, transform); const objectModelXml = createObjectModelXML(components); const modelSettingsXml = createModelSettingsXML(objectId, components); const projectSettingsConfig = createProjectSettingsConfig(materials, printConfig); zip.file("_rels/.rels", relationshipsXML({ id: `rel-1`, target: "/3D/3dmodel.model" })); zip.file("3D/3dmodel.model", mainModelXml); zip.file("3D/_rels/3dmodel.model.rels", relationshipsXML({ id: `rel-${objectId}`, target: `/3D/Objects/object-${objectId}.model` })); zip.file(`3D/Objects/object-${objectId}.model`, objectModelXml); zip.file("Metadata/model_settings.config", modelSettingsXml); zip.file("Metadata/project_settings.config", projectSettingsConfig); zip.file("[Content_Types].xml", contentTypesXML()); return await zip.generateAsync({ type: "blob", mimeType: "application/vnd.ms-package.3dmanufacturing-3dmodel+xml" }); } function collectComponents(object, components, materials) { object.updateMatrixWorld(true); if (object.type === "Mesh") { const mesh = object; const geometry = mesh.geometry; const positionAttr = geometry.attributes.position; const indexAttr = geometry.index; let materialInfo = null; if (mesh.material) { const color = new Color(); if ("color" in mesh.material && mesh.material.color) { color.copy(mesh.material.color); } else { color.set(8421504); } const existingMaterial = materials.find( (m) => m.color.r === color.r && m.color.g === color.g && m.color.b === color.b ); if (existingMaterial) { materialInfo = existingMaterial; } else { const extruder = materials.length + 1; materialInfo = { id: materials.length, color, name: mesh.name ? `${mesh.name}_material` : `material_${materials.length}`, extruder }; materials.push(materialInfo); } } const componentId = components.length; const component = { id: componentId, vertices: [], triangles: [], material: materialInfo, name: mesh.name || `Default-${componentId}` }; const vertexMap = /* @__PURE__ */ new Map(); const worldMatrix = mesh.matrixWorld; const processVertex = (vertexIndex) => { const vertex = new Vector3(); vertex.fromBufferAttribute(positionAttr, vertexIndex); vertex.applyMatrix4(worldMatrix); const vertexKey = `${vertex.x},${vertex.y},${vertex.z}`; if (!vertexMap.has(vertexKey)) { vertexMap.set(vertexKey, component.vertices.length); component.vertices.push({ x: vertex.x, y: vertex.y, z: vertex.z }); } return vertexMap.get(vertexKey); }; if (indexAttr) { for (let i = 0; i < indexAttr.count; i += 3) { const v1 = processVertex(indexAttr.getX(i)); const v2 = processVertex(indexAttr.getX(i + 1)); const v3 = processVertex(indexAttr.getX(i + 2)); component.triangles.push({ v1, v2, v3 }); } } else { for (let i = 0; i < positionAttr.count; i += 3) { const v1 = processVertex(i); const v2 = processVertex(i + 1); const v3 = processVertex(i + 2); component.triangles.push({ v1, v2, v3 }); } } components.push(component); } object.children.forEach((child) => { collectComponents(child, components, materials); }); } function calculateBoundingBox(components) { if (components.length === 0) { return { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } }; } const firstVertex = components[0].vertices[0] || { x: 0, y: 0, z: 0 }; const min = { x: firstVertex.x, y: firstVertex.y, z: firstVertex.z }; const max = { x: firstVertex.x, y: firstVertex.y, z: firstVertex.z }; for (const component of components) { for (const vertex of component.vertices) { min.x = Math.min(min.x, vertex.x); min.y = Math.min(min.y, vertex.y); min.z = Math.min(min.z, vertex.z); max.x = Math.max(max.x, vertex.x); max.y = Math.max(max.y, vertex.y); max.z = Math.max(max.z, vertex.z); } } return { min, max }; } function calculateModelCenter(boundingBox) { return { x: (boundingBox.min.x + boundingBox.max.x) / 2, y: (boundingBox.min.y + boundingBox.max.y) / 2, z: (boundingBox.min.z + boundingBox.max.z) / 2 }; } function calculateCenteringTransform(modelCenter, boundingBox, printBed) { const bedCenter = { x: printBed.printableWidth / 2, y: printBed.printableDepth / 2, z: 0 // 通常Z坐标为0,模型放在打印床表面 }; const translation = { x: bedCenter.x - modelCenter.x, y: bedCenter.y - modelCenter.y, z: bedCenter.z - boundingBox.min.z // 将模型的底部放在打印床上 }; return `1 0 0 0 1 0 0 0 1 ${translation.x.toFixed(4)} ${translation.y.toFixed(4)} ${translation.z.toFixed(4)}`; } function createMainModelXML(objectId, components, transform) { const model = { model: { "@_unit": "millimeter", "@_xml:lang": "en-US", "@_xmlns": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02", "@_xmlns:slic3rpe": "http://schemas.slic3r.org/3mf/2017/06", "@_xmlns:p": "http://schemas.microsoft.com/3dmanufacturing/production/2015/06", "@_requiredextensions": "p", "metadata": [ { "@_name": "Application", "#text": "BambuStudio-01.07.04.52" }, { "@_name": "Title", "#text": "Exported 3D Model" }, { "@_name": "CreationDate", "#text": (/* @__PURE__ */ new Date()).toString() }, { "@_name": "Copyright", "#text": "Copyright (c) 2023. All rights reserved." } ], "resources": { object: { "@_id": `${objectId}`, "@_p:uuid": generateUUID(), "@_type": "model", "components": { component: components.map((comp) => ({ "@_p:path": `/3D/Objects/object-${objectId}.model`, "@_objectid": comp.id.toString() })) } } }, "build": { "@_p:uuid": `${generateUUID()}1`, "item": { "@_objectid": `${objectId}`, "@_p:uuid": `${generateUUID()}2`, "@_transform": transform, "@_printable": "1" } } } }; const builder = new XMLBuilder({ attributeNamePrefix: "@_", ignoreAttributes: false, format: true, indentBy: " " }); return `<?xml version="1.0" encoding="UTF-8"?> ${builder.build(model)}`; } function createObjectModelXML(components) { const objects = components.map((component) => { return { object: { "@_id": component.id.toString(), "@_p:uuid": generateUUID(), "@_type": "model", "mesh": { vertices: { vertex: component.vertices.map((v) => ({ "@_x": v.x.toFixed(7), "@_y": v.y.toFixed(7), "@_z": v.z.toFixed(7) })) }, triangles: { triangle: component.triangles.map((t) => ({ "@_v1": t.v1, "@_v2": t.v2, "@_v3": t.v3 })) } } } }; }); const model = { model: { "@_unit": "millimeter", "@_xml:lang": "en-US", "@_xmlns": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02", "@_xmlns:p": "http://schemas.microsoft.com/3dmanufacturing/production/2015/06", "resources": objects, "build": {} } }; const builder = new XMLBuilder({ attributeNamePrefix: "@_", ignoreAttributes: false, format: true, indentBy: " " }); return `<?xml version="1.0" encoding="UTF-8"?> ${builder.build(model)}`; } function createModelSettingsXML(objectId, components) { const partsXml = components.map((comp) => { const extruder = comp.material ? comp.material.extruder : 1; return ` <part id="${comp.id}" subtype="normal_part"> <metadata key="name" value="${comp.name}"/> <metadata key="extruder" value="${extruder}"/> <mesh_stat edges_fixed="0" degenerate_facets="0" facets_removed="0" facets_reversed="0" backwards_edges="0"/> </part>`; }).join("\n"); return `<?xml version="1.0" encoding="UTF-8"?> <config> <object id="${objectId}"> <metadata key="name" value="Exported3DModel.3mf"/> <metadata key="extruder" value="1"/> <metadata key="thumbnail_file" value=""/> ${partsXml} </object> <plate> <metadata key="plater_id" value="1"/> <metadata key="plater_name" value="plate-1"/> <model_instance> <metadata key="object_id" value="${objectId}"/> <metadata key="instance_id" value="0"/> </model_instance> </plate> <assemble> <assemble_item object_id="${objectId}" instance_id="0" offset="0 0 0"/> </assemble> </config>`; } function createProjectSettingsConfig(materials, printConfig) { const colors = materials.map((m) => { const hex = `#${m.color.getHexString()}`; return hex; }); while (colors.length < 2) { colors.push("#FFFFFF"); } const projectSettings = { printable_area: printConfig.printableArea, printable_height: printConfig.printableHeight.toString(), bed_exclude_area: [], filament_colour: colors, filament_settings_id: Array.from({ length: colors.length }).fill(printConfig.filament), filament_diameter: Array.from({ length: colors.length }).fill("1.75"), filament_is_support: Array.from({ length: colors.length }).fill("0"), printer_model: printConfig.printer_name, layer_height: "0.2", wall_loops: "2", sparse_infill_density: "15%", printer_settings_id: printConfig.printerSettingsId, printer_variant: "0.4", nozzle_diameter: ["0.4"], enable_support: "0", support_type: "normal(auto)", print_settings_id: printConfig.printSettingsId }; return JSON.stringify(projectSettings); } function contentTypesXML() { return `<?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" /> <Default Extension="png" ContentType="image/png" /> <Default Extension="gcode" ContentType="text/x.gcode"/> </Types>`; } function relationshipsXML(options) { return `<?xml version="1.0" encoding="UTF-8"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Id="rel-${options.id}" Target="${options.target}" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/> </Relationships>`; } function generateUUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === "x" ? r : r & 3 | 8; return v.toString(16); }); } export { defaultPrintConfig, exportTo3MF };