from __future__ import annotations from typing import Any from numpy.typing import NDArray from pathlib import Path import numpy as np import os, shutil, tempfile import xml.etree.ElementTree as ET from datetime import date from ..blender.blender_mesh_capture import SlicingObject, SlicingGroup, SlicingCollection script_dir = os.path.dirname(os.path.abspath(__file__)) def indent(elem, level=0): i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " for child in elem: indent(child, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i def write_metadata_xml(group: SlicingGroup, filepath): # Custom sorting order for object types object_type_order = { 'ModelPart': 0, 'NegativeVolume': 1, 'ParameterModifier': 2, 'SupportBlocker': 3, 'SupportEnforcer': 4 } xml_content = ET.Element("config") valid_collections = {k: c for k, c in group.collections.items() if c.meshes} for j, (k, collection) in enumerate(valid_collections.items()): sorted_data: list[tuple[NDArray[Any], NDArray[Any], SlicingObject]] = sorted(zip(collection.mesh_start_ids, collection.mesh_end_ids, collection.objects), key=lambda x: object_type_order.get(x[2].object_type, 5)) object_elem = ET.SubElement(xml_content, "object", id=str(j+1), instances_count="1") ET.SubElement(object_elem, "metadata", type="object", key="name", value=collection.name) for i, (start, end, metadata) in enumerate(sorted_data): if metadata.object_type == "ModelPart": for mod in metadata.modifiers: ET.SubElement(object_elem, "metadata", type="object", key=mod['param_id'], value=mod['param_value']) volume_elem = ET.SubElement(object_elem, "volume", firstid=str(start), lastid=str(end)) ET.SubElement(volume_elem, "metadata", type="volume", key="name", value=metadata.name) if metadata.object_type == "ParameterModifier": ET.SubElement(volume_elem, "metadata", type="volume", key="modifier", value="1") for mod in metadata.modifiers: ET.SubElement(volume_elem, "metadata", type="volume", key=mod['param_id'], value=mod['param_value']) ET.SubElement(volume_elem, "metadata", type="volume", key="volume_type", value=metadata.object_type) ET.SubElement(volume_elem, "metadata", type="volume", key="extruder", value=str(metadata.extruder)) ET.SubElement(volume_elem, "metadata", type="volume", key="source_object_id", value="0") ET.SubElement(volume_elem, "metadata", type="volume", key="source_volume_id", value=str(i)) ET.SubElement(volume_elem, "metadata", type="volume", key="matrix", value="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1") ET.SubElement(volume_elem, "mesh", edges_fixed="0", degenerate_facets="0", facets_removed="0", facets_reversed="0", backwards_edges="0") indent(xml_content) xml_tree = ET.ElementTree(xml_content) xml_tree.write(filepath, encoding="UTF-8", xml_declaration=True) def write_wipe_tower_xml(group: SlicingGroup, filename): with open(filename, 'w', encoding="UTF-8") as file: file.write(f'\n') file.write(f'\n') def write_model_xml(group: SlicingGroup, filename: str): now = date.today().isoformat() # Open file for writing with open(filename, 'w', encoding="UTF-8") as file: # Write the XML declaration and opening model tag file.write(f'\n') file.write(f'\n') # Write metadata entries using list comprehension metadata_entries: list[tuple[str, str]] = [ ("slic3rpe:Version3mf", "1"), ("Title", "box"), ("Designer", ""), ("Description", "box"), ("Copyright", ""), ("LicenseTerms", ""), ("Rating", ""), ("CreationDate", now), ("ModificationDate", now), ("Application", "PrusaSlicer-2.9.0") ] file.writelines([f' {value}\n' for name, value in metadata_entries]) # Write resources element and object using list comprehension file.write(f' \n') verts_template = np.vectorize(lambda x, y, z: '\n' % (x, y, z)) idx_template = np.vectorize(lambda a, b, c: '\n' % (a, b, c)) valid_collections: dict[str, SlicingCollection] = {k: c for k, c in group.collections.items() if c.meshes} for i, (k, collection) in enumerate[tuple[str, SlicingCollection]](valid_collections.items()): if not collection.meshes: continue uv, t_idx = collection.unique_verts if uv.size and t_idx.size: file.write(f' \n') file.write(f' \n') file.write(f' \n') file.writelines(verts_template(uv[:,0], uv[:,1], uv[:,2])) file.write(f' \n') file.write(f' \n') file.writelines(idx_template(t_idx[:,0], t_idx[:,1], t_idx[:,2])) file.write(f' \n') file.write(f' \n') file.write(f' \n') file.write(f' \n') # Write build element file.write(f' \n') ox, oy, oz = group.object_offset[0], group.object_offset[1], group.object_offset[2] transform_str = f"1 0 0 0 1 0 0 0 1 {ox} {oy} {oz}" for i, k in enumerate(valid_collections): if not valid_collections[k].unique_verts[0].size: continue file.writelines([f' \n' ]) file.write(f' \n') # Close the model tag file.write(f'\n') def to_3mf(folder_path, output_base_path): zip_file_path = shutil.make_archive(os.path.splitext(output_base_path)[0], 'zip', folder_path) new_file_path = os.path.splitext(zip_file_path)[0] + '.3mf' os.replace(zip_file_path, new_file_path) def write_z_gcodes(z_gcodes, filename: str): root = ET.Element("custom_gcodes_per_print_z", bed_idx="0") for c in z_gcodes: ET.SubElement(root, "code", { "print_z": str(c.z), "type": str(c.type), "extruder": str(c.extruder), "color": c.color, "extra": c.extra, "gcode": c.gcode, }) ET.SubElement(root, "mode", {"value": "SingleExtruder"}) ET.ElementTree(root).write(filename, encoding="utf-8", xml_declaration=True) def prepare_3mf(filepath: Path, geoms: SlicingGroup, conf, z_gcodes) -> None: source_folder = os.path.join(script_dir, 'prusaslicer_3mf') temp_dir = tempfile.mkdtemp() shutil.copytree(source_folder, temp_dir, dirs_exist_ok=True) os.makedirs(os.path.join(temp_dir, '3D'), exist_ok=True) os.makedirs(os.path.join(temp_dir, 'Metadata'), exist_ok=True) write_model_xml(geoms, os.path.join(temp_dir, '3D', '3dmodel.model')) write_metadata_xml(geoms, os.path.join(temp_dir, 'Metadata', 'Slic3r_PE_model.config')) write_wipe_tower_xml(geoms, os.path.join(temp_dir, 'Metadata', 'Prusa_Slicer_wipe_tower_information.xml')) write_z_gcodes(z_gcodes, os.path.join(temp_dir, 'Metadata', 'Prusa_Slicer_custom_gcode_per_print_z.xml')) conf.write_ini_3mf(os.path.join(temp_dir, 'Metadata', 'Slic3r_PE.config')) to_3mf(temp_dir, filepath) return None