float
This commit is contained in:
parent
978dfcbe1d
commit
b0943f2ca9
3 changed files with 210 additions and 59 deletions
|
|
@ -354,7 +354,7 @@ class SLICENODE_PathOffset(SliceNode):
|
|||
|
||||
if not paths: return SlicePaths()
|
||||
|
||||
return paths.offset(np.array(offset, dtype=np.float32))
|
||||
return paths.offset(np.array(offset, dtype=np.float64))
|
||||
|
||||
@register_class
|
||||
class SLICENODE_RotatePath(SliceNode):
|
||||
|
|
@ -373,7 +373,7 @@ class SLICENODE_RotatePath(SliceNode):
|
|||
|
||||
if not paths: return SlicePaths()
|
||||
|
||||
center_vec = np.array(center, dtype=np.float32) if center and any(center) else None
|
||||
center_vec = np.array(center, dtype=np.float64) if center and any(center) else None
|
||||
|
||||
return paths.rotate(angle, center=center_vec)
|
||||
|
||||
|
|
@ -517,8 +517,10 @@ class SLICENODE_PathOperations(SliceNodeJoiner):
|
|||
|
||||
inputs_data: list[SlicePaths] = []
|
||||
for input_socket in self.inputs:
|
||||
paths: SlicePaths | None = resolve_input(input_socket)
|
||||
if paths is not None:
|
||||
if hasattr(input_socket, "is_linked") and not input_socket.is_linked:
|
||||
continue
|
||||
paths: Any = resolve_input(input_socket)
|
||||
if isinstance(paths, SlicePaths) and len(paths) > 0:
|
||||
inputs_data.append(paths)
|
||||
|
||||
if not inputs_data:
|
||||
|
|
|
|||
135
core/slice.py
135
core/slice.py
|
|
@ -77,8 +77,8 @@ def _transfer_group_attributes(src_sp: SlicePaths, res_paths: list[np.ndarray],
|
|||
M = len(p)
|
||||
src_indices = src_group_map.get(g, [])
|
||||
if not src_indices and len(src_sp) > 0:
|
||||
z = p[0, 2] if len(p) > 0 else 0.0
|
||||
src_indices = [min(range(len(src_sp)), key=lambda i: abs(src_sp[i][0, 2] - z) if len(src_sp[i]) > 0 else float('inf'))]
|
||||
z_um = int(round(p[0, 2] * SCALE)) if len(p) > 0 else 0
|
||||
src_indices = [min(range(len(src_sp)), key=lambda i: abs(int(round(src_sp[i][0, 2] * SCALE)) - z_um) if len(src_sp[i]) > 0 else float('inf'))]
|
||||
|
||||
if src_indices:
|
||||
src_i = src_indices[0]
|
||||
|
|
@ -336,11 +336,13 @@ class SlicePaths(list[np.ndarray]):
|
|||
if not paths_list:
|
||||
return SlicePaths()
|
||||
|
||||
base = primary_source if primary_source is not None else paths_list[0]
|
||||
base = primary_source if primary_source is not None else next((sp for sp in paths_list if len(sp) > 0), paths_list[0])
|
||||
merged = base.copy_with([])
|
||||
|
||||
merged_transforms = []
|
||||
for sp in paths_list:
|
||||
if not sp:
|
||||
continue
|
||||
merged.extend_with_groups(sp)
|
||||
for t in getattr(sp, "transforms", []):
|
||||
if t not in merged_transforms:
|
||||
|
|
@ -355,18 +357,25 @@ class SlicePaths(list[np.ndarray]):
|
|||
if not paths_list:
|
||||
return SlicePaths()
|
||||
|
||||
base = primary_source if primary_source is not None else paths_list[0]
|
||||
non_empty = [sp for sp in paths_list if len(sp) > 0]
|
||||
if not non_empty:
|
||||
return SlicePaths()
|
||||
|
||||
base = primary_source if primary_source is not None else non_empty[0]
|
||||
|
||||
all_groups = set()
|
||||
grouped_inputs = []
|
||||
for p_list in paths_list:
|
||||
group_z: dict[int, int] = {}
|
||||
for p_list in non_empty:
|
||||
d = {}
|
||||
for i, g in enumerate(p_list.groups):
|
||||
all_groups.add(g)
|
||||
d.setdefault(g, []).append((p_list[i], p_list.extrusion_rate[i], p_list.feedrate[i], {k: v[i] for k, v in p_list.extra_attributes.items()}))
|
||||
if g not in group_z and len(p_list[i]) > 0:
|
||||
group_z[g] = int(round(p_list[i][0, 2] * SCALE))
|
||||
grouped_inputs.append(d)
|
||||
|
||||
sorted_groups = sorted(list(all_groups))
|
||||
sorted_groups = sorted(list(all_groups), key=lambda g: (group_z.get(g, 0), g))
|
||||
|
||||
merged = base.copy_with([])
|
||||
for g in sorted_groups:
|
||||
|
|
@ -378,7 +387,7 @@ class SlicePaths(list[np.ndarray]):
|
|||
merged_transforms = []
|
||||
merged_names = []
|
||||
merged_objs = []
|
||||
for sp in paths_list:
|
||||
for sp in non_empty:
|
||||
obj_info = getattr(sp, "objects_info", None)
|
||||
if obj_info:
|
||||
for obj_entry in obj_info.get("objects", []):
|
||||
|
|
@ -443,15 +452,17 @@ class SlicePaths(list[np.ndarray]):
|
|||
angle_rad = np.radians(angle_deg)
|
||||
c, s = np.cos(angle_rad), np.sin(angle_rad)
|
||||
rot_mat = np.array([
|
||||
[c, -s, 0],
|
||||
[s, c, 0],
|
||||
[0, 0, 1]
|
||||
], dtype=np.float32)
|
||||
[c, -s, 0.0],
|
||||
[s, c, 0.0],
|
||||
[0.0, 0.0, 1.0]
|
||||
], dtype=np.float64)
|
||||
|
||||
new_paths = []
|
||||
for p in self:
|
||||
# Shift to center, rotate, shift back
|
||||
# Shift to center, rotate, shift back, preserving exact original Z
|
||||
new_p = (p - center) @ rot_mat.T + center
|
||||
if len(p) > 0 and p.shape[1] >= 3:
|
||||
new_p[:, 2] = p[:, 2]
|
||||
new_paths.append(new_p)
|
||||
|
||||
res = self.copy_with(new_paths)
|
||||
|
|
@ -467,18 +478,16 @@ class SlicePaths(list[np.ndarray]):
|
|||
if not self:
|
||||
return np.zeros(3)
|
||||
|
||||
# Find minimum group index
|
||||
min_g = min(self.groups)
|
||||
first_layer_indices = [i for i, g in enumerate(self.groups) if g == min_g]
|
||||
# Find true minimum Z layer in integer microns across non-empty paths
|
||||
min_z_um = min((int(round(p[0, 2] * SCALE)) for p in self if len(p) > 0), default=None)
|
||||
if min_z_um is None:
|
||||
return np.zeros(3)
|
||||
|
||||
all_pts = []
|
||||
for i in first_layer_indices:
|
||||
all_pts.append(self[i])
|
||||
|
||||
if not all_pts:
|
||||
first_layer_pts = [p for p in self if len(p) > 0 and int(round(p[0, 2] * SCALE)) == min_z_um]
|
||||
if not first_layer_pts:
|
||||
return np.zeros(3)
|
||||
|
||||
pts = np.concatenate(all_pts)
|
||||
pts = np.concatenate(first_layer_pts)
|
||||
min_pts = np.min(pts, axis=0)
|
||||
max_pts = np.max(pts, axis=0)
|
||||
return (min_pts + max_pts) / 2.0
|
||||
|
|
@ -1045,7 +1054,7 @@ def boolean_slice(subject: SlicePaths, object: SlicePaths, operation: str = 'UNI
|
|||
group += 1
|
||||
continue
|
||||
|
||||
z = subject_slices[0][0, 2]
|
||||
z = float(k) / SCALE
|
||||
|
||||
pc = pyclipper.Pyclipper() # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
|
|
@ -1257,12 +1266,12 @@ def minimum_coverage_slice(
|
|||
paths in that layer. If only one path (or single shape with holes) exists on a layer,
|
||||
it preserves that path/shape intact.
|
||||
"""
|
||||
if not paths_list:
|
||||
valid_paths = [sp for sp in paths_list if isinstance(sp, SlicePaths) and len(sp) > 0]
|
||||
if not valid_paths:
|
||||
return SlicePaths()
|
||||
|
||||
primary = next((sp for sp in paths_list if len(sp) > 0), paths_list[0])
|
||||
if not any(len(sp) > 0 for sp in paths_list):
|
||||
return SlicePaths()
|
||||
paths_list = valid_paths
|
||||
primary = paths_list[0]
|
||||
|
||||
all_z_keys: set[int] = set()
|
||||
input_layer_maps: list[dict[int, list[np.ndarray]]] = []
|
||||
|
|
@ -1397,16 +1406,15 @@ def maximum_coverage_slice(
|
|||
Computes the Maximum Coverage (Union) across multiple SlicePaths inputs (or within a single SlicePaths input).
|
||||
For each layer (grouped by integer micron Z), it calculates the union of all paths.
|
||||
"""
|
||||
if not paths_list:
|
||||
valid_paths = [sp for sp in paths_list if isinstance(sp, SlicePaths) and len(sp) > 0]
|
||||
if not valid_paths:
|
||||
return SlicePaths()
|
||||
|
||||
primary = next((sp for sp in paths_list if len(sp) > 0), paths_list[0])
|
||||
non_empty = [sp for sp in paths_list if len(sp) > 0]
|
||||
if not non_empty:
|
||||
return SlicePaths()
|
||||
paths_list = valid_paths
|
||||
primary = paths_list[0]
|
||||
|
||||
if len(non_empty) == 1:
|
||||
sp = non_empty[0]
|
||||
if len(paths_list) == 1:
|
||||
sp = paths_list[0]
|
||||
layer_map: dict[int, list[np.ndarray]] = {}
|
||||
for p in sp:
|
||||
if len(p) < 2:
|
||||
|
|
@ -1464,8 +1472,8 @@ def maximum_coverage_slice(
|
|||
|
||||
return results
|
||||
|
||||
accumulated = non_empty[0]
|
||||
for sp in non_empty[1:]:
|
||||
accumulated = paths_list[0]
|
||||
for sp in paths_list[1:]:
|
||||
accumulated = boolean_slice(
|
||||
accumulated,
|
||||
sp,
|
||||
|
|
@ -1486,7 +1494,7 @@ def intersection_slice(
|
|||
|
||||
accumulated = paths_list[0]
|
||||
for sp in paths_list[1:]:
|
||||
if not accumulated:
|
||||
if not sp or not accumulated:
|
||||
return SlicePaths()
|
||||
accumulated = boolean_slice(
|
||||
accumulated,
|
||||
|
|
@ -1494,6 +1502,8 @@ def intersection_slice(
|
|||
operation='INTERSECTION',
|
||||
connect_along_boundary=connect_along_boundary
|
||||
)
|
||||
if not accumulated:
|
||||
return SlicePaths()
|
||||
return accumulated
|
||||
|
||||
def subtract_slice(
|
||||
|
|
@ -1510,6 +1520,8 @@ def subtract_slice(
|
|||
for sp in paths_list[1:]:
|
||||
if not accumulated:
|
||||
return SlicePaths()
|
||||
if not sp:
|
||||
continue
|
||||
accumulated = boolean_slice(
|
||||
accumulated,
|
||||
sp,
|
||||
|
|
@ -1530,6 +1542,11 @@ def xor_slice(
|
|||
|
||||
accumulated = paths_list[0]
|
||||
for sp in paths_list[1:]:
|
||||
if not sp:
|
||||
continue
|
||||
if not accumulated:
|
||||
accumulated = sp
|
||||
continue
|
||||
accumulated = boolean_slice(
|
||||
accumulated,
|
||||
sp,
|
||||
|
|
@ -1611,7 +1628,7 @@ def buffer_curve_slice(
|
|||
group_id = 0
|
||||
for z_key in sorted(groups.keys()):
|
||||
layer_paths = groups[z_key]
|
||||
z = layer_paths[0][0, 2]
|
||||
z = float(z_key) / SCALE
|
||||
|
||||
for p in layer_paths:
|
||||
if len(p) < 2:
|
||||
|
|
@ -1685,7 +1702,7 @@ def offset_curve_slice(
|
|||
group_id = 0
|
||||
for z_key in sorted(groups.keys()):
|
||||
layer_paths = groups[z_key]
|
||||
z = layer_paths[0][0, 2]
|
||||
z = float(z_key) / SCALE
|
||||
|
||||
for p in layer_paths:
|
||||
if len(p) < 2:
|
||||
|
|
@ -1812,7 +1829,7 @@ def get_infill_mask_slice(slice_paths: list[np.ndarray]) -> tuple[list[np.ndarra
|
|||
if not layer_paths:
|
||||
continue
|
||||
|
||||
z = layer_paths[0][0, 2]
|
||||
z = float(z_key) / SCALE
|
||||
|
||||
# Convert layer paths to a single Shapely geometry (handling holes and multiple islands)
|
||||
geom = paths_to_shapely_polygon(layer_paths)
|
||||
|
|
@ -1842,7 +1859,10 @@ def get_overhang_mask_slice(slice_paths: list[np.ndarray], groups: tuple[int, ..
|
|||
for p, g in zip(slice_paths, groups):
|
||||
group_map.setdefault(g, []).append(p)
|
||||
|
||||
sorted_groups = sorted(group_map.keys())
|
||||
sorted_groups = sorted(
|
||||
group_map.keys(),
|
||||
key=lambda g: int(round(group_map[g][0][0, 2] * SCALE)) if group_map[g] and len(group_map[g][0]) > 0 else g
|
||||
)
|
||||
if len(sorted_groups) < 2:
|
||||
# If there's only 1 layer, there are no overhangs under any layer
|
||||
return [], []
|
||||
|
|
@ -1858,7 +1878,7 @@ def get_overhang_mask_slice(slice_paths: list[np.ndarray], groups: tuple[int, ..
|
|||
# Iterate from top group down to bottom group
|
||||
for i in range(len(sorted_groups) - 1, -1, -1):
|
||||
g = sorted_groups[i]
|
||||
z = group_map[g][0][0, 2] if group_map[g] and len(group_map[g][0]) > 0 else 0.0
|
||||
z = float(int(round(group_map[g][0][0, 2] * SCALE))) / SCALE if group_map[g] and len(group_map[g][0]) > 0 else 0.0
|
||||
geom_i = layer_geoms[g]
|
||||
|
||||
# Calculate raw support needed at layer g: union of all layers above g, minus geom_i.
|
||||
|
|
@ -1913,7 +1933,7 @@ def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round',
|
|||
group_id = 0
|
||||
for z_key in sorted(groups.keys()):
|
||||
layer_paths = groups[z_key]
|
||||
z = layer_paths[0][0, 2]
|
||||
z = float(z_key) / SCALE
|
||||
|
||||
pco.Clear()
|
||||
scaled_paths = []
|
||||
|
|
@ -2017,11 +2037,14 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
|||
output_paths: list[np.ndarray] = []
|
||||
output_groups: list[int] = []
|
||||
|
||||
# Sort groups by Z to determine layer index for alternating rectilinear infill
|
||||
unique_groups = sorted(groups.keys())
|
||||
# Sort groups by Z in integer microns to determine layer index for alternating rectilinear infill
|
||||
unique_groups = sorted(
|
||||
groups.keys(),
|
||||
key=lambda g: int(round(groups[g][0][0, 2] * SCALE)) if groups[g] and len(groups[g][0]) > 0 else g
|
||||
)
|
||||
group_to_layer_index = {g: i for i, g in enumerate(unique_groups)}
|
||||
|
||||
for g in groups.keys():
|
||||
for g in unique_groups:
|
||||
layer_idx = group_to_layer_index[g]
|
||||
|
||||
if infill_type == 'GRID':
|
||||
|
|
@ -2034,7 +2057,7 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
|||
current_grid_paths = []
|
||||
|
||||
layer_paths = groups[g]
|
||||
z = layer_paths[0][0, 2]
|
||||
z = float(int(round(layer_paths[0][0, 2] * SCALE))) / SCALE if layer_paths and len(layer_paths[0]) > 0 else 0.0
|
||||
|
||||
# De-rotate layer paths
|
||||
offset_2d = np.array([global_center[0], global_center[1], 0])
|
||||
|
|
@ -2069,10 +2092,11 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
|||
return output_paths, output_groups
|
||||
|
||||
def group_segments_by_z(segments: np.ndarray) -> dict[int, np.ndarray]:
|
||||
z_values: np.ndarray = segments[:, 0, 2]
|
||||
unique_z: np.ndarray = np.unique(z_values)
|
||||
groups: dict[int, np.ndarray] = {int(round(z * SCALE)): segments[z_values == z] for z in unique_z}
|
||||
return groups
|
||||
if len(segments) == 0:
|
||||
return {}
|
||||
z_um = np.round(segments[:, 0, 2] * SCALE).astype(np.int64)
|
||||
unique_z_um = np.unique(z_um)
|
||||
return {int(z): segments[z_um == z] for z in unique_z_um}
|
||||
|
||||
############################################################
|
||||
|
||||
|
|
@ -2246,7 +2270,7 @@ class Pather(object):
|
|||
except (TypeError, ValueError):
|
||||
lift_val = 0.0
|
||||
|
||||
last_z = -1.0
|
||||
last_z_um: int | None = None
|
||||
max_z = 0
|
||||
last_nozzle_z = 0.0
|
||||
|
||||
|
|
@ -2260,6 +2284,7 @@ class Pather(object):
|
|||
extras = {prefix: vals[i] for prefix, vals in self.extra_attributes.items()}
|
||||
|
||||
x0, y0, z0 = coords[0]
|
||||
z0_um = int(round(z0 * SCALE))
|
||||
|
||||
active_feedrate = self.feedrate
|
||||
if i > 0:
|
||||
|
|
@ -2279,11 +2304,11 @@ class Pather(object):
|
|||
active_feedrate = travel_fr
|
||||
|
||||
# 4. Handle Z alignment (layer change and/or un-lift)
|
||||
if abs(z0 - last_z) > 0.001:
|
||||
if last_z_um is None or z0_um != last_z_um:
|
||||
gcode.append(f";BEFORE_LAYER_CHANGE")
|
||||
gcode.append(f"G0 Z{z0:.3f} F720")
|
||||
gcode.append(f";AFTER_LAYER_CHANGE")
|
||||
last_z = z0
|
||||
last_z_um = z0_um
|
||||
active_feedrate = 720.0
|
||||
elif lift_val > 0.0:
|
||||
gcode.append(f"G0 Z{z0:.3f} F720")
|
||||
|
|
@ -2294,11 +2319,11 @@ class Pather(object):
|
|||
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:
|
||||
if last_z_um is None or z0_um != last_z_um:
|
||||
gcode.append(f";BEFORE_LAYER_CHANGE")
|
||||
gcode.append(f"G0 Z{z0:.3f} F720")
|
||||
gcode.append(f";AFTER_LAYER_CHANGE")
|
||||
last_z = z0
|
||||
last_z_um = z0_um
|
||||
active_feedrate = 720.0
|
||||
|
||||
old_fr = f_rate[0] if f_rate[0] > 0 else self.travel_feedrate
|
||||
|
|
|
|||
|
|
@ -1181,7 +1181,9 @@ class TestSlice(unittest.TestCase):
|
|||
|
||||
node = SLICENODE_PathOperations()
|
||||
mock_socket1 = MagicMock()
|
||||
mock_socket1.is_linked = True
|
||||
mock_socket2 = MagicMock()
|
||||
mock_socket2.is_linked = True
|
||||
node.inputs = [mock_socket1, mock_socket2]
|
||||
|
||||
# Mock resolve_input to return sp1 and sp2
|
||||
|
|
@ -1207,6 +1209,84 @@ class TestSlice(unittest.TestCase):
|
|||
node.operation = 'XOR'
|
||||
self.assertEqual(len(node.evaluate_output("Paths")), 2)
|
||||
|
||||
def test_path_operations_node_ignores_unplugged_and_invalid_sockets(self):
|
||||
from NodeSlicer.core.slice import SlicePaths
|
||||
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathOperations
|
||||
|
||||
p1 = np.array([[0, 0, 1.0], [10, 0, 1.0], [10, 10, 1.0], [0, 10, 1.0], [0, 0, 1.0]])
|
||||
p2 = np.array([[5, 0, 1.0], [15, 0, 1.0], [15, 10, 1.0], [5, 10, 1.0], [5, 0, 1.0]])
|
||||
sp1 = SlicePaths([p1], groups=[0])
|
||||
sp2 = SlicePaths([p2], groups=[0])
|
||||
|
||||
node = SLICENODE_PathOperations()
|
||||
|
||||
# Case 1: 1 linked socket, 1 unlinked trailing socket (typical Blender node state)
|
||||
sock_linked = MagicMock()
|
||||
sock_linked.is_linked = True
|
||||
sock_unlinked = MagicMock()
|
||||
sock_unlinked.is_linked = False
|
||||
|
||||
node.inputs = [sock_linked, sock_unlinked]
|
||||
|
||||
with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", return_value=sp1):
|
||||
# When only 1 socket is plugged in, operations should NOT be broken by the unlinked socket
|
||||
node.operation = 'MIN_COVERAGE'
|
||||
self.assertEqual(len(node.evaluate_output("Paths")), 1)
|
||||
|
||||
node.operation = 'MAX_COVERAGE'
|
||||
self.assertEqual(len(node.evaluate_output("Paths")), 1)
|
||||
|
||||
node.operation = 'INTERSECTION'
|
||||
self.assertEqual(len(node.evaluate_output("Paths")), 1)
|
||||
|
||||
node.operation = 'SUBTRACT'
|
||||
self.assertEqual(len(node.evaluate_output("Paths")), 1)
|
||||
|
||||
node.operation = 'XOR'
|
||||
self.assertEqual(len(node.evaluate_output("Paths")), 1)
|
||||
|
||||
node.operation = 'ZIP'
|
||||
self.assertEqual(len(node.evaluate_output("Paths")), 1)
|
||||
|
||||
node.operation = 'EXTEND'
|
||||
self.assertEqual(len(node.evaluate_output("Paths")), 1)
|
||||
|
||||
# Case 2: Mixed sockets with unlinked and invalid (None, non-SlicePaths, empty SlicePaths)
|
||||
sock1 = MagicMock(is_linked=True)
|
||||
sock_unlinked_mid = MagicMock(is_linked=False)
|
||||
sock_invalid_type = MagicMock(is_linked=True)
|
||||
sock_empty_paths = MagicMock(is_linked=True)
|
||||
sock2 = MagicMock(is_linked=True)
|
||||
sock_unlinked_end = MagicMock(is_linked=False)
|
||||
|
||||
node.inputs = [sock1, sock_unlinked_mid, sock_invalid_type, sock_empty_paths, sock2, sock_unlinked_end]
|
||||
|
||||
def mock_resolve(sock):
|
||||
if sock == sock1:
|
||||
return sp1
|
||||
elif sock == sock_invalid_type:
|
||||
return "not_a_slice_path"
|
||||
elif sock == sock_empty_paths:
|
||||
return SlicePaths()
|
||||
elif sock == sock2:
|
||||
return sp2
|
||||
return None
|
||||
|
||||
with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", side_effect=mock_resolve):
|
||||
node.operation = 'INTERSECTION'
|
||||
res = node.evaluate_output("Paths")
|
||||
self.assertEqual(len(res), 1)
|
||||
|
||||
node.operation = 'ZIP'
|
||||
res_zip = node.evaluate_output("Paths")
|
||||
self.assertEqual(len(res_zip), 2)
|
||||
|
||||
# Case 3: All sockets unlinked
|
||||
node.inputs = [sock_unlinked, sock_unlinked_end]
|
||||
res_empty = node.evaluate_output("Paths")
|
||||
self.assertIsInstance(res_empty, SlicePaths)
|
||||
self.assertEqual(len(res_empty), 0)
|
||||
|
||||
def test_path_operations_single_input_min_coverage(self):
|
||||
from NodeSlicer.core.slice import SlicePaths
|
||||
# Single input with 1 path per layer (Z=1.0 and Z=2.0)
|
||||
|
|
@ -1450,6 +1530,50 @@ class TestSlice(unittest.TestCase):
|
|||
self.assertEqual(infilled.first_layer_height, 0.3)
|
||||
self.assertEqual(infilled.name, "Part1")
|
||||
|
||||
def test_group_segments_by_z_float_drift(self):
|
||||
from NodeSlicer.core.slice import group_segments_by_z
|
||||
# Segments with slight float drift on the same layer
|
||||
seg1 = np.array([[[0.0, 0.0, 0.20000000000000004], [10.0, 0.0, 0.20000000000000004]]])
|
||||
seg2 = np.array([[[10.0, 0.0, 0.20000000000000007], [10.0, 10.0, 0.20000000000000007]]])
|
||||
seg3 = np.array([[[0.0, 0.0, 0.40000000000000002], [10.0, 0.0, 0.40000000000000002]]])
|
||||
all_segs = np.vstack([seg1, seg2, seg3])
|
||||
|
||||
groups = group_segments_by_z(all_segs)
|
||||
self.assertEqual(len(groups), 2)
|
||||
self.assertIn(200, groups)
|
||||
self.assertIn(400, groups)
|
||||
# Key 200 must have both seg1 and seg2
|
||||
self.assertEqual(len(groups[200]), 2)
|
||||
self.assertEqual(len(groups[400]), 1)
|
||||
|
||||
def test_pather_z_layer_change_drift(self):
|
||||
from NodeSlicer.core.slice import SlicePaths, Pather
|
||||
# 2 paths on layer 1 with tiny drift, 1 path on layer 2
|
||||
p1 = np.array([[0, 0, 0.20000000000000004], [10, 0, 0.20000000000000004]])
|
||||
p2 = np.array([[10, 0, 0.20000000000000007], [10, 10, 0.20000000000000007]])
|
||||
p3 = np.array([[0, 0, 0.4], [10, 0, 0.4]])
|
||||
sp = SlicePaths([p1, p2, p3], groups=[0, 0, 1])
|
||||
|
||||
pather = Pather(sp)
|
||||
gcode = pather.to_gcode(include_header=False)
|
||||
lines = gcode.splitlines()
|
||||
before_layer_changes = [l for l in lines if l == ";BEFORE_LAYER_CHANGE"]
|
||||
# Exactly 2 layer changes (one for Z=0.2, one for Z=0.4)
|
||||
self.assertEqual(len(before_layer_changes), 2)
|
||||
|
||||
def test_get_first_layer_midpoint_out_of_order_groups(self):
|
||||
from NodeSlicer.core.slice import SlicePaths
|
||||
# Top layer first, bottom layer second
|
||||
top_p = np.array([[0, 0, 1.0], [10, 0, 1.0], [10, 10, 1.0], [0, 10, 1.0], [0, 0, 1.0]])
|
||||
bottom_p = np.array([[10, 10, 0.2], [20, 10, 0.2], [20, 20, 0.2], [10, 20, 0.2], [10, 10, 0.2]])
|
||||
sp = SlicePaths([top_p, bottom_p], groups=[99, 10])
|
||||
|
||||
midpoint = sp.get_first_layer_midpoint()
|
||||
# Midpoint of bottom_p is [15.0, 15.0, 0.2]
|
||||
self.assertAlmostEqual(midpoint[0], 15.0)
|
||||
self.assertAlmostEqual(midpoint[1], 15.0)
|
||||
self.assertAlmostEqual(midpoint[2], 0.2)
|
||||
|
||||
|
||||
class TestCodeReviewBugFixes(unittest.TestCase):
|
||||
def test_ndarray_lru_cache_empty_list_and_dict(self):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue