From c77412bb50fd245c0e91bddde5f0174b74233fdd Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Wed, 26 Aug 2026 12:39:49 +0300 Subject: [PATCH] up --- core/nodeslicer/nodes/mesh.py | 122 ++++++++++++++++++++++++++++++++++ core/nodeslicer/tree.py | 1 + doc/NODES.md | 5 ++ infra/_3mf_writer/_3mf.py | 7 -- tests/test_wipe_tower.py | 90 ++++++++++++++++++++++++- 5 files changed, 216 insertions(+), 9 deletions(-) diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index 564d0d7..864892e 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -835,3 +835,125 @@ class SLICENODE_AssignPropertyToObject(SliceNode): props.append(("", str(item))) return _assign_properties_to_item(geom, props) + + +OBJECT_TYPE_ITEMS: list[tuple[str, str, str]] = [ + ("NO_CHANGE", "Keep Original", "Do not modify the object/part type"), + ("ModelPart", "Model Part", "Standard model part"), + ("ParameterModifier", "Modifier", "Parameter modifier volume"), + ("NegativeVolume", "Negative Volume", "Negative volume for subtractions"), + ("SupportBlocker", "Support Blocker", "Support blocker volume"), + ("SupportEnforcer", "Support Enforcer", "Support enforcer volume"), + ("WipeTower", "Wipe Tower", "Wipe tower marker"), +] + +EXTRUDER_ITEMS: list[tuple[str, str, str]] = [ + ("NO_CHANGE", "Keep Original", "Do not modify extruder assignment"), + ("0", "Default Extruder", "Default Extruder"), + ("1", "Extruder 1", "Extruder 1"), + ("2", "Extruder 2", "Extruder 2"), + ("3", "Extruder 3", "Extruder 3"), + ("4", "Extruder 4", "Extruder 4"), + ("5", "Extruder 5", "Extruder 5"), +] + + +def _apply_modifiers_to_so( + so: SlicingObject, + part_type: str, + extruder: str, + modifiers_dict: dict[str, str] | None +) -> None: + if part_type != "NO_CHANGE": + so.object_type = part_type + if extruder != "NO_CHANGE": + so.extruder = extruder + if modifiers_dict: + existing = {m['param_id']: str(m['param_value']) for m in getattr(so, 'modifiers', []) if m and 'param_id' in m} + for k, v in modifiers_dict.items(): + existing[str(k)] = str(v) + so.modifiers = [{'param_id': k, 'param_value': v} for k, v in existing.items()] + + +def _assign_modifiers_to_item( + geom: Any, + part_type: str = "NO_CHANGE", + extruder: str = "NO_CHANGE", + modifiers_dict: dict[str, str] | None = None +) -> Any: + if geom is None: + return None + elif isinstance(geom, (list, tuple)): + return [_assign_modifiers_to_item(item, part_type, extruder, modifiers_dict) for item in geom] + elif isinstance(geom, SlicingGroup): + copied = _copy_slicing_group(geom) + for coll in copied.collections.values(): + for obj in coll.objects: + _apply_modifiers_to_so(obj, part_type, extruder, modifiers_dict) + return copied + elif isinstance(geom, SlicingCollection): + copied = _copy_slicing_collection(geom) + for obj in copied.objects: + _apply_modifiers_to_so(obj, part_type, extruder, modifiers_dict) + return copied + elif isinstance(geom, SlicingObject): + copied = _copy_slicing_object(geom) + _apply_modifiers_to_so(copied, part_type, extruder, modifiers_dict) + return copied + return geom + + +@register_class +class SLICENODE_AssignModifiers(SliceNode): + bl_label = "Assign Modifiers" + + part_type: str = NfEnumProperty( + name="Part Type", + description="PrusaSlicer part/volume type", + items=OBJECT_TYPE_ITEMS, + default="NO_CHANGE" + ) + extruder: str = NfEnumProperty( + name="Extruder", + description="Extruder assignment", + items=EXTRUDER_ITEMS, + default="NO_CHANGE" + ) + + def init(self, context): + self.add_input("SLICESOCK_Mesh", "Geometry") + self.add_input("SLICESOCK_Settings", "Modifiers") + self.add_output("SLICESOCK_Mesh", "Geometry") + + def draw_buttons(self, context, layout): + assert layout + layout.prop(self, "part_type", text="Type") + layout.prop(self, "extruder", text="Extruder") + + def evaluate_output(self, socket_name: str, **_kwargs) -> Any: + if socket_name != "Geometry": + return None + + geom: Any = self.resolve("Geometry") + if not geom: + return None + + raw_modifiers = self.resolve("Modifiers") + if raw_modifiers is None: + raw_modifiers = self.resolve("Settings") + + mod_dict: dict[str, str] = {} + if raw_modifiers: + if isinstance(raw_modifiers, dict): + for k, v in raw_modifiers.items(): + mod_dict[str(k)] = str(v) + elif isinstance(raw_modifiers, (list, tuple)): + for item in raw_modifiers: + if isinstance(item, tuple) and len(item) == 2: + mod_dict[str(item[0])] = str(item[1]) + elif isinstance(item, dict): + for k, v in item.items(): + mod_dict[str(k)] = str(v) + + return _assign_modifiers_to_item(geom, self.part_type, self.extruder, mod_dict if mod_dict else None) + diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index 9507ba4..33ecccb 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -517,6 +517,7 @@ class SLICENODE_MT_Geometry(Menu): layout.operator("node.add_node", text="Rename Mesh").type = "SLICENODE_RenameMesh" layout.operator("node.add_node", text="Assign PDF Properties").type = "SLICENODE_AssignPdfProperties" layout.operator("node.add_node", text="Assign Property to Object").type = "SLICENODE_AssignPropertyToObject" + layout.operator("node.add_node", text="Assign Modifiers").type = "SLICENODE_AssignModifiers" layout.operator("node.add_node", text="Transform Geometry").type = "SLICENODE_TransformGeometry" layout.operator("node.add_node", text="Drop Geometry").type = "SLICENODE_DropGeometry" layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh" diff --git a/doc/NODES.md b/doc/NODES.md index 3f9ec32..267d1d7 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -73,6 +73,11 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Assign Property to Object**: Assigns custom settings/properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) for rendering in PDF properties tables. - **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`). - **Settings**: Input socket accepting configuration settings or property items. +- **Assign Modifiers**: Assigns PrusaSlicer part/volume types (`ModelPart`, `ParameterModifier`, `NegativeVolume`, `SupportBlocker`, `SupportEnforcer`, `WipeTower`), extruder assignments (`0` to `5`), and config parameter modifiers directly to geometry (`SlicingObject`, `SlicingCollection`, or `SlicingGroup`) for 3MF export. + - **Geometry**: Generic geometry input socket (`SLICESOCK_Mesh`). + - **Modifiers**: Optional input socket accepting configuration settings dictionary (`SLICESOCK_Settings`). + - **Part Type**: Dropdown property specifying target part/volume type (`Keep Original`, `Model Part`, `Modifier`, `Negative Volume`, `Support Blocker`, `Support Enforcer`, `Wipe Tower`). + - **Extruder**: Dropdown property specifying target extruder assignment (`Keep Original`, `Default Extruder`, `Extruder 1-5`). - **Transform Geometry**: Moves, rotates, and scales geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) relative to the origin. - **Geometry**: Input geometry data. - **Translation**: 3D vector specifying the translation offset. diff --git a/infra/_3mf_writer/_3mf.py b/infra/_3mf_writer/_3mf.py index 6e2a5ff..4cc6047 100644 --- a/infra/_3mf_writer/_3mf.py +++ b/infra/_3mf_writer/_3mf.py @@ -7,14 +7,7 @@ import xml.etree.ElementTree as ET from datetime import date from typing import Any -<<<<<<< HEAD from ..blender.blender_mesh_capture import SlicingGroup, SlicingCollection -======= -try: - from ..blender.blender_mesh_capture import SlicingGroup, SlicingCollection -except ImportError: - from ..infra.blender_mesh_capture import SlicingGroup, SlicingCollection ->>>>>>> 9d282dee016bdbc10e4529120ee4e3faedbe6685 script_dir = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/test_wipe_tower.py b/tests/test_wipe_tower.py index 49c635c..d9d6206 100644 --- a/tests/test_wipe_tower.py +++ b/tests/test_wipe_tower.py @@ -35,9 +35,11 @@ sys.modules['bpy.app'] = MagicMock() sys.modules['bpy.app.handlers'] = MagicMock() from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_WipeTower, SLICENODE_OffsetObject +from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_AssignModifiers from NodeSlicer.infra.prusaslicer_profiles.models import ConfigWriter -from NodeSlicer.infra._3mf_writer._3mf import write_model_xml -from NodeSlicer.infra.blender.blender_mesh_capture import SlicingGroup +from NodeSlicer.infra._3mf_writer._3mf import write_model_xml, write_metadata_xml +from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingCollection, SlicingGroup +import xml.etree.ElementTree as ET class TestWipeTowerNode(unittest.TestCase): def test_wipe_tower_node_output(self): @@ -77,5 +79,89 @@ class TestWipeTowerNode(unittest.TestCase): self.assertNotIn("wipe_tower_position", content) self.assertNotIn("offset_object", content) + +class TestAssignModifiersNode(unittest.TestCase): + def setUp(self): + self.so = SlicingObject() + self.so.name = "TestPart" + self.so.object_type = "ModelPart" + self.so.extruder = "1" + self.so.modifiers = [{"param_id": "initial_param", "param_value": "123"}] + self.so.mesh = np.array([[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]], dtype=np.float64) + + self.sc = SlicingCollection([], "TestColl") + self.sc.objects = [self.so] + self.sc.mesh_start_ids = [0] + self.sc.mesh_end_ids = [1] + + self.sg = SlicingGroup([]) + self.sg.collections = {"TestColl": self.sc} + + def test_assign_modifiers_to_slicing_object(self): + node = SLICENODE_AssignModifiers() + node.part_type = "ParameterModifier" + node.extruder = "3" + node.resolve = lambda f: { + "Geometry": self.so, + "Modifiers": {"infill_density": "20%", "layer_height": "0.15"} + }.get(f) + + result_so = node.evaluate_output("Geometry") + self.assertIsNotNone(result_so) + self.assertEqual(result_so.object_type, "ParameterModifier") + self.assertEqual(result_so.extruder, "3") + + mod_dict = {m['param_id']: m['param_value'] for m in result_so.modifiers} + self.assertEqual(mod_dict["initial_param"], "123") + self.assertEqual(mod_dict["infill_density"], "20%") + self.assertEqual(mod_dict["layer_height"], "0.15") + + def test_assign_modifiers_to_slicing_group(self): + node = SLICENODE_AssignModifiers() + node.part_type = "NegativeVolume" + node.extruder = "2" + node.resolve = lambda f: { + "Geometry": self.sg, + "Modifiers": {"perimeters": "4"} + }.get(f) + + result_sg = node.evaluate_output("Geometry") + self.assertIsNotNone(result_sg) + res_so = result_sg.collections["TestColl"].objects[0] + self.assertEqual(res_so.object_type, "NegativeVolume") + self.assertEqual(res_so.extruder, "2") + + mod_dict = {m['param_id']: m['param_value'] for m in res_so.modifiers} + self.assertEqual(mod_dict["perimeters"], "4") + + def test_3mf_metadata_xml_output(self): + node = SLICENODE_AssignModifiers() + node.part_type = "ParameterModifier" + node.extruder = "2" + node.resolve = lambda f: { + "Geometry": self.sg, + "Modifiers": {"fill_density": "30%"} + }.get(f) + + modified_sg = node.evaluate_output("Geometry") + + with tempfile.TemporaryDirectory() as temp_dir: + xml_path = Path(temp_dir) / "Slic3r_PE_model.config" + write_metadata_xml(modified_sg, xml_path) + + tree = ET.parse(xml_path) + root = tree.getroot() + + volumes = root.findall(".//volume") + self.assertEqual(len(volumes), 1) + vol = volumes[0] + + meta_entries = {m.get("key"): m.get("value") for m in vol.findall("metadata")} + self.assertEqual(meta_entries.get("volume_type"), "ParameterModifier") + self.assertEqual(meta_entries.get("modifier"), "1") + self.assertEqual(meta_entries.get("extruder"), "2") + self.assertEqual(meta_entries.get("fill_density"), "30%") + + if __name__ == "__main__": unittest.main()