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
|
||||
retraction: 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_y = bed_y
|
||||
self.feedrate = feedrate
|
||||
|
|
@ -29,6 +30,7 @@ class SliceIntrinsics:
|
|||
self.extrusion_rate = extrusion_rate
|
||||
self.retraction = retraction
|
||||
self.lift = lift
|
||||
self.retraction_feedrate = retraction_feedrate
|
||||
|
||||
class InputNode(SliceNode):
|
||||
input_type: str = ""
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class SLICENODE_PathsToGcode(SliceNode):
|
|||
pather.travel_feedrate = intrinsics.travel_feedrate
|
||||
pather.extrusion_rate = intrinsics.extrusion_rate
|
||||
pather.retraction = intrinsics.retraction
|
||||
pather.retraction_feedrate = getattr(intrinsics, "retraction_feedrate", 0.0)
|
||||
pather.lift = intrinsics.lift
|
||||
|
||||
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", "Extrusion Rate").value = 0.035
|
||||
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
|
||||
|
||||
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(
|
||||
bed_x=self.resolve("Bed X"),
|
||||
bed_y=self.resolve("Bed Y"),
|
||||
|
|
@ -296,5 +303,6 @@ class SLICENODE_InputIntrinsics(SliceNode):
|
|||
travel_feedrate=self.resolve("Travel Feedrate"),
|
||||
extrusion_rate=self.resolve("Extrusion Rate"),
|
||||
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())
|
||||
slices = slices.transform(mat_inv)
|
||||
|
||||
slices = slices.align_seams(mode="sharpest")
|
||||
|
||||
return slices
|
||||
|
||||
@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
|
||||
|
||||
|
||||
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]):
|
||||
groups: list[int]
|
||||
extrusion_rate: list[np.ndarray]
|
||||
|
|
@ -398,6 +462,105 @@ class SlicePaths(list[np.ndarray]):
|
|||
def normalize_groups(self) -> None:
|
||||
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]]:
|
||||
if not models:
|
||||
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)])
|
||||
# Close loop by appending the first point for internal consistency
|
||||
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
||||
fpath_3d = align_path_seam_sharpest(fpath_3d)
|
||||
res.append(fpath_3d)
|
||||
|
||||
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)])
|
||||
# Re-close loop
|
||||
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
||||
fpath_3d = align_path_seam_sharpest(fpath_3d)
|
||||
paths_list.append(fpath_3d)
|
||||
groups_list.append(group_id)
|
||||
|
||||
|
|
@ -1391,6 +1556,7 @@ class Pather(object):
|
|||
self.first_layer_height: float | None = None
|
||||
self._layer_width: float | None = None
|
||||
self.retraction: float = 0.0
|
||||
self.retraction_feedrate: float = 0.0
|
||||
self.lift: float = 0.0
|
||||
|
||||
@property
|
||||
|
|
@ -1434,6 +1600,14 @@ class Pather(object):
|
|||
dz = p2[2] - p1[2]
|
||||
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] = []
|
||||
|
||||
if include_header:
|
||||
|
|
@ -1442,6 +1616,7 @@ class Pather(object):
|
|||
f";TYPE:Custom",
|
||||
f";DEFAULT_FEEDRATE={self.feedrate}",
|
||||
f";DEFAULT_TRAVEL_FEEDRATE={self.travel_feedrate}",
|
||||
f";DEFAULT_RETRACTION_FEEDRATE={retraction_fr_val}",
|
||||
f";DEFAULT_EXTRUSION_RATE={self.extrusion_rate}",
|
||||
f";DEFAULT_LAYER_HEIGHT={self.layer_height:.6f}",
|
||||
]
|
||||
|
|
@ -1489,8 +1664,8 @@ class Pather(object):
|
|||
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
|
||||
gcode.append(f"G1 E-{retraction_val:.5f} F{retraction_fr_val:.3f}")
|
||||
active_feedrate = retraction_fr_val
|
||||
|
||||
# 2. Lift Z if lift is enabled
|
||||
if lift_val > 0.0:
|
||||
|
|
@ -1515,8 +1690,8 @@ class Pather(object):
|
|||
|
||||
# 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
|
||||
gcode.append(f"G1 E{retraction_val:.5f} F{retraction_fr_val:.3f}")
|
||||
active_feedrate = retraction_fr_val
|
||||
else:
|
||||
if abs(z0 - last_z) > 0.001:
|
||||
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_at_range()`: Top-level slicing function that iterates through Z-heights.
|
||||
- `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.
|
||||
- `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.
|
||||
|
|
|
|||
|
|
@ -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 Y**: Y 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.
|
||||
|
||||
### 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.
|
||||
- **Angle**: Rotation angle in degrees.
|
||||
- **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 Curve**: Buffers open/closed curves using Shapely, allowing open path offsetting.
|
||||
- **Cap Style**: Style of the curve ends (Round, Flat, Square).
|
||||
|
|
|
|||
|
|
@ -401,20 +401,28 @@ class TestSlice(unittest.TestCase):
|
|||
self.assertNotIn("E-", gcode)
|
||||
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.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)
|
||||
# Retraction should be present before travel using default feedrate 1200.0
|
||||
self.assertIn("G1 E-1.20000 F1200.000", 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)
|
||||
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)
|
||||
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, "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__':
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue