retraction feedrate + seam alignment
This commit is contained in:
parent
e3e2335754
commit
98c9032f8f
8 changed files with 314 additions and 12 deletions
|
|
@ -20,8 +20,9 @@ class SliceIntrinsics:
|
||||||
extrusion_rate: float
|
extrusion_rate: float
|
||||||
retraction: float
|
retraction: float
|
||||||
lift: float
|
lift: float
|
||||||
|
retraction_feedrate: float
|
||||||
|
|
||||||
def __init__(self, bed_x: float = 250.0, bed_y: float = 250.0, feedrate: float = 1200.0, travel_feedrate: float = 2400.0, extrusion_rate: float = 0.017, retraction: float = 0.0, lift: float = 0.0):
|
def __init__(self, bed_x: float = 250.0, bed_y: float = 250.0, feedrate: float = 1200.0, travel_feedrate: float = 2400.0, extrusion_rate: float = 0.017, retraction: float = 0.0, lift: float = 0.0, retraction_feedrate: float = 0.0):
|
||||||
self.bed_x = bed_x
|
self.bed_x = bed_x
|
||||||
self.bed_y = bed_y
|
self.bed_y = bed_y
|
||||||
self.feedrate = feedrate
|
self.feedrate = feedrate
|
||||||
|
|
@ -29,6 +30,7 @@ class SliceIntrinsics:
|
||||||
self.extrusion_rate = extrusion_rate
|
self.extrusion_rate = extrusion_rate
|
||||||
self.retraction = retraction
|
self.retraction = retraction
|
||||||
self.lift = lift
|
self.lift = lift
|
||||||
|
self.retraction_feedrate = retraction_feedrate
|
||||||
|
|
||||||
class InputNode(SliceNode):
|
class InputNode(SliceNode):
|
||||||
input_type: str = ""
|
input_type: str = ""
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ class SLICENODE_PathsToGcode(SliceNode):
|
||||||
pather.travel_feedrate = intrinsics.travel_feedrate
|
pather.travel_feedrate = intrinsics.travel_feedrate
|
||||||
pather.extrusion_rate = intrinsics.extrusion_rate
|
pather.extrusion_rate = intrinsics.extrusion_rate
|
||||||
pather.retraction = intrinsics.retraction
|
pather.retraction = intrinsics.retraction
|
||||||
|
pather.retraction_feedrate = getattr(intrinsics, "retraction_feedrate", 0.0)
|
||||||
pather.lift = intrinsics.lift
|
pather.lift = intrinsics.lift
|
||||||
|
|
||||||
gcode: str = pather.to_gcode(include_header=self.use_header)
|
gcode: str = pather.to_gcode(include_header=self.use_header)
|
||||||
|
|
|
||||||
|
|
@ -286,9 +286,16 @@ class SLICENODE_InputIntrinsics(SliceNode):
|
||||||
self.add_input("SLICESOCK_Float", "Travel Feedrate").value = 2400.0
|
self.add_input("SLICESOCK_Float", "Travel Feedrate").value = 2400.0
|
||||||
self.add_input("SLICESOCK_Float", "Extrusion Rate").value = 0.035
|
self.add_input("SLICESOCK_Float", "Extrusion Rate").value = 0.035
|
||||||
self.add_input("SLICESOCK_Float", "Retraction").value = 0.0
|
self.add_input("SLICESOCK_Float", "Retraction").value = 0.0
|
||||||
|
self.add_input("SLICESOCK_Float", "Retraction Feedrate").value = 0.0
|
||||||
self.add_input("SLICESOCK_Float", "Lift").value = 0.0
|
self.add_input("SLICESOCK_Float", "Lift").value = 0.0
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics:
|
||||||
|
ret_fr = 0.0
|
||||||
|
try:
|
||||||
|
ret_fr = self.resolve("Retraction Feedrate") or 0.0
|
||||||
|
except Exception:
|
||||||
|
ret_fr = 0.0
|
||||||
|
|
||||||
return SliceIntrinsics(
|
return SliceIntrinsics(
|
||||||
bed_x=self.resolve("Bed X"),
|
bed_x=self.resolve("Bed X"),
|
||||||
bed_y=self.resolve("Bed Y"),
|
bed_y=self.resolve("Bed Y"),
|
||||||
|
|
@ -296,5 +303,6 @@ class SLICENODE_InputIntrinsics(SliceNode):
|
||||||
travel_feedrate=self.resolve("Travel Feedrate"),
|
travel_feedrate=self.resolve("Travel Feedrate"),
|
||||||
extrusion_rate=self.resolve("Extrusion Rate"),
|
extrusion_rate=self.resolve("Extrusion Rate"),
|
||||||
retraction=self.resolve("Retraction"),
|
retraction=self.resolve("Retraction"),
|
||||||
lift=self.resolve("Lift")
|
lift=self.resolve("Lift"),
|
||||||
|
retraction_feedrate=ret_fr
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ class SLICENODE_SliceShells(SliceNode):
|
||||||
mat_inv = np.array(Euler(rotation).to_matrix())
|
mat_inv = np.array(Euler(rotation).to_matrix())
|
||||||
slices = slices.transform(mat_inv)
|
slices = slices.transform(mat_inv)
|
||||||
|
|
||||||
|
slices = slices.align_seams(mode="sharpest")
|
||||||
|
|
||||||
return slices
|
return slices
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
|
||||||
183
core/slice.py
183
core/slice.py
|
|
@ -88,6 +88,70 @@ def _transfer_group_attributes(src_sp: SlicePaths, res_paths: list[np.ndarray],
|
||||||
return out_ext, out_feed, out_extra
|
return out_ext, out_feed, out_extra
|
||||||
|
|
||||||
|
|
||||||
|
def align_path_seam_sharpest(path: np.ndarray) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Reorders a closed 2D/3D polygon path (N, 3) or (N, 2) so that its starting vertex (seam)
|
||||||
|
is located at the sharpest concave angle (inner corner). If no concave angles exist,
|
||||||
|
falls back to the sharpest angle overall.
|
||||||
|
|
||||||
|
If path is open or has fewer than 3 distinct vertices, returns path unchanged.
|
||||||
|
Uses vectorized NumPy operations for high performance.
|
||||||
|
"""
|
||||||
|
if len(path) < 4:
|
||||||
|
return path
|
||||||
|
|
||||||
|
if not np.allclose(path[0], path[-1]):
|
||||||
|
return path
|
||||||
|
|
||||||
|
pts = path[:-1]
|
||||||
|
M = len(pts)
|
||||||
|
if M < 3:
|
||||||
|
return path
|
||||||
|
|
||||||
|
pts2d = pts[:, :2]
|
||||||
|
x = pts2d[:, 0]
|
||||||
|
y = pts2d[:, 1]
|
||||||
|
x_next = np.roll(x, -1)
|
||||||
|
y_next = np.roll(y, -1)
|
||||||
|
signed_area = 0.5 * np.sum(x * y_next - x_next * y)
|
||||||
|
if abs(signed_area) < 1e-9:
|
||||||
|
return path
|
||||||
|
|
||||||
|
prev_pts = np.roll(pts2d, 1, axis=0)
|
||||||
|
next_pts = np.roll(pts2d, -1, axis=0)
|
||||||
|
|
||||||
|
v_in = pts2d - prev_pts
|
||||||
|
v_out = next_pts - pts2d
|
||||||
|
|
||||||
|
l_in = np.linalg.norm(v_in, axis=1)
|
||||||
|
l_out = np.linalg.norm(v_out, axis=1)
|
||||||
|
|
||||||
|
valid = (l_in > 1e-6) & (l_out > 1e-6)
|
||||||
|
if not np.any(valid):
|
||||||
|
return path
|
||||||
|
|
||||||
|
cross_2d = v_in[:, 0] * v_out[:, 1] - v_in[:, 1] * v_out[:, 0]
|
||||||
|
if signed_area > 0:
|
||||||
|
concave_mask = valid & (cross_2d < -1e-6)
|
||||||
|
else:
|
||||||
|
concave_mask = valid & (cross_2d > 1e-6)
|
||||||
|
|
||||||
|
cos_angles = np.full(M, 1.0)
|
||||||
|
dot_prods = np.sum(v_in[valid] * v_out[valid], axis=1)
|
||||||
|
cos_angles[valid] = dot_prods / (l_in[valid] * l_out[valid])
|
||||||
|
|
||||||
|
if np.any(concave_mask):
|
||||||
|
concave_indices = np.where(concave_mask)[0]
|
||||||
|
best_idx = int(concave_indices[np.argmin(cos_angles[concave_mask])])
|
||||||
|
else:
|
||||||
|
best_idx = int(np.argmin(cos_angles))
|
||||||
|
|
||||||
|
if best_idx == 0:
|
||||||
|
return path
|
||||||
|
|
||||||
|
return np.vstack([path[best_idx:-1], path[:best_idx + 1]])
|
||||||
|
|
||||||
|
|
||||||
class SlicePaths(list[np.ndarray]):
|
class SlicePaths(list[np.ndarray]):
|
||||||
groups: list[int]
|
groups: list[int]
|
||||||
extrusion_rate: list[np.ndarray]
|
extrusion_rate: list[np.ndarray]
|
||||||
|
|
@ -398,6 +462,105 @@ class SlicePaths(list[np.ndarray]):
|
||||||
def normalize_groups(self) -> None:
|
def normalize_groups(self) -> None:
|
||||||
self.groups = [m.setdefault(x, len(m)) for m in [{}] for x in self.groups]
|
self.groups = [m.setdefault(x, len(m)) for m in [{}] for x in self.groups]
|
||||||
|
|
||||||
|
def align_seams(self, mode: str = "sharpest") -> SlicePaths:
|
||||||
|
"""
|
||||||
|
Aligns the seams (starting point of closed paths) in SlicePaths.
|
||||||
|
Supported modes: 'sharpest'.
|
||||||
|
"""
|
||||||
|
if not self or mode != "sharpest":
|
||||||
|
return self
|
||||||
|
|
||||||
|
new_paths: list[np.ndarray] = []
|
||||||
|
new_ext: list[np.ndarray] = []
|
||||||
|
new_feed: list[np.ndarray] = []
|
||||||
|
new_extra: dict[str, list[np.ndarray]] = {k: [] for k in self.extra_attributes}
|
||||||
|
|
||||||
|
for i, p in enumerate(self):
|
||||||
|
if len(p) < 4 or not np.allclose(p[0], p[-1]):
|
||||||
|
new_paths.append(p)
|
||||||
|
new_ext.append(self.extrusion_rate[i])
|
||||||
|
new_feed.append(self.feedrate[i])
|
||||||
|
for k in self.extra_attributes:
|
||||||
|
new_extra[k].append(self.extra_attributes[k][i])
|
||||||
|
continue
|
||||||
|
|
||||||
|
pts = p[:-1]
|
||||||
|
M = len(pts)
|
||||||
|
if M < 3:
|
||||||
|
new_paths.append(p)
|
||||||
|
new_ext.append(self.extrusion_rate[i])
|
||||||
|
new_feed.append(self.feedrate[i])
|
||||||
|
for k in self.extra_attributes:
|
||||||
|
new_extra[k].append(self.extra_attributes[k][i])
|
||||||
|
continue
|
||||||
|
|
||||||
|
pts2d = pts[:, :2]
|
||||||
|
x = pts2d[:, 0]
|
||||||
|
y = pts2d[:, 1]
|
||||||
|
x_next = np.roll(x, -1)
|
||||||
|
y_next = np.roll(y, -1)
|
||||||
|
signed_area = 0.5 * np.sum(x * y_next - x_next * y)
|
||||||
|
|
||||||
|
prev_pts = np.roll(pts2d, 1, axis=0)
|
||||||
|
next_pts = np.roll(pts2d, -1, axis=0)
|
||||||
|
|
||||||
|
v_in = pts2d - prev_pts
|
||||||
|
v_out = next_pts - pts2d
|
||||||
|
|
||||||
|
l_in = np.linalg.norm(v_in, axis=1)
|
||||||
|
l_out = np.linalg.norm(v_out, axis=1)
|
||||||
|
|
||||||
|
valid = (l_in > 1e-6) & (l_out > 1e-6)
|
||||||
|
if not np.any(valid) or abs(signed_area) < 1e-9:
|
||||||
|
best_idx = 0
|
||||||
|
else:
|
||||||
|
cross_2d = v_in[:, 0] * v_out[:, 1] - v_in[:, 1] * v_out[:, 0]
|
||||||
|
if signed_area > 0:
|
||||||
|
concave_mask = valid & (cross_2d < -1e-6)
|
||||||
|
else:
|
||||||
|
concave_mask = valid & (cross_2d > 1e-6)
|
||||||
|
|
||||||
|
cos_angles = np.full(M, 1.0)
|
||||||
|
dot_prods = np.sum(v_in[valid] * v_out[valid], axis=1)
|
||||||
|
cos_angles[valid] = dot_prods / (l_in[valid] * l_out[valid])
|
||||||
|
|
||||||
|
if np.any(concave_mask):
|
||||||
|
concave_indices = np.where(concave_mask)[0]
|
||||||
|
best_idx = int(concave_indices[np.argmin(cos_angles[concave_mask])])
|
||||||
|
else:
|
||||||
|
best_idx = int(np.argmin(cos_angles))
|
||||||
|
|
||||||
|
if best_idx == 0:
|
||||||
|
new_paths.append(p)
|
||||||
|
new_ext.append(self.extrusion_rate[i])
|
||||||
|
new_feed.append(self.feedrate[i])
|
||||||
|
for k in self.extra_attributes:
|
||||||
|
new_extra[k].append(self.extra_attributes[k][i])
|
||||||
|
else:
|
||||||
|
new_p = np.vstack([p[best_idx:-1], p[:best_idx + 1]])
|
||||||
|
new_paths.append(new_p)
|
||||||
|
|
||||||
|
ext = self.extrusion_rate[i]
|
||||||
|
if len(ext) == len(p):
|
||||||
|
new_ext.append(np.concatenate([ext[best_idx:-1], ext[:best_idx + 1]]))
|
||||||
|
else:
|
||||||
|
new_ext.append(ext)
|
||||||
|
|
||||||
|
feed = self.feedrate[i]
|
||||||
|
if len(feed) == len(p):
|
||||||
|
new_feed.append(np.concatenate([feed[best_idx:-1], feed[:best_idx + 1]]))
|
||||||
|
else:
|
||||||
|
new_feed.append(feed)
|
||||||
|
|
||||||
|
for k in self.extra_attributes:
|
||||||
|
attr = self.extra_attributes[k][i]
|
||||||
|
if len(attr) == len(p):
|
||||||
|
new_extra[k].append(np.concatenate([attr[best_idx:-1], attr[:best_idx + 1]]))
|
||||||
|
else:
|
||||||
|
new_extra[k].append(attr)
|
||||||
|
|
||||||
|
return self.copy_with(new_paths, extrusion_rate=new_ext, feedrate=new_feed, extra_attributes=new_extra)
|
||||||
|
|
||||||
def slice_at_range(models, z_values) -> list[list[np.ndarray]]:
|
def slice_at_range(models, z_values) -> list[list[np.ndarray]]:
|
||||||
if not models:
|
if not models:
|
||||||
return []
|
return []
|
||||||
|
|
@ -568,6 +731,7 @@ def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
|
||||||
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
|
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
|
||||||
# Close loop by appending the first point for internal consistency
|
# Close loop by appending the first point for internal consistency
|
||||||
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
||||||
|
fpath_3d = align_path_seam_sharpest(fpath_3d)
|
||||||
res.append(fpath_3d)
|
res.append(fpath_3d)
|
||||||
|
|
||||||
res.extend(open_paths)
|
res.extend(open_paths)
|
||||||
|
|
@ -1227,6 +1391,7 @@ def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round',
|
||||||
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
|
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
|
||||||
# Re-close loop
|
# Re-close loop
|
||||||
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
||||||
|
fpath_3d = align_path_seam_sharpest(fpath_3d)
|
||||||
paths_list.append(fpath_3d)
|
paths_list.append(fpath_3d)
|
||||||
groups_list.append(group_id)
|
groups_list.append(group_id)
|
||||||
|
|
||||||
|
|
@ -1391,6 +1556,7 @@ class Pather(object):
|
||||||
self.first_layer_height: float | None = None
|
self.first_layer_height: float | None = None
|
||||||
self._layer_width: float | None = None
|
self._layer_width: float | None = None
|
||||||
self.retraction: float = 0.0
|
self.retraction: float = 0.0
|
||||||
|
self.retraction_feedrate: float = 0.0
|
||||||
self.lift: float = 0.0
|
self.lift: float = 0.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -1434,6 +1600,14 @@ class Pather(object):
|
||||||
dz = p2[2] - p1[2]
|
dz = p2[2] - p1[2]
|
||||||
return (dx * dx + dy * dy + dz * dz) ** 0.5
|
return (dx * dx + dy * dy + dz * dz) ** 0.5
|
||||||
|
|
||||||
|
try:
|
||||||
|
retraction_fr_val = float(self.retraction_feedrate) if self.retraction_feedrate is not None else 0.0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
retraction_fr_val = 0.0
|
||||||
|
|
||||||
|
if retraction_fr_val <= 0.0:
|
||||||
|
retraction_fr_val = self.feedrate
|
||||||
|
|
||||||
gcode: list[str] = []
|
gcode: list[str] = []
|
||||||
|
|
||||||
if include_header:
|
if include_header:
|
||||||
|
|
@ -1442,6 +1616,7 @@ class Pather(object):
|
||||||
f";TYPE:Custom",
|
f";TYPE:Custom",
|
||||||
f";DEFAULT_FEEDRATE={self.feedrate}",
|
f";DEFAULT_FEEDRATE={self.feedrate}",
|
||||||
f";DEFAULT_TRAVEL_FEEDRATE={self.travel_feedrate}",
|
f";DEFAULT_TRAVEL_FEEDRATE={self.travel_feedrate}",
|
||||||
|
f";DEFAULT_RETRACTION_FEEDRATE={retraction_fr_val}",
|
||||||
f";DEFAULT_EXTRUSION_RATE={self.extrusion_rate}",
|
f";DEFAULT_EXTRUSION_RATE={self.extrusion_rate}",
|
||||||
f";DEFAULT_LAYER_HEIGHT={self.layer_height:.6f}",
|
f";DEFAULT_LAYER_HEIGHT={self.layer_height:.6f}",
|
||||||
]
|
]
|
||||||
|
|
@ -1489,8 +1664,8 @@ class Pather(object):
|
||||||
if i > 0:
|
if i > 0:
|
||||||
# 1. Retract if retraction is enabled
|
# 1. Retract if retraction is enabled
|
||||||
if retraction_val > 0.0:
|
if retraction_val > 0.0:
|
||||||
gcode.append(f"G1 E-{retraction_val:.5f} F1800")
|
gcode.append(f"G1 E-{retraction_val:.5f} F{retraction_fr_val:.3f}")
|
||||||
active_feedrate = 1800.0
|
active_feedrate = retraction_fr_val
|
||||||
|
|
||||||
# 2. Lift Z if lift is enabled
|
# 2. Lift Z if lift is enabled
|
||||||
if lift_val > 0.0:
|
if lift_val > 0.0:
|
||||||
|
|
@ -1515,8 +1690,8 @@ class Pather(object):
|
||||||
|
|
||||||
# 5. Un-retract if retraction was enabled
|
# 5. Un-retract if retraction was enabled
|
||||||
if retraction_val > 0.0:
|
if retraction_val > 0.0:
|
||||||
gcode.append(f"G1 E{retraction_val:.5f} F1800")
|
gcode.append(f"G1 E{retraction_val:.5f} F{retraction_fr_val:.3f}")
|
||||||
active_feedrate = 1800.0
|
active_feedrate = retraction_fr_val
|
||||||
else:
|
else:
|
||||||
if abs(z0 - last_z) > 0.001:
|
if abs(z0 - last_z) > 0.001:
|
||||||
gcode.append(f";BEFORE_LAYER_CHANGE")
|
gcode.append(f";BEFORE_LAYER_CHANGE")
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
- `slice.py`:
|
- `slice.py`:
|
||||||
- `slice_at_range()`: Top-level slicing function that iterates through Z-heights.
|
- `slice_at_range()`: Top-level slicing function that iterates through Z-heights.
|
||||||
- `slice_at_z()`: Vectorized plane-mesh intersection using NumPy.
|
- `slice_at_z()`: Vectorized plane-mesh intersection using NumPy.
|
||||||
|
- `align_path_seam_sharpest()`: High-performance vectorized heuristic that positions the start vertex (seam) of closed polygon loops at the sharpest concave angle (inner corner), with fallback to sharpest angle overall.
|
||||||
|
- `SlicePaths.align_seams()`: Method on `SlicePaths` to align path seams to sharpest concave angles while preserving per-vertex attributes and metadata.
|
||||||
- `Pather`: Class that converts toolpaths into G-code strings. Supports per-segment extrusion rates, feedrates, and custom extra attributes.
|
- `Pather`: Class that converts toolpaths into G-code strings. Supports per-segment extrusion rates, feedrates, and custom extra attributes.
|
||||||
- `segments_to_slice()`: Utility to convert raw intersection segments into continuous paths (closed polygons are processed via Pyclipper, open paths remain open).
|
- `segments_to_slice()`: Utility to convert raw intersection segments into continuous paths (closed polygons are processed via Pyclipper, open paths remain open).
|
||||||
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Offset X**: X translation offset for the build item.
|
- **Offset X**: X translation offset for the build item.
|
||||||
- **Offset Y**: Y translation offset for the build item.
|
- **Offset Y**: Y translation offset for the build item.
|
||||||
- **Offset Z**: Z translation offset for the build item.
|
- **Offset Z**: Z translation offset for the build item.
|
||||||
- **Printer Intrinsics**: Configures bed size, extrusion/travel rates, retraction, and Z-lift.
|
- **Printer Intrinsics**: Configures bed size, extrusion/travel rates, retraction distance & feedrate, and Z-lift.
|
||||||
- **Gcode Block**: Manual text area for entering raw G-code.
|
- **Gcode Block**: Manual text area for entering raw G-code.
|
||||||
|
|
||||||
### Geometry Processing
|
### Geometry Processing
|
||||||
|
|
@ -76,7 +76,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Rotate Path**: Rotates `SlicePaths` around the Z-axis.
|
- **Rotate Path**: Rotates `SlicePaths` around the Z-axis.
|
||||||
- **Angle**: Rotation angle in degrees.
|
- **Angle**: Rotation angle in degrees.
|
||||||
- **Center**: Optional center of rotation. If not provided, it defaults to the midpoint of the first layer.
|
- **Center**: Optional center of rotation. If not provided, it defaults to the midpoint of the first layer.
|
||||||
- **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height. Supports rotation for slicing at an angle.
|
- **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height. Automatically aligns path seams to the sharpest concave angle (inner corner) of each layer. Supports rotation for slicing at an angle.
|
||||||
- **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types.
|
- **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types.
|
||||||
- **Buffer Curve**: Buffers open/closed curves using Shapely, allowing open path offsetting.
|
- **Buffer Curve**: Buffers open/closed curves using Shapely, allowing open path offsetting.
|
||||||
- **Cap Style**: Style of the curve ends (Round, Flat, Square).
|
- **Cap Style**: Style of the curve ends (Round, Flat, Square).
|
||||||
|
|
|
||||||
|
|
@ -401,20 +401,28 @@ class TestSlice(unittest.TestCase):
|
||||||
self.assertNotIn("E-", gcode)
|
self.assertNotIn("E-", gcode)
|
||||||
self.assertNotIn("Z0.700", gcode) # no lift
|
self.assertNotIn("Z0.700", gcode) # no lift
|
||||||
|
|
||||||
# Test with retraction and lift
|
# Test with retraction and lift (default retraction feedrate uses default feedrate 1200)
|
||||||
pather = Pather([path1, path2])
|
pather = Pather([path1, path2])
|
||||||
pather.retraction = 1.2
|
pather.retraction = 1.2
|
||||||
pather.lift = 0.5
|
pather.lift = 0.5
|
||||||
gcode = pather.to_gcode(include_header=False)
|
gcode = pather.to_gcode(include_header=False)
|
||||||
|
|
||||||
# Retraction should be present before travel
|
# Retraction should be present before travel using default feedrate 1200.0
|
||||||
self.assertIn("G1 E-1.20000 F1800", gcode)
|
self.assertIn("G1 E-1.20000 F1200.000", gcode)
|
||||||
# Lift should be present: 0.2 (last_nozzle_z) + 0.5 (lift) = 0.7
|
# Lift should be present: 0.2 (last_nozzle_z) + 0.5 (lift) = 0.7
|
||||||
self.assertIn("G0 Z0.700 F720", gcode)
|
self.assertIn("G0 Z0.700 F720", gcode)
|
||||||
# Un-lift back to 0.2 should be present
|
# Un-lift back to 0.2 should be present
|
||||||
self.assertIn("G0 Z0.200 F720", gcode)
|
self.assertIn("G0 Z0.200 F720", gcode)
|
||||||
# Un-retraction should be present
|
# Un-retraction should be present
|
||||||
self.assertIn("G1 E1.20000 F1800", gcode)
|
self.assertIn("G1 E1.20000 F1200.000", gcode)
|
||||||
|
|
||||||
|
# Test with explicit retraction_feedrate
|
||||||
|
pather = Pather([path1, path2])
|
||||||
|
pather.retraction = 1.2
|
||||||
|
pather.retraction_feedrate = 1800.0
|
||||||
|
gcode_rf = pather.to_gcode(include_header=False)
|
||||||
|
self.assertIn("G1 E-1.20000 F1800.000", gcode_rf)
|
||||||
|
self.assertIn("G1 E1.20000 F1800.000", gcode_rf)
|
||||||
|
|
||||||
# Test with lift only (no retraction)
|
# Test with lift only (no retraction)
|
||||||
pather = Pather([path1, path2])
|
pather = Pather([path1, path2])
|
||||||
|
|
@ -594,5 +602,109 @@ class TestSlice(unittest.TestCase):
|
||||||
self.assertEqual(getattr(extended, "first_layer_height", None), 0.3)
|
self.assertEqual(getattr(extended, "first_layer_height", None), 0.3)
|
||||||
self.assertEqual(getattr(extended, "custom_meta", None), "test_value")
|
self.assertEqual(getattr(extended, "custom_meta", None), "test_value")
|
||||||
|
|
||||||
|
def test_align_seam_sharpest_rectangle(self):
|
||||||
|
from NodeSlicer.core.slice import align_path_seam_sharpest
|
||||||
|
# Rectangle where index 0 is a midpoint on a flat edge (5, 0, 0)
|
||||||
|
path = np.array([
|
||||||
|
[5.0, 0.0, 0.0],
|
||||||
|
[10.0, 0.0, 0.0],
|
||||||
|
[10.0, 10.0, 0.0],
|
||||||
|
[0.0, 10.0, 0.0],
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[5.0, 0.0, 0.0]
|
||||||
|
])
|
||||||
|
aligned = align_path_seam_sharpest(path)
|
||||||
|
self.assertEqual(len(aligned), len(path))
|
||||||
|
self.assertTrue(np.allclose(aligned[0], aligned[-1]))
|
||||||
|
# Start point should be moved from flat edge (5,0,0) to one of the 90-degree corners
|
||||||
|
self.assertFalse(np.allclose(aligned[0, :2], [5.0, 0.0]))
|
||||||
|
corners = np.array([[10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]])
|
||||||
|
is_corner = any(np.allclose(aligned[0, :2], c) for c in corners)
|
||||||
|
self.assertTrue(is_corner)
|
||||||
|
|
||||||
|
def test_align_seam_sharpest_triangle(self):
|
||||||
|
from NodeSlicer.core.slice import align_path_seam_sharpest
|
||||||
|
# Right triangle with acute 30-degree vertex at (10, 0)
|
||||||
|
# Vertices: 90 deg at (0,0), 60 deg at (0, 5.7735), 30 deg at (10,0)
|
||||||
|
# Start at 90 deg vertex (0,0)
|
||||||
|
path = np.array([
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[0.0, 5.7735, 0.0],
|
||||||
|
[10.0, 0.0, 0.0],
|
||||||
|
[0.0, 0.0, 0.0]
|
||||||
|
])
|
||||||
|
aligned = align_path_seam_sharpest(path)
|
||||||
|
self.assertTrue(np.allclose(aligned[0], aligned[-1]))
|
||||||
|
# Sharpest angle is 30 degrees at (10.0, 0.0)
|
||||||
|
np.testing.assert_allclose(aligned[0, :2], [10.0, 0.0], atol=1e-3)
|
||||||
|
|
||||||
|
def test_slicepaths_align_seams(self):
|
||||||
|
from NodeSlicer.core.slice import SlicePaths
|
||||||
|
path = np.array([
|
||||||
|
[5.0, 0.0, 0.2],
|
||||||
|
[10.0, 0.0, 0.2],
|
||||||
|
[10.0, 10.0, 0.2],
|
||||||
|
[0.0, 10.0, 0.2],
|
||||||
|
[0.0, 0.0, 0.2],
|
||||||
|
[5.0, 0.0, 0.2]
|
||||||
|
])
|
||||||
|
ext_rates = np.array([1.0, 1.1, 1.2, 1.3, 1.4, 1.0])
|
||||||
|
sp = SlicePaths([path], groups=[0], extrusion_rate=[ext_rates])
|
||||||
|
sp.layer_height = 0.2
|
||||||
|
|
||||||
|
aligned_sp = sp.align_seams(mode="sharpest")
|
||||||
|
self.assertEqual(getattr(aligned_sp, "layer_height", None), 0.2)
|
||||||
|
self.assertEqual(len(aligned_sp[0]), len(path))
|
||||||
|
self.assertTrue(np.allclose(aligned_sp[0][0], aligned_sp[0][-1]))
|
||||||
|
self.assertFalse(np.allclose(aligned_sp[0][0, :2], [5.0, 0.0]))
|
||||||
|
self.assertEqual(len(aligned_sp.extrusion_rate[0]), len(aligned_sp[0]))
|
||||||
|
|
||||||
|
def test_align_seam_sharpest_concave_l_shape(self):
|
||||||
|
from NodeSlicer.core.slice import align_path_seam_sharpest
|
||||||
|
# L-shaped polygon in CCW order:
|
||||||
|
# Outer convex corners: (0,0), (10,0), (10,5), (5,10), (0,10)
|
||||||
|
# Inner concave corner: (5,5)
|
||||||
|
path = np.array([
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[10.0, 0.0, 0.0],
|
||||||
|
[10.0, 5.0, 0.0],
|
||||||
|
[5.0, 5.0, 0.0],
|
||||||
|
[5.0, 10.0, 0.0],
|
||||||
|
[0.0, 10.0, 0.0],
|
||||||
|
[0.0, 0.0, 0.0]
|
||||||
|
])
|
||||||
|
aligned = align_path_seam_sharpest(path)
|
||||||
|
self.assertEqual(len(aligned), len(path))
|
||||||
|
self.assertTrue(np.allclose(aligned[0], aligned[-1]))
|
||||||
|
# Seam should be placed at the concave inner corner (5.0, 5.0)
|
||||||
|
np.testing.assert_allclose(aligned[0, :2], [5.0, 5.0])
|
||||||
|
|
||||||
|
def test_intrinsics_retraction_feedrate(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.base import SliceIntrinsics
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_InputIntrinsics
|
||||||
|
|
||||||
|
# Test SliceIntrinsics default and custom retraction feedrate
|
||||||
|
intr = SliceIntrinsics(retraction_feedrate=1800.0)
|
||||||
|
self.assertEqual(intr.retraction_feedrate, 1800.0)
|
||||||
|
|
||||||
|
intr_default = SliceIntrinsics()
|
||||||
|
self.assertEqual(intr_default.retraction_feedrate, 0.0)
|
||||||
|
|
||||||
|
# Test Node evaluation
|
||||||
|
node = SLICENODE_InputIntrinsics()
|
||||||
|
values = {
|
||||||
|
"Bed X": 200.0,
|
||||||
|
"Bed Y": 200.0,
|
||||||
|
"Feedrate": 1500.0,
|
||||||
|
"Travel Feedrate": 3000.0,
|
||||||
|
"Extrusion Rate": 0.02,
|
||||||
|
"Retraction": 1.0,
|
||||||
|
"Retraction Feedrate": 1800.0,
|
||||||
|
"Lift": 0.2
|
||||||
|
}
|
||||||
|
node.resolve = lambda name: values.get(name, 0.0)
|
||||||
|
res_intr = node.evaluate_output("SLICESOCK_Intrinsics")
|
||||||
|
self.assertEqual(res_intr.retraction_feedrate, 1800.0)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue