From 9d282dee016bdbc10e4529120ee4e3faedbe6685 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Wed, 26 Aug 2026 12:04:35 +0300 Subject: [PATCH] up --- infra/_3mf_writer/_3mf.py | 194 +++++++++++++++++++++++++++++--------- 1 file changed, 151 insertions(+), 43 deletions(-) diff --git a/infra/_3mf_writer/_3mf.py b/infra/_3mf_writer/_3mf.py index 743e9de..071f45e 100644 --- a/infra/_3mf_writer/_3mf.py +++ b/infra/_3mf_writer/_3mf.py @@ -1,18 +1,21 @@ 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 typing import Any -from ..blender.blender_mesh_capture import SlicingObject, SlicingGroup, SlicingCollection +try: + from ..blender.blender_mesh_capture import SlicingGroup, SlicingCollection +except ImportError: + from ..infra.blender_mesh_capture import SlicingGroup, SlicingCollection script_dir = os.path.dirname(os.path.abspath(__file__)) -def write_metadata_xml(group: SlicingGroup, filepath): + +def write_metadata_xml(group: SlicingGroup, filepath: str | Path) -> None: # Custom sorting order for object types object_type_order = { 'ModelPart': 0, @@ -28,23 +31,31 @@ def write_metadata_xml(group: SlicingGroup, filepath): 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)) + sorted_data = sorted( + zip(collection.mesh_start_ids, collection.mesh_end_ids, collection.objects), + key=lambda x: ( + 0 if x[2].name == collection.name else 1, + object_type_order.get(x[2].object_type, 5), + x[2].name, + ), + ) 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") + + if metadata.object_type in ["ModelPart", "ParameterModifier"]: for mod in metadata.modifiers: + if not mod: continue 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") @@ -56,19 +67,21 @@ def write_metadata_xml(group: SlicingGroup, filepath): xml_tree = ET.ElementTree(xml_content) xml_tree.write(filepath, encoding="UTF-8", xml_declaration=True) -def write_wipe_tower_xml(group: SlicingGroup, filename): + +def write_wipe_tower_xml(group: SlicingGroup, filename: str | Path) -> None: with open(filename, 'w', encoding="UTF-8") as file: - file.write(f'\n') + file.write('\n') file.write(f'\n') -def write_model_xml(group: SlicingGroup, filename: str): + +def write_model_xml(group: SlicingGroup, filename: str | Path) -> None: 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') + file.write('\n') + file.write('\n') # Write metadata entries using list comprehension metadata_entries: list[tuple[str, str]] = [ @@ -86,7 +99,7 @@ def write_model_xml(group: SlicingGroup, filename: str): file.writelines([f' {value}\n' for name, value in metadata_entries]) # Write resources element and object using list comprehension - file.write(f' \n') + file.write(' \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)) @@ -101,40 +114,41 @@ def write_model_xml(group: SlicingGroup, filename: str): if uv.size and t_idx.size: file.write(f' \n') - file.write(f' \n') + file.write(' \n') - file.write(f' \n') + file.write(' \n') file.writelines(verts_template(uv[:,0], uv[:,1], uv[:,2])) - file.write(f' \n') + file.write(' \n') - file.write(f' \n') + file.write(' \n') file.writelines(idx_template(t_idx[:,0], t_idx[:,1], t_idx[:,2])) - file.write(f' \n') + file.write(' \n') - file.write(f' \n') - file.write(f' \n') + file.write(' \n') + file.write(' \n') - file.write(f' \n') + file.write(' \n') # Write build element - file.write(f' \n') - ox, oy, oz = group.object_offset[0], group.object_offset[1], group.object_offset[2] + file.write(' \n') + ox, oy, oz = getattr(group, "object_offset", (0.0, 0.0, 0.0)) 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') + file.writelines([f' \n']) + file.write(' \n') # Close the model tag - file.write(f'\n') + file.write('\n') -def to_3mf(folder_path, output_base_path): +def to_3mf(folder_path: str | Path, output_base_path: str | Path) -> None: 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): + +def write_z_gcodes(z_gcodes, filename: str | Path) -> None: root = ET.Element("custom_gcodes_per_print_z", bed_idx="0") for c in z_gcodes: @@ -151,22 +165,116 @@ def write_z_gcodes(z_gcodes, filename: str): 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) +def rgb_to_html_hex(rgb: tuple[float, float, float]) -> str: + r: int = int(round(rgb[0] * 255)) + g: int = int(round(rgb[1] * 255)) + b: int = int(round(rgb[2] * 255)) + return "#{:02X}{:02X}{:02X}".format(r, g, b) + + +def generate_full_spectrum_data(pg: Any, cx: Any = None) -> dict: + physical_colors = [ + pg.filament_color, + pg.filament_2_color, + pg.filament_3_color, + pg.filament_4_color, + pg.filament_5_color, + ] - os.makedirs(os.path.join(temp_dir, '3D'), exist_ok=True) - os.makedirs(os.path.join(temp_dir, 'Metadata'), exist_ok=True) + physical_extruders = [ + { + "color": rgb_to_html_hex(color), + "id": i + 1 + } + for i, color in enumerate(physical_colors) + ] + + virtual_extruders = [] + + if cx is not None: + try: + from ..blender.blender_bridge import get_inherited_virtual_extruders + from ..blender.blender_mesh_capture import TYPES_NAME + except ImportError: + from ..infra.blender_bridge import get_inherited_virtual_extruders + from .. import TYPES_NAME + ve_list = get_inherited_virtual_extruders(cx, TYPES_NAME) + else: + ve_list = [ + {'id': 6 + idx, 'ratios': list(item.ratios), 'inherited': False} + for idx, item in enumerate(getattr(pg, 'virtual_extruders', [])) + ] - write_model_xml(geoms, os.path.join(temp_dir, '3D', '3dmodel.model')) + for ve_item in ve_list: + idx_id = ve_item['id'] + raw_ratios = list(ve_item['ratios']) + active = [(i + 1, r) for i, r in enumerate(raw_ratios) if r > 0.0] + if len(active) > 3: + active = sorted(active, key=lambda x: x[1], reverse=True)[:3] + active.sort(key=lambda x: x[0]) + + total_sum = sum(r for _, r in active) + if total_sum > 1.0: + normalized = [(ext, r / total_sum) for ext, r in active] + else: + normalized = active + + components = [ + {"extruder": ext, "ratio": round(r, 5)} + for ext, r in normalized + ] + + if total_sum > 0: + calc_norm = total_sum if total_sum > 1.0 else total_sum + r_mix = sum(r * physical_colors[ext - 1][0] for ext, r in normalized) / calc_norm + g_mix = sum(r * physical_colors[ext - 1][1] for ext, r in normalized) / calc_norm + b_mix = sum(r * physical_colors[ext - 1][2] for ext, r in normalized) / calc_norm + virt_color = rgb_to_html_hex((r_mix, g_mix, b_mix)).lower() + else: + virt_color = "#000000" + + virtual_extruders.append({ + "color": virt_color, + "components": components, + "id": idx_id, + "kind": "fullspectrum" + }) + + return { + "physical_extruders": physical_extruders, + "version": 1, + "virtual_extruders": virtual_extruders + } - 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) +def write_full_spectrum_json(pg: Any, filepath: str | Path, cx: Any = None) -> None: + import json + data = generate_full_spectrum_data(pg, cx=cx) + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=4) - return None \ No newline at end of file + +def prepare_3mf(filepath: Path, geoms: SlicingGroup, conf: Any, z_gcodes: Any, pg: Any = None, cx: Any = None) -> None: + source_folder = os.path.join(script_dir, 'prusaslicer_3mf') + with tempfile.TemporaryDirectory() as temp_dir: + 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')) + + if pg is not None: + full_spec = generate_full_spectrum_data(pg, cx=cx) + if len(full_spec["virtual_extruders"]) > 0: + import json + with open(os.path.join(temp_dir, 'Metadata', 'Prusa_Slicer_full_spectrum.json'), 'w', encoding='utf-8') as f: + json.dump(full_spec, f, indent=4) + + to_3mf(temp_dir, filepath) \ No newline at end of file