diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index 08db20f..564d0d7 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -534,7 +534,7 @@ class SLICENODE_TransformGeometry(SliceNode): self.add_output("SLICESOCK_Geometry", "Geometry") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: - if socket_name not in ("Geometry"): + if socket_name not in ("Geometry",): return None geom_data: Any = self.resolve("Geometry") @@ -835,6 +835,3 @@ class SLICENODE_AssignPropertyToObject(SliceNode): props.append(("", str(item))) return _assign_properties_to_item(geom, props) - - - diff --git a/infra/_3mf_writer/_3mf.py b/infra/_3mf_writer/_3mf.py index 743e9de..4cc6047 100644 --- a/infra/_3mf_writer/_3mf.py +++ b/infra/_3mf_writer/_3mf.py @@ -1,18 +1,18 @@ 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 +from ..blender.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 +28,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 +64,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 +96,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 +111,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 +162,20 @@ 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: +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') - 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) + 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_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')) + 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 \ No newline at end of file + to_3mf(temp_dir, filepath) \ No newline at end of file diff --git a/tests/test_transform.py b/tests/test_transform.py index ab03456..aef5e4c 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -170,7 +170,7 @@ class TestTransform(unittest.TestCase): # Define mock resolver to return our inputs def mock_resolve(fieldname): - if fieldname == "Mesh": + if fieldname == "Geometry": return sc elif fieldname == "Translation": return (10.0, 20.0, 30.0) @@ -184,7 +184,7 @@ class TestTransform(unittest.TestCase): node.resolve = mock_resolve # Evaluate output - result_sc = node.evaluate_output("Mesh") + result_sc = node.evaluate_output("Geometry") self.assertIsNotNone(result_sc) # Ensure it's not the same object (deep copied) @@ -338,13 +338,13 @@ class TestTransform(unittest.TestCase): # 1. Transform Geometry t_node = SLICENODE_TransformGeometry() t_node.resolve = lambda f: { - "Mesh": sc, + "Geometry": sc, "Translation": (10.0, 20.0, 0.0), "Rotation": (0.0, 0.0, 0.0), "Scale": (1.0, 1.0, 1.0) }.get(f) - sc_transformed = t_node.evaluate_output("Mesh") + sc_transformed = t_node.evaluate_output("Geometry") self.assertIsNotNone(sc_transformed) self.assertEqual(len(sc_transformed.transforms), 1) self.assertEqual(sc_transformed.transforms[0]["type"], "transform_geometry") diff --git a/tests/test_wipe_tower.py b/tests/test_wipe_tower.py index 87f522a..49c635c 100644 --- a/tests/test_wipe_tower.py +++ b/tests/test_wipe_tower.py @@ -6,7 +6,33 @@ import os import sys PROJECT_ROOT = Path(__file__).resolve().parent.parent -sys.path.append(str(PROJECT_ROOT)) +sys.path.append(str(PROJECT_ROOT.parent)) + +from unittest.mock import MagicMock + +class DummyBase: pass +mock_types = MagicMock() +mock_types.NodeSocket = type("NodeSocket", (DummyBase,), {}) +mock_types.Node = type("Node", (DummyBase,), {}) +mock_types.NodeCustomGroup = type("NodeCustomGroup", (DummyBase,), {}) +mock_types.NodeTree = type("NodeTree", (DummyBase,), {}) +mock_types.PropertyGroup = type("PropertyGroup", (DummyBase,), {}) +mock_types.Operator = type("Operator", (DummyBase,), {}) +mock_types.Panel = type("Panel", (DummyBase,), {}) +mock_types.Object = MagicMock +mock_types.Mesh = MagicMock + +mock_bpy = MagicMock() +mock_bpy.types = mock_types +mock_bpy.path.abspath = lambda path: path.replace("//", "/abs/") + +sys.modules['bpy'] = mock_bpy +sys.modules['bpy.types'] = mock_types +sys.modules['bpy.props'] = MagicMock() +sys.modules['bpy.utils'] = MagicMock() +sys.modules['bpy.utils.previews'] = MagicMock() +sys.modules['bpy.app'] = MagicMock() +sys.modules['bpy.app.handlers'] = MagicMock() from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_WipeTower, SLICENODE_OffsetObject from NodeSlicer.infra.prusaslicer_profiles.models import ConfigWriter