diff --git a/core/nodeslicer/nodes/inputs.py b/core/nodeslicer/nodes/inputs.py index a48d7e7..abe2320 100644 --- a/core/nodeslicer/nodes/inputs.py +++ b/core/nodeslicer/nodes/inputs.py @@ -2,6 +2,7 @@ from __future__ import annotations from typing import Any import bpy import os +import numpy as np from ..bpy_ns_types import NfStringProperty, notify_tree_changed, NfEnumProperty from ....registry import register_class @@ -76,6 +77,32 @@ class SLICENODE_CollectionObjects(SliceNode): return bpy.data.objects[socket_name] return None +@register_class +class SLICENODE_WipeTower(SliceNode): + bl_label = "Wipe Tower" + + def init(self, context): + self.add_input("SLICESOCK_Float", "X Position").value = 170.0 + self.add_input("SLICESOCK_Float", "Y Position").value = 140.0 + self.add_output("SLICESOCK_SettingsRo", "Settings") + + def evaluate_output(self, socket_name: str, **_kwargs) -> Any: + if socket_name != "Settings": + return {} + + raw_x = self.resolve("X Position") + raw_y = self.resolve("Y Position") + + if isinstance(raw_x, (list, tuple, np.ndarray)) and len(raw_x) >= 2: + pos_x, pos_y = float(raw_x[0]), float(raw_x[1]) + else: + pos_x = float(raw_x) if raw_x is not None else 0.0 + pos_y = float(raw_y) if raw_y is not None else 0.0 + + return { + "wipe_tower_position": (pos_x, pos_y) + } + @register_class class SLICENODE_InputSettings(InputNode): input_type = "Settings" diff --git a/core/nodeslicer/nodes/system.py b/core/nodeslicer/nodes/system.py index 06ac285..fd7d575 100644 --- a/core/nodeslicer/nodes/system.py +++ b/core/nodeslicer/nodes/system.py @@ -220,8 +220,20 @@ class SLICENODE_PrusaslicerSlice(SliceNode): offset = np.array([bed_center[0], bed_center[1], a_z]) - np.array([slicing_group.center_xy[0], slicing_group.center_xy[1], 0]) slicing_group.offset(offset) - # Prepare overrides for generate_conf_writer - writer_overrides = {k: {'value': v} for k, v in overrides.items()} + # Check for wipe_tower_position override + if overrides and "wipe_tower_position" in overrides: + wipe_tower_pos = overrides["wipe_tower_position"] + try: + wx = float(wipe_tower_pos[0]) + wy = float(wipe_tower_pos[1]) + slicing_group.wipe_tower_xy = np.array([wx, wy]) + except Exception: + pass + + # Prepare overrides for generate_conf_writer (excluding wipe_tower_position) + writer_overrides = { + k: {'value': v} for k, v in (overrides or {}).items() if k != "wipe_tower_position" + } # Generate the ConfigWriter from ....infra.prusaslicer_profiles import ConfigWriter diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index dabd455..0af228a 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -481,6 +481,7 @@ class SLICENODE_MT_DataInput(Menu): layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject" layout.operator("node.add_node", text="Collection Objects").type = "SLICENODE_CollectionObjects" layout.operator("node.add_node", text="Settings").type = "SLICENODE_InputSettings" + layout.operator("node.add_node", text="Wipe Tower").type = "SLICENODE_WipeTower" layout.operator("node.add_node", text="PrusaSlicer Config").type = "SLICENODE_PrusaslicerConf" layout.separator() layout.operator("node.add_node", text="Printer").type = "SLICENODE_InputPrinter" diff --git a/doc/NODES.md b/doc/NODES.md index effec97..29a1724 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -47,7 +47,10 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Float**: Single float input. - **Integer**: Single integer input. - **String**: Single string/path input. -- **Rotation**: Input for Euler rotation vectors. +- **Settings**: Read-only/write configuration settings dictionary. +- **Wipe Tower**: Configures the Wipe Tower printbed position (`wipe_tower_position`) passed to 3MF metadata (`Prusa_Slicer_wipe_tower_information.xml`) without injecting into profile INI settings. + - **Position X**: X coordinate of the wipe tower on the printbed. + - **Position Y**: Y coordinate of the wipe tower on the printbed. - **Printer Intrinsics**: Configures bed size, extrusion/travel rates, retraction, and Z-lift. - **Gcode Block**: Manual text area for entering raw G-code. diff --git a/infra/prusaslicer_profiles/models.py b/infra/prusaslicer_profiles/models.py index e712467..72a63e0 100644 --- a/infra/prusaslicer_profiles/models.py +++ b/infra/prusaslicer_profiles/models.py @@ -70,6 +70,8 @@ class ConfigWriter: def write_ini_3mf(self, config_local_path: Path): with open(config_local_path, 'w') as file: for key, val in dict(sorted(self.config_dict.items())).items(): + if key == "wipe_tower_position": + continue file.write(f"; {key} = {val}\n") def get(self, key: str, default: str = "") -> str | list[str]: diff --git a/tests/test_wipe_tower.py b/tests/test_wipe_tower.py new file mode 100644 index 0000000..463a98f --- /dev/null +++ b/tests/test_wipe_tower.py @@ -0,0 +1,44 @@ +import unittest +import numpy as np +from pathlib import Path +import tempfile +import os +import sys + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.append(str(PROJECT_ROOT)) + +from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_WipeTower +from NodeSlicer.infra.prusaslicer_profiles.models import ConfigWriter +from NodeSlicer.infra.blender.blender_mesh_capture import SlicingGroup + +class TestWipeTowerNode(unittest.TestCase): + def test_wipe_tower_node_output(self): + node = SLICENODE_WipeTower() + node.resolve = lambda key: 180.5 if key == "Position X" else 135.2 + + res = node.evaluate_output("Settings") + self.assertIn("wipe_tower_position", res) + self.assertEqual(res["wipe_tower_position"], (180.5, 135.2)) + + def test_config_writer_excludes_wipe_tower_position(self): + conf = { + "layer_height": "0.2", + "wipe_tower_position": "(180.5, 135.2)", + "nozzle_diameter": "0.4" + } + writer = ConfigWriter(conf) + + with tempfile.TemporaryDirectory() as temp_dir: + ini_path = Path(temp_dir) / "Slic3r_PE.config" + writer.write_ini_3mf(ini_path) + + with open(ini_path, 'r', encoding='utf-8') as f: + content = f.read() + + self.assertIn("; layer_height = 0.2", content) + self.assertIn("; nozzle_diameter = 0.4", content) + self.assertNotIn("wipe_tower_position", content) + +if __name__ == "__main__": + unittest.main()