wipe tower

This commit is contained in:
Nicolas Predella 2026-07-20 19:19:20 +03:00
parent 0e14fa19f7
commit 8cff75d569
6 changed files with 92 additions and 3 deletions

View file

@ -2,6 +2,7 @@ from __future__ import annotations
from typing import Any from typing import Any
import bpy import bpy
import os import os
import numpy as np
from ..bpy_ns_types import NfStringProperty, notify_tree_changed, NfEnumProperty from ..bpy_ns_types import NfStringProperty, notify_tree_changed, NfEnumProperty
from ....registry import register_class from ....registry import register_class
@ -76,6 +77,32 @@ class SLICENODE_CollectionObjects(SliceNode):
return bpy.data.objects[socket_name] return bpy.data.objects[socket_name]
return None 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 @register_class
class SLICENODE_InputSettings(InputNode): class SLICENODE_InputSettings(InputNode):
input_type = "Settings" input_type = "Settings"

View file

@ -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]) 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) slicing_group.offset(offset)
# Prepare overrides for generate_conf_writer # Check for wipe_tower_position override
writer_overrides = {k: {'value': v} for k, v in overrides.items()} 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 # Generate the ConfigWriter
from ....infra.prusaslicer_profiles import ConfigWriter from ....infra.prusaslicer_profiles import ConfigWriter

View file

@ -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="Object").type = "SLICENODE_InputObject"
layout.operator("node.add_node", text="Collection Objects").type = "SLICENODE_CollectionObjects" 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="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.operator("node.add_node", text="PrusaSlicer Config").type = "SLICENODE_PrusaslicerConf"
layout.separator() layout.separator()
layout.operator("node.add_node", text="Printer").type = "SLICENODE_InputPrinter" layout.operator("node.add_node", text="Printer").type = "SLICENODE_InputPrinter"

View file

@ -47,7 +47,10 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **Float**: Single float input. - **Float**: Single float input.
- **Integer**: Single integer input. - **Integer**: Single integer input.
- **String**: Single string/path 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. - **Printer Intrinsics**: Configures bed size, extrusion/travel rates, retraction, and Z-lift.
- **Gcode Block**: Manual text area for entering raw G-code. - **Gcode Block**: Manual text area for entering raw G-code.

View file

@ -70,6 +70,8 @@ class ConfigWriter:
def write_ini_3mf(self, config_local_path: Path): def write_ini_3mf(self, config_local_path: Path):
with open(config_local_path, 'w') as file: with open(config_local_path, 'w') as file:
for key, val in dict(sorted(self.config_dict.items())).items(): for key, val in dict(sorted(self.config_dict.items())).items():
if key == "wipe_tower_position":
continue
file.write(f"; {key} = {val}\n") file.write(f"; {key} = {val}\n")
def get(self, key: str, default: str = "") -> str | list[str]: def get(self, key: str, default: str = "") -> str | list[str]:

44
tests/test_wipe_tower.py Normal file
View file

@ -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()