png23mf / src /3mf2mmuv3.py
mikhail-shevtsov's picture
feat: initial commit
f257a91
#!/bin/env python3
"""
Convert a standard 3mf generated by OpenSCAD with color information
to a MMU‑painted 3mf that PrusaSlicer expects
"""
import argparse
import logging
import zipfile
from pathlib import Path
from xml.etree import ElementTree as ET
# Mapping from extruder index to paint color code used by PrusaSlicer / BambuStudio
PAINT_COLOR_MAP = [
None,
"8",
"0C",
"1C",
"2C",
"3C",
"4C",
"5C",
"6C",
"7C",
"8C",
"9C",
"AC",
"BC",
"CC",
"DC",
]
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--loglevel",
type=str,
choices=["INFO", "DEBUG", "WARN", "ERROR"],
default="INFO",
help="Logging Level",
)
parser.add_argument(
"--force",
default=False,
action="store_true",
help="Force overwriting the output file",
)
parser.add_argument("openscad_3mf", help="OpenSCAD 3MF input")
parser.add_argument("prusaslicer_3mf", help="PrusaSlicer 3MF output")
args = parser.parse_args()
logging.basicConfig(
format="%(asctime)s %(levelname)8s %(message)s", level=args.loglevel
)
# register the namespaces we care about
namespaces = {
"xml": "http://www.w3.org/XML/1998/namespace",
"": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
"p": "http://schemas.microsoft.com/3dmanufacturing/production/2015/06",
"slic3rpe": "http://schemas.slic3r.org/3mf/2017/06",
}
for k, v in namespaces.items():
ET.register_namespace(k, v)
os3mf = Path(args.openscad_3mf)
ps3mf = Path(args.prusaslicer_3mf)
if not os3mf.exists():
logging.error(f"The input file {os3mf} doesn't exist")
exit(1)
if ps3mf.exists() and not args.force:
logging.error(
f"The output file {ps3mf} exists and will not be overwritten without --force"
)
exit(1)
with zipfile.ZipFile(
ps3mf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
) as pszip:
with zipfile.ZipFile(os3mf, "r") as oszip:
# copy the files from the openscad 3mf to the prusaslicer 3mf
for file in [
x for x in oszip.infolist() if x.filename != "3D/3dmodel.model"
]:
logging.info(f"Copying file {file}")
pszip.writestr(file, oszip.open(file).read())
model_raw = oszip.read("3D/3dmodel.model")
logging.info("Updating 3dmodel.model")
# Parse the XML
model_root = ET.fromstring(str(model_raw, encoding="utf-8"))
# Remove any existing Application metadata and set to PrusaSlicer
for meta in list(model_root.findall("metadata")):
if meta.get("name") == "Application":
model_root.remove(meta)
# Add Application metadata
app_meta = ET.Element(
"metadata", {"name": "Application", "preserve": "1"}
)
app_meta.text = "PrusaSlicer"
model_root.append(app_meta)
# add slic3rpe metadata
for k, v in {
"slic3rpe:Version3mf": "1",
"slic3rpe:MmPaintingVersion": "1",
"BambuStudio:3mfVersion": "1",
}.items():
e = ET.Element("metadata", {"name": k})
e.text = v
model_root.append(e)
# Set color attributes on triangles
for t in model_root.findall(
"resources/object/mesh/triangles/triangle", namespaces
):
p1_value = t.get("p1")
if not p1_value or p1_value == "0":
continue
try:
p1_index = int(p1_value)
except ValueError:
logging.warning(
f"Triangle has non‑integer p1 value: {p1_value}"
)
continue
if p1_index < len(PAINT_COLOR_MAP):
color_code = PAINT_COLOR_MAP[p1_index]
else:
logging.warning(
f"Material number {p1_index} is greater than supported extruders"
)
color_code = None
if color_code:
t.set("slic3rpe:mmu_segmentation", color_code)
t.set("paint_color", color_code)
model_text = str(
ET.tostring(
model_root, encoding="utf-8", xml_declaration=True
),
encoding="utf-8",
)
pszip.writestr("3D/3dmodel.model", model_text)
if __name__ == "__main__":
main()