139 lines
4.8 KiB
Python
139 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Combine aligned lid meshes into one slicer-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'''<?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>
|
|
'''
|
|
|
|
RELATIONSHIPS_XML = b'''<?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>
|
|
'''
|
|
|
|
|
|
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(base: Path, accent: 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 = (
|
|
"feather-case-wizard"
|
|
)
|
|
resources = ET.SubElement(root, f"{{{CORE}}}resources")
|
|
materials = ET.SubElement(resources, f"{{{CORE}}}basematerials", {"id": "1"})
|
|
ET.SubElement(
|
|
materials, f"{{{CORE}}}base", {"name": "Lid base", "displaycolor": "#D1D5DB"}
|
|
)
|
|
ET.SubElement(
|
|
materials,
|
|
f"{{{CORE}}}base",
|
|
{"name": "Accent inlays", "displaycolor": "#F97316"},
|
|
)
|
|
|
|
for object_id, name, source in (
|
|
(2, "Lid base — Filament 1", base),
|
|
(3, "Accent inlays — Filament 2", accent),
|
|
):
|
|
obj = ET.SubElement(
|
|
resources,
|
|
f"{{{CORE}}}object",
|
|
{
|
|
"id": str(object_id),
|
|
"name": name,
|
|
"type": "model",
|
|
"pid": "1",
|
|
"pindex": str(object_id - 2),
|
|
},
|
|
)
|
|
obj.append(load_mesh(source))
|
|
|
|
assembly = ET.SubElement(
|
|
resources, f"{{{CORE}}}object", {"id": "4", "name": title, "type": "model"}
|
|
)
|
|
components = ET.SubElement(assembly, f"{{{CORE}}}components")
|
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "2"})
|
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "3"})
|
|
build = ET.SubElement(root, f"{{{CORE}}}build")
|
|
ET.SubElement(build, f"{{{CORE}}}item", {"objectid": "4"})
|
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
|
|
|
|
|
def bambu_model_settings_xml(title: str) -> bytes:
|
|
root = ET.Element("config")
|
|
obj = ET.SubElement(root, "object", {"id": "4"})
|
|
ET.SubElement(obj, "metadata", {"key": "name", "value": title})
|
|
for part_id, name, extruder in (
|
|
(2, "Lid base — Filament 1", 1),
|
|
(3, "Accent inlays — Filament 2", 2),
|
|
):
|
|
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("base", type=Path)
|
|
parser.add_argument("accent", type=Path)
|
|
parser.add_argument("output", type=Path)
|
|
parser.add_argument("--title", required=True)
|
|
args = parser.parse_args()
|
|
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
write_entry(archive, "[Content_Types].xml", CONTENT_TYPES_XML)
|
|
write_entry(archive, "_rels/.rels", RELATIONSHIPS_XML)
|
|
write_entry(
|
|
archive,
|
|
"3D/3dmodel.model",
|
|
model_xml(args.base, args.accent, args.title),
|
|
)
|
|
write_entry(
|
|
archive,
|
|
"Metadata/model_settings.config",
|
|
bambu_model_settings_xml(args.title),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|