diff --git a/core/nodeslicer/nodes/base.py b/core/nodeslicer/nodes/base.py index 829315e..0984456 100644 --- a/core/nodeslicer/nodes/base.py +++ b/core/nodeslicer/nodes/base.py @@ -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 = "" diff --git a/core/nodeslicer/nodes/gcode.py b/core/nodeslicer/nodes/gcode.py index 084adc8..6c2b780 100644 --- a/core/nodeslicer/nodes/gcode.py +++ b/core/nodeslicer/nodes/gcode.py @@ -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 diff --git a/core/nodeslicer/nodes/inputs.py b/core/nodeslicer/nodes/inputs.py index 5653c46..2dc3f89 100644 --- a/core/nodeslicer/nodes/inputs.py +++ b/core/nodeslicer/nodes/inputs.py @@ -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") ) diff --git a/core/slice.py b/core/slice.py index b22f79e..3ac80bc 100644 --- a/core/slice.py +++ b/core/slice.py @@ -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] - 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 = 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 - 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}") + # 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) \ No newline at end of file diff --git a/doc/NODES.md b/doc/NODES.md index 79c5121..f4b8b71 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -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 diff --git a/tests/test_slice.py b/tests/test_slice.py index 8ce68bf..75472c3 100644 --- a/tests/test_slice.py +++ b/tests/test_slice.py @@ -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()