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()
|
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
|
@register_class
|
||||||
class SLICENODE_RotatePath(SliceNode):
|
class SLICENODE_RotatePath(SliceNode):
|
||||||
|
|
@ -373,7 +373,7 @@ class SLICENODE_RotatePath(SliceNode):
|
||||||
|
|
||||||
if not paths: return SlicePaths()
|
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)
|
return paths.rotate(angle, center=center_vec)
|
||||||
|
|
||||||
|
|
@ -517,8 +517,10 @@ class SLICENODE_PathOperations(SliceNodeJoiner):
|
||||||
|
|
||||||
inputs_data: list[SlicePaths] = []
|
inputs_data: list[SlicePaths] = []
|
||||||
for input_socket in self.inputs:
|
for input_socket in self.inputs:
|
||||||
paths: SlicePaths | None = resolve_input(input_socket)
|
if hasattr(input_socket, "is_linked") and not input_socket.is_linked:
|
||||||
if paths is not None:
|
continue
|
||||||
|
paths: Any = resolve_input(input_socket)
|
||||||
|
if isinstance(paths, SlicePaths) and len(paths) > 0:
|
||||||
inputs_data.append(paths)
|
inputs_data.append(paths)
|
||||||
|
|
||||||
if not inputs_data:
|
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)
|
M = len(p)
|
||||||
src_indices = src_group_map.get(g, [])
|
src_indices = src_group_map.get(g, [])
|
||||||
if not src_indices and len(src_sp) > 0:
|
if not src_indices and len(src_sp) > 0:
|
||||||
z = p[0, 2] if len(p) > 0 else 0.0
|
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(src_sp[i][0, 2] - z) if len(src_sp[i]) > 0 else float('inf'))]
|
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:
|
if src_indices:
|
||||||
src_i = src_indices[0]
|
src_i = src_indices[0]
|
||||||
|
|
@ -336,11 +336,13 @@ class SlicePaths(list[np.ndarray]):
|
||||||
if not paths_list:
|
if not paths_list:
|
||||||
return SlicePaths()
|
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 = base.copy_with([])
|
||||||
|
|
||||||
merged_transforms = []
|
merged_transforms = []
|
||||||
for sp in paths_list:
|
for sp in paths_list:
|
||||||
|
if not sp:
|
||||||
|
continue
|
||||||
merged.extend_with_groups(sp)
|
merged.extend_with_groups(sp)
|
||||||
for t in getattr(sp, "transforms", []):
|
for t in getattr(sp, "transforms", []):
|
||||||
if t not in merged_transforms:
|
if t not in merged_transforms:
|
||||||
|
|
@ -355,18 +357,25 @@ class SlicePaths(list[np.ndarray]):
|
||||||
if not paths_list:
|
if not paths_list:
|
||||||
return SlicePaths()
|
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()
|
all_groups = set()
|
||||||
grouped_inputs = []
|
grouped_inputs = []
|
||||||
for p_list in paths_list:
|
group_z: dict[int, int] = {}
|
||||||
|
for p_list in non_empty:
|
||||||
d = {}
|
d = {}
|
||||||
for i, g in enumerate(p_list.groups):
|
for i, g in enumerate(p_list.groups):
|
||||||
all_groups.add(g)
|
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()}))
|
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)
|
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([])
|
merged = base.copy_with([])
|
||||||
for g in sorted_groups:
|
for g in sorted_groups:
|
||||||
|
|
@ -378,7 +387,7 @@ class SlicePaths(list[np.ndarray]):
|
||||||
merged_transforms = []
|
merged_transforms = []
|
||||||
merged_names = []
|
merged_names = []
|
||||||
merged_objs = []
|
merged_objs = []
|
||||||
for sp in paths_list:
|
for sp in non_empty:
|
||||||
obj_info = getattr(sp, "objects_info", None)
|
obj_info = getattr(sp, "objects_info", None)
|
||||||
if obj_info:
|
if obj_info:
|
||||||
for obj_entry in obj_info.get("objects", []):
|
for obj_entry in obj_info.get("objects", []):
|
||||||
|
|
@ -443,15 +452,17 @@ class SlicePaths(list[np.ndarray]):
|
||||||
angle_rad = np.radians(angle_deg)
|
angle_rad = np.radians(angle_deg)
|
||||||
c, s = np.cos(angle_rad), np.sin(angle_rad)
|
c, s = np.cos(angle_rad), np.sin(angle_rad)
|
||||||
rot_mat = np.array([
|
rot_mat = np.array([
|
||||||
[c, -s, 0],
|
[c, -s, 0.0],
|
||||||
[s, c, 0],
|
[s, c, 0.0],
|
||||||
[0, 0, 1]
|
[0.0, 0.0, 1.0]
|
||||||
], dtype=np.float32)
|
], dtype=np.float64)
|
||||||
|
|
||||||
new_paths = []
|
new_paths = []
|
||||||
for p in self:
|
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
|
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)
|
new_paths.append(new_p)
|
||||||
|
|
||||||
res = self.copy_with(new_paths)
|
res = self.copy_with(new_paths)
|
||||||
|
|
@ -467,18 +478,16 @@ class SlicePaths(list[np.ndarray]):
|
||||||
if not self:
|
if not self:
|
||||||
return np.zeros(3)
|
return np.zeros(3)
|
||||||
|
|
||||||
# Find minimum group index
|
# Find true minimum Z layer in integer microns across non-empty paths
|
||||||
min_g = min(self.groups)
|
min_z_um = min((int(round(p[0, 2] * SCALE)) for p in self if len(p) > 0), default=None)
|
||||||
first_layer_indices = [i for i, g in enumerate(self.groups) if g == min_g]
|
if min_z_um is None:
|
||||||
|
return np.zeros(3)
|
||||||
|
|
||||||
all_pts = []
|
first_layer_pts = [p for p in self if len(p) > 0 and int(round(p[0, 2] * SCALE)) == min_z_um]
|
||||||
for i in first_layer_indices:
|
if not first_layer_pts:
|
||||||
all_pts.append(self[i])
|
|
||||||
|
|
||||||
if not all_pts:
|
|
||||||
return np.zeros(3)
|
return np.zeros(3)
|
||||||
|
|
||||||
pts = np.concatenate(all_pts)
|
pts = np.concatenate(first_layer_pts)
|
||||||
min_pts = np.min(pts, axis=0)
|
min_pts = np.min(pts, axis=0)
|
||||||
max_pts = np.max(pts, axis=0)
|
max_pts = np.max(pts, axis=0)
|
||||||
return (min_pts + max_pts) / 2.0
|
return (min_pts + max_pts) / 2.0
|
||||||
|
|
@ -1045,7 +1054,7 @@ def boolean_slice(subject: SlicePaths, object: SlicePaths, operation: str = 'UNI
|
||||||
group += 1
|
group += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
z = subject_slices[0][0, 2]
|
z = float(k) / SCALE
|
||||||
|
|
||||||
pc = pyclipper.Pyclipper() # pyright: ignore[reportAttributeAccessIssue]
|
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,
|
paths in that layer. If only one path (or single shape with holes) exists on a layer,
|
||||||
it preserves that path/shape intact.
|
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()
|
return SlicePaths()
|
||||||
|
|
||||||
primary = next((sp for sp in paths_list if len(sp) > 0), paths_list[0])
|
paths_list = valid_paths
|
||||||
if not any(len(sp) > 0 for sp in paths_list):
|
primary = paths_list[0]
|
||||||
return SlicePaths()
|
|
||||||
|
|
||||||
all_z_keys: set[int] = set()
|
all_z_keys: set[int] = set()
|
||||||
input_layer_maps: list[dict[int, list[np.ndarray]]] = []
|
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).
|
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.
|
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()
|
return SlicePaths()
|
||||||
|
|
||||||
primary = next((sp for sp in paths_list if len(sp) > 0), paths_list[0])
|
paths_list = valid_paths
|
||||||
non_empty = [sp for sp in paths_list if len(sp) > 0]
|
primary = paths_list[0]
|
||||||
if not non_empty:
|
|
||||||
return SlicePaths()
|
|
||||||
|
|
||||||
if len(non_empty) == 1:
|
if len(paths_list) == 1:
|
||||||
sp = non_empty[0]
|
sp = paths_list[0]
|
||||||
layer_map: dict[int, list[np.ndarray]] = {}
|
layer_map: dict[int, list[np.ndarray]] = {}
|
||||||
for p in sp:
|
for p in sp:
|
||||||
if len(p) < 2:
|
if len(p) < 2:
|
||||||
|
|
@ -1464,8 +1472,8 @@ def maximum_coverage_slice(
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
accumulated = non_empty[0]
|
accumulated = paths_list[0]
|
||||||
for sp in non_empty[1:]:
|
for sp in paths_list[1:]:
|
||||||
accumulated = boolean_slice(
|
accumulated = boolean_slice(
|
||||||
accumulated,
|
accumulated,
|
||||||
sp,
|
sp,
|
||||||
|
|
@ -1486,7 +1494,7 @@ def intersection_slice(
|
||||||
|
|
||||||
accumulated = paths_list[0]
|
accumulated = paths_list[0]
|
||||||
for sp in paths_list[1:]:
|
for sp in paths_list[1:]:
|
||||||
if not accumulated:
|
if not sp or not accumulated:
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
accumulated = boolean_slice(
|
accumulated = boolean_slice(
|
||||||
accumulated,
|
accumulated,
|
||||||
|
|
@ -1494,6 +1502,8 @@ def intersection_slice(
|
||||||
operation='INTERSECTION',
|
operation='INTERSECTION',
|
||||||
connect_along_boundary=connect_along_boundary
|
connect_along_boundary=connect_along_boundary
|
||||||
)
|
)
|
||||||
|
if not accumulated:
|
||||||
|
return SlicePaths()
|
||||||
return accumulated
|
return accumulated
|
||||||
|
|
||||||
def subtract_slice(
|
def subtract_slice(
|
||||||
|
|
@ -1510,6 +1520,8 @@ def subtract_slice(
|
||||||
for sp in paths_list[1:]:
|
for sp in paths_list[1:]:
|
||||||
if not accumulated:
|
if not accumulated:
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
|
if not sp:
|
||||||
|
continue
|
||||||
accumulated = boolean_slice(
|
accumulated = boolean_slice(
|
||||||
accumulated,
|
accumulated,
|
||||||
sp,
|
sp,
|
||||||
|
|
@ -1530,6 +1542,11 @@ def xor_slice(
|
||||||
|
|
||||||
accumulated = paths_list[0]
|
accumulated = paths_list[0]
|
||||||
for sp in paths_list[1:]:
|
for sp in paths_list[1:]:
|
||||||
|
if not sp:
|
||||||
|
continue
|
||||||
|
if not accumulated:
|
||||||
|
accumulated = sp
|
||||||
|
continue
|
||||||
accumulated = boolean_slice(
|
accumulated = boolean_slice(
|
||||||
accumulated,
|
accumulated,
|
||||||
sp,
|
sp,
|
||||||
|
|
@ -1611,7 +1628,7 @@ def buffer_curve_slice(
|
||||||
group_id = 0
|
group_id = 0
|
||||||
for z_key in sorted(groups.keys()):
|
for z_key in sorted(groups.keys()):
|
||||||
layer_paths = groups[z_key]
|
layer_paths = groups[z_key]
|
||||||
z = layer_paths[0][0, 2]
|
z = float(z_key) / SCALE
|
||||||
|
|
||||||
for p in layer_paths:
|
for p in layer_paths:
|
||||||
if len(p) < 2:
|
if len(p) < 2:
|
||||||
|
|
@ -1685,7 +1702,7 @@ def offset_curve_slice(
|
||||||
group_id = 0
|
group_id = 0
|
||||||
for z_key in sorted(groups.keys()):
|
for z_key in sorted(groups.keys()):
|
||||||
layer_paths = groups[z_key]
|
layer_paths = groups[z_key]
|
||||||
z = layer_paths[0][0, 2]
|
z = float(z_key) / SCALE
|
||||||
|
|
||||||
for p in layer_paths:
|
for p in layer_paths:
|
||||||
if len(p) < 2:
|
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:
|
if not layer_paths:
|
||||||
continue
|
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)
|
# Convert layer paths to a single Shapely geometry (handling holes and multiple islands)
|
||||||
geom = paths_to_shapely_polygon(layer_paths)
|
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):
|
for p, g in zip(slice_paths, groups):
|
||||||
group_map.setdefault(g, []).append(p)
|
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 len(sorted_groups) < 2:
|
||||||
# If there's only 1 layer, there are no overhangs under any layer
|
# If there's only 1 layer, there are no overhangs under any layer
|
||||||
return [], []
|
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
|
# Iterate from top group down to bottom group
|
||||||
for i in range(len(sorted_groups) - 1, -1, -1):
|
for i in range(len(sorted_groups) - 1, -1, -1):
|
||||||
g = sorted_groups[i]
|
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]
|
geom_i = layer_geoms[g]
|
||||||
|
|
||||||
# Calculate raw support needed at layer g: union of all layers above g, minus geom_i.
|
# 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
|
group_id = 0
|
||||||
for z_key in sorted(groups.keys()):
|
for z_key in sorted(groups.keys()):
|
||||||
layer_paths = groups[z_key]
|
layer_paths = groups[z_key]
|
||||||
z = layer_paths[0][0, 2]
|
z = float(z_key) / SCALE
|
||||||
|
|
||||||
pco.Clear()
|
pco.Clear()
|
||||||
scaled_paths = []
|
scaled_paths = []
|
||||||
|
|
@ -2017,11 +2037,14 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
||||||
output_paths: list[np.ndarray] = []
|
output_paths: list[np.ndarray] = []
|
||||||
output_groups: list[int] = []
|
output_groups: list[int] = []
|
||||||
|
|
||||||
# Sort groups by Z to determine layer index for alternating rectilinear infill
|
# Sort groups by Z in integer microns to determine layer index for alternating rectilinear infill
|
||||||
unique_groups = sorted(groups.keys())
|
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)}
|
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]
|
layer_idx = group_to_layer_index[g]
|
||||||
|
|
||||||
if infill_type == 'GRID':
|
if infill_type == 'GRID':
|
||||||
|
|
@ -2034,7 +2057,7 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
||||||
current_grid_paths = []
|
current_grid_paths = []
|
||||||
|
|
||||||
layer_paths = groups[g]
|
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
|
# De-rotate layer paths
|
||||||
offset_2d = np.array([global_center[0], global_center[1], 0])
|
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
|
return output_paths, output_groups
|
||||||
|
|
||||||
def group_segments_by_z(segments: np.ndarray) -> dict[int, np.ndarray]:
|
def group_segments_by_z(segments: np.ndarray) -> dict[int, np.ndarray]:
|
||||||
z_values: np.ndarray = segments[:, 0, 2]
|
if len(segments) == 0:
|
||||||
unique_z: np.ndarray = np.unique(z_values)
|
return {}
|
||||||
groups: dict[int, np.ndarray] = {int(round(z * SCALE)): segments[z_values == z] for z in unique_z}
|
z_um = np.round(segments[:, 0, 2] * SCALE).astype(np.int64)
|
||||||
return groups
|
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):
|
except (TypeError, ValueError):
|
||||||
lift_val = 0.0
|
lift_val = 0.0
|
||||||
|
|
||||||
last_z = -1.0
|
last_z_um: int | None = None
|
||||||
max_z = 0
|
max_z = 0
|
||||||
last_nozzle_z = 0.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()}
|
extras = {prefix: vals[i] for prefix, vals in self.extra_attributes.items()}
|
||||||
|
|
||||||
x0, y0, z0 = coords[0]
|
x0, y0, z0 = coords[0]
|
||||||
|
z0_um = int(round(z0 * SCALE))
|
||||||
|
|
||||||
active_feedrate = self.feedrate
|
active_feedrate = self.feedrate
|
||||||
if i > 0:
|
if i > 0:
|
||||||
|
|
@ -2279,11 +2304,11 @@ class Pather(object):
|
||||||
active_feedrate = travel_fr
|
active_feedrate = travel_fr
|
||||||
|
|
||||||
# 4. Handle Z alignment (layer change and/or un-lift)
|
# 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";BEFORE_LAYER_CHANGE")
|
||||||
gcode.append(f"G0 Z{z0:.3f} F720")
|
gcode.append(f"G0 Z{z0:.3f} F720")
|
||||||
gcode.append(f";AFTER_LAYER_CHANGE")
|
gcode.append(f";AFTER_LAYER_CHANGE")
|
||||||
last_z = z0
|
last_z_um = z0_um
|
||||||
active_feedrate = 720.0
|
active_feedrate = 720.0
|
||||||
elif lift_val > 0.0:
|
elif lift_val > 0.0:
|
||||||
gcode.append(f"G0 Z{z0:.3f} F720")
|
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}")
|
gcode.append(f"G1 E{retraction_val:.5f} F{retraction_fr_val:.3f}")
|
||||||
active_feedrate = retraction_fr_val
|
active_feedrate = retraction_fr_val
|
||||||
else:
|
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";BEFORE_LAYER_CHANGE")
|
||||||
gcode.append(f"G0 Z{z0:.3f} F720")
|
gcode.append(f"G0 Z{z0:.3f} F720")
|
||||||
gcode.append(f";AFTER_LAYER_CHANGE")
|
gcode.append(f";AFTER_LAYER_CHANGE")
|
||||||
last_z = z0
|
last_z_um = z0_um
|
||||||
active_feedrate = 720.0
|
active_feedrate = 720.0
|
||||||
|
|
||||||
old_fr = f_rate[0] if f_rate[0] > 0 else self.travel_feedrate
|
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()
|
node = SLICENODE_PathOperations()
|
||||||
mock_socket1 = MagicMock()
|
mock_socket1 = MagicMock()
|
||||||
|
mock_socket1.is_linked = True
|
||||||
mock_socket2 = MagicMock()
|
mock_socket2 = MagicMock()
|
||||||
|
mock_socket2.is_linked = True
|
||||||
node.inputs = [mock_socket1, mock_socket2]
|
node.inputs = [mock_socket1, mock_socket2]
|
||||||
|
|
||||||
# Mock resolve_input to return sp1 and sp2
|
# Mock resolve_input to return sp1 and sp2
|
||||||
|
|
@ -1207,6 +1209,84 @@ class TestSlice(unittest.TestCase):
|
||||||
node.operation = 'XOR'
|
node.operation = 'XOR'
|
||||||
self.assertEqual(len(node.evaluate_output("Paths")), 2)
|
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):
|
def test_path_operations_single_input_min_coverage(self):
|
||||||
from NodeSlicer.core.slice import SlicePaths
|
from NodeSlicer.core.slice import SlicePaths
|
||||||
# Single input with 1 path per layer (Z=1.0 and Z=2.0)
|
# 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.first_layer_height, 0.3)
|
||||||
self.assertEqual(infilled.name, "Part1")
|
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):
|
class TestCodeReviewBugFixes(unittest.TestCase):
|
||||||
def test_ndarray_lru_cache_empty_list_and_dict(self):
|
def test_ndarray_lru_cache_empty_list_and_dict(self):
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue