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(' \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