feat: add support for retraction and Z-lift in Pather and Printer Intrinsics

This commit is contained in:
Nicolas Predella 2026-07-13 13:10:14 +03:00
parent 76cf856155
commit 21dde0c4a5
6 changed files with 129 additions and 10 deletions

View file

@ -17,12 +17,16 @@ class SliceIntrinsics:
bed_y: float
feedrate: float
extrusion_rate: float
retraction: float
lift: float
def __init__(self, bed_x=250.0, bed_y=250.0, feedrate=1200.0, extrusion_rate=0.017):
def __init__(self, bed_x: float = 250.0, bed_y: float = 250.0, feedrate: float = 1200.0, extrusion_rate: float = 0.017, retraction: float = 0.0, lift: float = 0.0):
self.bed_x = bed_x
self.bed_y = bed_y
self.feedrate = feedrate
self.extrusion_rate = extrusion_rate
self.retraction = retraction
self.lift = lift
class InputNode(SliceNode):
input_type: str = ""

View file

@ -35,6 +35,8 @@ class SLICENODE_PathsToGcode(SliceNode):
pather.bed_center_y = intrinsics.bed_y / 2.0
pather.feedrate = intrinsics.feedrate
pather.extrusion_rate = intrinsics.extrusion_rate
pather.retraction = intrinsics.retraction
pather.lift = intrinsics.lift
gcode: str = pather.to_gcode(include_header=self.use_header)
return gcode

View file

@ -226,11 +226,15 @@ class SLICENODE_InputIntrinsics(SliceNode):
self.add_input("SLICESOCK_Float", "Bed Y").value = 210.0
self.add_input("SLICESOCK_Float", "Feedrate").value = 1200.0
self.add_input("SLICESOCK_Float", "Extrusion Rate").value = 0.035
self.add_input("SLICESOCK_Float", "Retraction").value = 0.0
self.add_input("SLICESOCK_Float", "Lift").value = 0.0
def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics:
return SliceIntrinsics(
bed_x=self.resolve("Bed X"),
bed_y=self.resolve("Bed Y"),
feedrate=self.resolve("Feedrate"),
extrusion_rate=self.resolve("Extrusion Rate")
extrusion_rate=self.resolve("Extrusion Rate"),
retraction=self.resolve("Retraction"),
lift=self.resolve("Lift")
)

View file

@ -1108,6 +1108,8 @@ class Pather(object):
self.bed_center_y: float = 250.
self.feedrate: float = 1200.
self.extrusion_rate: float = 0.017
self.retraction: float = 0.0
self.lift: float = 0.0
def to_gcode(self, include_header: bool = True) -> str:
if not self.paths:
@ -1153,8 +1155,21 @@ class Pather(object):
]
gcode += header
# Safely parse retraction and lift fields.
# If they cannot be parsed or value is zero, we skip them.
try:
retraction_val = float(self.retraction) if self.retraction is not None else 0.0
except (TypeError, ValueError):
retraction_val = 0.0
try:
lift_val = float(self.lift) if self.lift is not None else 0.0
except (TypeError, ValueError):
lift_val = 0.0
last_z = -1.0
max_z = 0
last_nozzle_z = 0.0
for i, line in enumerate(centered_paths):
coords = line.tolist()
@ -1167,15 +1182,51 @@ class Pather(object):
x0, y0, z0 = coords[0]
active_feedrate = self.feedrate
if i > 0:
# 1. Retract if retraction is enabled
if retraction_val > 0.0:
gcode.append(f"G1 E-{retraction_val:.5f} F1800")
active_feedrate = 1800.0
# 2. Lift Z if lift is enabled
if lift_val > 0.0:
gcode.append(f"G0 Z{last_nozzle_z + lift_val:.3f} F720")
active_feedrate = 720.0
# 3. Travel to the start of path `i` (x0, y0) in XY
travel_fr = f_rate[0] if f_rate[0] > 0 else self.feedrate
gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} F{travel_fr}")
active_feedrate = travel_fr
# 4. Handle Z alignment (layer change and/or un-lift)
if abs(z0 - last_z) > 0.001:
gcode.append(f";BEFORE_LAYER_CHANGE")
gcode.append(f"G0 Z{z0:.3f} F720")
gcode.append(f";AFTER_LAYER_CHANGE")
last_z = z0
active_feedrate = 720.0
elif lift_val > 0.0:
gcode.append(f"G0 Z{z0:.3f} F720")
active_feedrate = 720.0
# 5. Un-retract if retraction was enabled
if retraction_val > 0.0:
gcode.append(f"G1 E{retraction_val:.5f} F1800")
active_feedrate = 1800.0
else:
if abs(z0 - last_z) > 0.001:
gcode.append(f";BEFORE_LAYER_CHANGE")
gcode.append(f"G0 Z{z0:.3f} F720")
gcode.append(f";AFTER_LAYER_CHANGE")
last_z = z0
active_feedrate = 720.0
old_fr = f_rate[0] if f_rate[0] > 0 else self.feedrate
gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f} F{old_fr}")
active_feedrate = old_fr
old_fr = active_feedrate
old_x, old_y, old_z = x0, y0, z0
old_extras = {prefix: None for prefix in extras}
for j, (p1, p2) in enumerate(zip(coords, coords[1:])):
@ -1207,4 +1258,6 @@ class Pather(object):
max_z = max(max_z, z)
old_fr = fr
last_nozzle_z = coords[-1][2]
return "\n".join(gcode)

