#!/usr/bin/env python3 """Combine two aligned OpenSCAD 3MF meshes into a Bambu-friendly multipart 3MF.""" from __future__ import annotations import argparse import copy import zipfile from pathlib import Path from xml.etree import ElementTree as ET CORE = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02" ET.register_namespace("", CORE) CONTENT_TYPES_XML = b''' ''' RELATIONSHIPS_XML = b''' ''' def load_mesh(path: Path) -> ET.Element: with zipfile.ZipFile(path) as archive: root = ET.fromstring(archive.read("3D/3dmodel.model")) mesh = root.find(f".//{{{CORE}}}mesh") if mesh is None: raise ValueError(f"{path} does not contain a 3MF mesh") return copy.deepcopy(mesh) def model_xml(bases: list[Path], accents: list[Path], title: str) -> bytes: root = ET.Element(f"{{{CORE}}}model", { "unit": "millimeter", "{http://www.w3.org/XML/1998/namespace}lang": "en-US", }) ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Title"}).text = title ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Generator"}).text = "copy-stand" resources = ET.SubElement(root, f"{{{CORE}}}resources") materials = ET.SubElement(resources, f"{{{CORE}}}basematerials", {"id": "1"}) ET.SubElement(materials, f"{{{CORE}}}base", {"name": "Black ASA", "displaycolor": "#101014"}) ET.SubElement(materials, f"{{{CORE}}}base", {"name": "Blue ASA inlay", "displaycolor": "#0D47CC"}) parts: list[tuple[int, str, Path, int]] = [] next_id = 2 for index, source in enumerate(bases): parts.append((next_id, f"Black ASA segment {index + 1} - Filament 1", source, 0)) next_id += 1 for index, source in enumerate(accents): parts.append((next_id, f"Blue ASA inlay {index + 1} - Filament 2", source, 1)) next_id += 1 for object_id, name, source, material_index in parts: obj = ET.SubElement(resources, f"{{{CORE}}}object", { "id": str(object_id), "name": name, "type": "model", "pid": "1", "pindex": str(material_index), }) obj.append(load_mesh(source)) assembly_id = next_id assembly = ET.SubElement(resources, f"{{{CORE}}}object", { "id": str(assembly_id), "name": title, "type": "model", }) components = ET.SubElement(assembly, f"{{{CORE}}}components") for object_id, _name, _source, _material_index in parts: ET.SubElement(components, f"{{{CORE}}}component", {"objectid": str(object_id)}) build = ET.SubElement(root, f"{{{CORE}}}build") ET.SubElement(build, f"{{{CORE}}}item", {"objectid": str(assembly_id)}) return ET.tostring(root, encoding="utf-8", xml_declaration=True), assembly_id, parts def model_settings_xml(title: str, assembly_id: int, parts: list[tuple[int, str, Path, int]]) -> bytes: root = ET.Element("config") obj = ET.SubElement(root, "object", {"id": str(assembly_id)}) ET.SubElement(obj, "metadata", {"key": "name", "value": title}) for part_id, name, _source, material_index in parts: extruder = material_index + 1 part = ET.SubElement(obj, "part", {"id": str(part_id), "subtype": "normal_part"}) ET.SubElement(part, "metadata", {"key": "name", "value": name}) ET.SubElement(part, "metadata", {"key": "extruder", "value": str(extruder)}) ET.SubElement(root, "assemble") return ET.tostring(root, encoding="utf-8", xml_declaration=True) def write_entry(archive: zipfile.ZipFile, name: str, data: bytes) -> None: info = zipfile.ZipInfo(name) info.create_system = 3 info.external_attr = 0o100644 << 16 info.compress_type = zipfile.ZIP_DEFLATED archive.writestr(info, data) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("output", type=Path) parser.add_argument("--base", action="append", type=Path, required=True) parser.add_argument("--accent", action="append", type=Path, default=[]) parser.add_argument("--title", required=True) args = parser.parse_args() args.output.parent.mkdir(parents=True, exist_ok=True) xml, assembly_id, parts = model_xml(args.base, args.accent, args.title) with zipfile.ZipFile(args.output, "w") as archive: write_entry(archive, "[Content_Types].xml", CONTENT_TYPES_XML) write_entry(archive, "_rels/.rels", RELATIONSHIPS_XML) write_entry(archive, "3D/3dmodel.model", xml) write_entry(archive, "Metadata/model_settings.config", model_settings_xml(args.title, assembly_id, parts)) if __name__ == "__main__": main()