View file

@ -48,7 +48,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **Integer**: Single integer input.
- **String**: Single string/path input.
- **Rotation**: Input for Euler rotation vectors.
- **Printer Intrinsics**: Configures bed size and extrusion rates.
- **Printer Intrinsics**: Configures bed size, extrusion rates, retraction, and Z-lift.
- **Gcode Block**: Manual text area for entering raw G-code.
### Geometry Processing

View file

@ -244,5 +244,61 @@ class TestSlice(unittest.TestCase):
self.assertAlmostEqual(area, 25.0, places=3)
self.assertTrue(np.allclose(mask_path[:, 2], 3.0))
def test_pather_retraction_lift(self):
from NodeSlicer.core.slice import Pather
path1 = np.array([
[10.0, 10.0, 0.2],
[20.0, 10.0, 0.2]
])
path2 = np.array([
[30.0, 30.0, 0.2],
[40.0, 30.0, 0.2]
])
# Test default (no retraction or lift)
pather = Pather([path1, path2])
pather.retraction = 0.0
pather.lift = 0.0
gcode = pather.to_gcode(include_header=False)
self.assertNotIn("E-", gcode)
self.assertNotIn("Z0.700", gcode) # no lift
# Test with retraction and lift
pather = Pather([path1, path2])
pather.retraction = 1.2
pather.lift = 0.5
gcode = pather.to_gcode(include_header=False)
# Retraction should be present before travel
self.assertIn("G1 E-1.20000 F1800", gcode)
# Lift should be present: 0.2 (last_nozzle_z) + 0.5 (lift) = 0.7
self.assertIn("G0 Z0.700 F720", gcode)
# Un-lift back to 0.2 should be present
self.assertIn("G0 Z0.200 F720", gcode)
# Un-retraction should be present
self.assertIn("G1 E1.20000 F1800", gcode)
# Test with lift only (no retraction)
pather = Pather([path1, path2])
pather.retraction = 0.0
pather.lift = 0.5
gcode = pather.to_gcode(include_header=False)
self.assertNotIn("E-", gcode)
# Lift should be present: 0.2 (last_nozzle_z) + 0.5 (lift) = 0.7
self.assertIn("G0 Z0.700 F720", gcode)
# Un-lift back to 0.2 should be present
self.assertIn("G0 Z0.200 F720", gcode)
# Test with unparsable retraction and lift fields
pather = Pather([path1, path2])
pather.retraction = "invalid"
pather.lift = None
gcode = pather.to_gcode(include_header=False)
self.assertNotIn("E-", gcode)
self.assertNotIn("Z0.700", gcode)
if __name__ == '__main__':
unittest.main()