seam stagger
This commit is contained in:
parent
d54349594e
commit
973e38fe53
5 changed files with 364 additions and 96 deletions
|
|
@ -748,3 +748,59 @@ EXPORT int reposition_path_seam_c(
|
||||||
|
|
||||||
return out_idx;
|
return out_idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EXPORT int shift_path_seam_by_distance_c(
|
||||||
|
const double* in_pts,
|
||||||
|
int num_pts,
|
||||||
|
int stride,
|
||||||
|
double dist_mm,
|
||||||
|
double* out_pts
|
||||||
|
) {
|
||||||
|
if (num_pts < 4 || fabs(dist_mm) < 1e-6) {
|
||||||
|
memcpy(out_pts, in_pts, num_pts * stride * sizeof(double));
|
||||||
|
return num_pts;
|
||||||
|
}
|
||||||
|
int M = num_pts - 1;
|
||||||
|
double total_len = 0.0;
|
||||||
|
for (int i = 0; i < M; ++i) {
|
||||||
|
double dx = in_pts[(i + 1) * stride + 0] - in_pts[i * stride + 0];
|
||||||
|
double dy = in_pts[(i + 1) * stride + 1] - in_pts[i * stride + 1];
|
||||||
|
double dz = in_pts[(i + 1) * stride + 2] - in_pts[i * stride + 2];
|
||||||
|
total_len += sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
}
|
||||||
|
if (total_len <= 1e-6) {
|
||||||
|
memcpy(out_pts, in_pts, num_pts * stride * sizeof(double));
|
||||||
|
return num_pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
double target_d = fmod(dist_mm, total_len);
|
||||||
|
if (target_d < 0.0) target_d += total_len;
|
||||||
|
|
||||||
|
if (target_d <= 1e-6 || fabs(target_d - total_len) <= 1e-6) {
|
||||||
|
memcpy(out_pts, in_pts, num_pts * stride * sizeof(double));
|
||||||
|
return num_pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
double cum = 0.0;
|
||||||
|
for (int i = 0; i < M; ++i) {
|
||||||
|
double dx = in_pts[(i + 1) * stride + 0] - in_pts[i * stride + 0];
|
||||||
|
double dy = in_pts[(i + 1) * stride + 1] - in_pts[i * stride + 1];
|
||||||
|
double dz = in_pts[(i + 1) * stride + 2] - in_pts[i * stride + 2];
|
||||||
|
double seg_l = sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
if (cum + seg_l >= target_d || i == M - 1) {
|
||||||
|
double t = (seg_l > 1e-7) ? (target_d - cum) / seg_l : 0.0;
|
||||||
|
if (t < 0.0) t = 0.0;
|
||||||
|
if (t > 1.0) t = 1.0;
|
||||||
|
|
||||||
|
double ipt[3];
|
||||||
|
for (int s = 0; s < 3; ++s) {
|
||||||
|
ipt[s] = (1.0 - t) * in_pts[i * stride + s] + t * in_pts[(i + 1) * stride + s];
|
||||||
|
}
|
||||||
|
return reposition_path_seam_c(in_pts, num_pts, stride, i, t, ipt, out_pts);
|
||||||
|
}
|
||||||
|
cum += seg_l;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(out_pts, in_pts, num_pts * stride * sizeof(double));
|
||||||
|
return num_pts;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -606,6 +606,7 @@ class SLICENODE_SeamPlacement(SliceNode):
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Paths")
|
self.add_input("SLICESOCK_Paths")
|
||||||
self.add_input("SLICESOCK_Mesh")
|
self.add_input("SLICESOCK_Mesh")
|
||||||
|
self.add_input("NodeSocketFloat", "Seam Staggering").value = 0.0
|
||||||
self.add_output("SLICESOCK_Paths")
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
|
|
@ -619,13 +620,16 @@ class SLICENODE_SeamPlacement(SliceNode):
|
||||||
|
|
||||||
paths: SlicePaths = self.resolve("Paths")
|
paths: SlicePaths = self.resolve("Paths")
|
||||||
mesh_input: Any = self.resolve("Mesh")
|
mesh_input: Any = self.resolve("Mesh")
|
||||||
|
stagger_val = self.resolve("Seam Staggering")
|
||||||
|
stagger_mm: float = float(stagger_val) if stagger_val is not None else 0.0
|
||||||
|
|
||||||
if not paths:
|
if not paths:
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
|
|
||||||
if not mesh_input:
|
if not mesh_input and abs(stagger_mm) < 1e-6:
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
acquire_t = bool(getattr(self, "acquire_transforms", True))
|
acquire_t = bool(getattr(self, "acquire_transforms", True))
|
||||||
return paths.place_seams(mesh_input, mode=self.mode.lower(), acquire_transforms=acquire_t)
|
return paths.place_seams(mesh_input, mode=self.mode.lower(), acquire_transforms=acquire_t, stagger_mm=stagger_mm)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
256
core/slice.py
256
core/slice.py
|
|
@ -204,6 +204,16 @@ try:
|
||||||
ctypes.POINTER(ctypes.c_double)
|
ctypes.POINTER(ctypes.c_double)
|
||||||
]
|
]
|
||||||
_c_seam_lib.intersect_path_with_2d_segments_c.restype = ctypes.c_int
|
_c_seam_lib.intersect_path_with_2d_segments_c.restype = ctypes.c_int
|
||||||
|
|
||||||
|
if hasattr(_c_seam_lib, "shift_path_seam_by_distance_c"):
|
||||||
|
_c_seam_lib.shift_path_seam_by_distance_c.argtypes = [
|
||||||
|
ctypes.POINTER(ctypes.c_double),
|
||||||
|
ctypes.c_int,
|
||||||
|
ctypes.c_int,
|
||||||
|
ctypes.c_double,
|
||||||
|
ctypes.POINTER(ctypes.c_double)
|
||||||
|
]
|
||||||
|
_c_seam_lib.shift_path_seam_by_distance_c.restype = ctypes.c_int
|
||||||
except Exception:
|
except Exception:
|
||||||
_c_seam_lib = None
|
_c_seam_lib = None
|
||||||
|
|
||||||
|
|
@ -969,6 +979,58 @@ def reposition_path_seam(
|
||||||
return new_p, new_ext, new_feed, new_extra if extra_attributes else extra_attributes
|
return new_p, new_ext, new_feed, new_extra if extra_attributes else extra_attributes
|
||||||
|
|
||||||
|
|
||||||
|
def shift_path_seam_by_distance(
|
||||||
|
path: np.ndarray,
|
||||||
|
dist_mm: float,
|
||||||
|
extrusion_rate: np.ndarray | None = None,
|
||||||
|
feedrate: np.ndarray | None = None,
|
||||||
|
extra_attributes: dict[str, np.ndarray] | None = None
|
||||||
|
) -> tuple[np.ndarray, np.ndarray | None, np.ndarray | None, dict[str, np.ndarray] | None]:
|
||||||
|
"""
|
||||||
|
Shifts the starting point (seam) of a closed path along its perimeter by dist_mm.
|
||||||
|
Positive dist_mm moves forward along the path direction, negative moves backward.
|
||||||
|
Preserves and interpolates per-vertex attributes.
|
||||||
|
"""
|
||||||
|
if len(path) < 4 or not is_closed_path(path) or abs(dist_mm) < 1e-6:
|
||||||
|
return path, extrusion_rate, feedrate, extra_attributes
|
||||||
|
|
||||||
|
pts = path[:, :3]
|
||||||
|
diffs = np.diff(pts, axis=0)
|
||||||
|
seg_lens = np.sqrt(np.sum(diffs ** 2, axis=1))
|
||||||
|
total_len = float(np.sum(seg_lens))
|
||||||
|
|
||||||
|
if total_len <= 1e-6:
|
||||||
|
return path, extrusion_rate, feedrate, extra_attributes
|
||||||
|
|
||||||
|
target_d = dist_mm % total_len
|
||||||
|
if target_d < 0:
|
||||||
|
target_d += total_len
|
||||||
|
|
||||||
|
if target_d <= 1e-6 or abs(target_d - total_len) <= 1e-6:
|
||||||
|
return path, extrusion_rate, feedrate, extra_attributes
|
||||||
|
|
||||||
|
cum = np.concatenate(([0.0], np.cumsum(seg_lens)))
|
||||||
|
seg_idx = int(np.searchsorted(cum, target_d, side='right') - 1)
|
||||||
|
seg_idx = max(0, min(len(seg_lens) - 1, seg_idx))
|
||||||
|
|
||||||
|
l_seg = seg_lens[seg_idx]
|
||||||
|
if l_seg > 1e-7:
|
||||||
|
t = float((target_d - cum[seg_idx]) / l_seg)
|
||||||
|
else:
|
||||||
|
t = 0.0
|
||||||
|
t = max(0.0, min(1.0, t))
|
||||||
|
|
||||||
|
p0 = path[seg_idx]
|
||||||
|
p1 = path[seg_idx + 1]
|
||||||
|
ipt = (1.0 - t) * p0 + t * p1
|
||||||
|
|
||||||
|
return reposition_path_seam(
|
||||||
|
path, seg_idx, t, ipt,
|
||||||
|
extrusion_rate=extrusion_rate,
|
||||||
|
feedrate=feedrate,
|
||||||
|
extra_attributes=extra_attributes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SlicePaths(list[np.ndarray]):
|
class SlicePaths(list[np.ndarray]):
|
||||||
groups: list[int]
|
groups: list[int]
|
||||||
|
|
@ -1435,35 +1497,50 @@ class SlicePaths(list[np.ndarray]):
|
||||||
|
|
||||||
return self.copy_with(new_paths, extrusion_rate=new_ext, feedrate=new_feed, extra_attributes=new_extra)
|
return self.copy_with(new_paths, extrusion_rate=new_ext, feedrate=new_feed, extra_attributes=new_extra)
|
||||||
|
|
||||||
|
def stagger_seams(self, stagger_mm: float) -> SlicePaths:
|
||||||
|
"""
|
||||||
|
Shifts the seams of closed paths layer-by-layer, alternating between +stagger_mm (even layers)
|
||||||
|
and -stagger_mm (odd layers) along the perimeter to avoid vertical seam alignment weak spots.
|
||||||
|
"""
|
||||||
|
return self.place_seams(mesh_input=None, stagger_mm=stagger_mm)
|
||||||
|
|
||||||
from ..infra.debug import debug
|
from ..infra.debug import debug
|
||||||
@debug
|
@debug
|
||||||
def place_seams(self, mesh_input: Any, mode: str = "closest", acquire_transforms: bool = True) -> SlicePaths:
|
def place_seams(self, mesh_input: Any = None, mode: str = "closest", acquire_transforms: bool = True, stagger_mm: float = 0.0) -> SlicePaths:
|
||||||
"""
|
"""
|
||||||
Repositions the starting point (seam) of closed slice paths (including outer contours
|
Repositions the starting point (seam) of closed slice paths (including outer contours
|
||||||
and interior holes) to the intersection points between the slice paths and the mesh.
|
and interior holes) to the intersection points between the slice paths and the mesh.
|
||||||
If acquire_transforms is True, applies upstream transforms from slicepaths metadata to the cutting mesh.
|
If acquire_transforms is True, applies upstream transforms from slicepaths metadata to the cutting mesh.
|
||||||
If there is no intersection for a given path, its seam remains unchanged.
|
If stagger_mm is non-zero, offsets the seam along the path perimeter (+stagger_mm on even layers, -stagger_mm on odd layers).
|
||||||
|
If there is no intersection for a given path, its seam remains unchanged (aside from any staggering).
|
||||||
"""
|
"""
|
||||||
if not self:
|
if not self:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
tris = extract_mesh_triangles(mesh_input)
|
tris = extract_mesh_triangles(mesh_input) if mesh_input is not None else np.empty((0, 3, 3), dtype=np.float64)
|
||||||
if len(tris) == 0:
|
if len(tris) == 0 and abs(stagger_mm) < 1e-6:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
if acquire_transforms and getattr(self, "transforms", None):
|
if len(tris) > 0:
|
||||||
mesh_transforms = extract_mesh_transforms(mesh_input)
|
if acquire_transforms and getattr(self, "transforms", None):
|
||||||
delta_transforms = compute_delta_transforms(self.transforms, mesh_transforms)
|
mesh_transforms = extract_mesh_transforms(mesh_input)
|
||||||
if delta_transforms:
|
delta_transforms = compute_delta_transforms(self.transforms, mesh_transforms)
|
||||||
tris = apply_transforms_to_triangles(tris, delta_transforms)
|
if delta_transforms:
|
||||||
|
tris = apply_transforms_to_triangles(tris, delta_transforms)
|
||||||
|
|
||||||
mesh_center = np.mean(tris[:, :3, :].reshape(-1, 3), axis=0)
|
mesh_center = np.mean(tris[:, :3, :].reshape(-1, 3), axis=0)
|
||||||
|
|
||||||
# Precompute triangle Z bounds
|
# Precompute triangle Z bounds
|
||||||
tz_min = np.min(tris[:, :3, 2], axis=1) - 1e-4
|
tz_min = np.min(tris[:, :3, 2], axis=1) - 1e-4
|
||||||
tz_max = np.max(tris[:, :3, 2], axis=1) + 1e-4
|
tz_max = np.max(tris[:, :3, 2], axis=1) + 1e-4
|
||||||
mesh_z_min = float(np.min(tz_min)) if len(tz_min) > 0 else 0.0
|
mesh_z_min = float(np.min(tz_min)) if len(tz_min) > 0 else 0.0
|
||||||
mesh_z_max = float(np.max(tz_max)) if len(tz_max) > 0 else 0.0
|
mesh_z_max = float(np.max(tz_max)) if len(tz_max) > 0 else 0.0
|
||||||
|
else:
|
||||||
|
mesh_center = np.zeros(3)
|
||||||
|
tz_min = np.empty(0, dtype=np.float64)
|
||||||
|
tz_max = np.empty(0, dtype=np.float64)
|
||||||
|
mesh_z_min = 0.0
|
||||||
|
mesh_z_max = 0.0
|
||||||
|
|
||||||
n_paths = len(self)
|
n_paths = len(self)
|
||||||
res_paths: list[np.ndarray | None] = [None] * n_paths
|
res_paths: list[np.ndarray | None] = [None] * n_paths
|
||||||
|
|
@ -1487,107 +1564,98 @@ class SlicePaths(list[np.ndarray]):
|
||||||
pz_min = float(np.min(p[:, 2]))
|
pz_min = float(np.min(p[:, 2]))
|
||||||
pz_max = float(np.max(p[:, 2]))
|
pz_max = float(np.max(p[:, 2]))
|
||||||
|
|
||||||
if pz_max < mesh_z_min or pz_min > mesh_z_max:
|
|
||||||
# Completely outside cutting mesh Z-bounds
|
|
||||||
res_paths[i] = p
|
|
||||||
res_ext[i] = self.extrusion_rate[i]
|
|
||||||
res_feed[i] = self.feedrate[i]
|
|
||||||
for k in self.extra_attributes:
|
|
||||||
res_extra[k][i] = self.extra_attributes[k][i]
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (pz_max - pz_min) < 1e-3:
|
if (pz_max - pz_min) < 1e-3:
|
||||||
z_um = int(round(float(p[0, 2]) * SCALE))
|
z_um = int(round(float(p[0, 2]) * SCALE))
|
||||||
z_to_indices.setdefault(z_um, []).append(i)
|
z_to_indices.setdefault(z_um, []).append(i)
|
||||||
else:
|
else:
|
||||||
non_planar_indices.append(i)
|
non_planar_indices.append(i)
|
||||||
|
|
||||||
# Process planar paths layer-by-layer
|
# Process planar paths layer-by-layer in sorted Z order
|
||||||
for z_um, indices in z_to_indices.items():
|
sorted_z_keys = sorted(z_to_indices.keys())
|
||||||
|
for layer_idx, z_um in enumerate(sorted_z_keys):
|
||||||
|
layer_stagger = float(stagger_mm) if (layer_idx % 2 == 0) else -float(stagger_mm)
|
||||||
|
indices = z_to_indices[z_um]
|
||||||
first_p = self[indices[0]]
|
first_p = self[indices[0]]
|
||||||
pz = float(first_p[0, 2])
|
pz = float(first_p[0, 2])
|
||||||
|
|
||||||
# Filter triangles overlapping this layer Z
|
mesh_segs = np.empty((0, 2, 2), dtype=np.float64)
|
||||||
t_mask = (tz_min <= pz) & (tz_max >= pz)
|
if len(tris) > 0 and (mesh_z_min <= pz <= mesh_z_max):
|
||||||
active_tris = tris[t_mask]
|
t_mask = (tz_min <= pz) & (tz_max >= pz)
|
||||||
|
active_tris = tris[t_mask]
|
||||||
|
if len(active_tris) > 0:
|
||||||
|
mesh_segs = slice_triangles_at_z(active_tris, pz)
|
||||||
|
|
||||||
if len(active_tris) == 0:
|
|
||||||
for idx in indices:
|
|
||||||
res_paths[idx] = self[idx]
|
|
||||||
res_ext[idx] = self.extrusion_rate[idx]
|
|
||||||
res_feed[idx] = self.feedrate[idx]
|
|
||||||
for k in self.extra_attributes:
|
|
||||||
res_extra[k][idx] = self.extra_attributes[k][idx]
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Slice triangles at Z height ONCE for all paths in this layer
|
|
||||||
mesh_segs = slice_triangles_at_z(active_tris, pz)
|
|
||||||
|
|
||||||
if len(mesh_segs) == 0:
|
|
||||||
for idx in indices:
|
|
||||||
res_paths[idx] = self[idx]
|
|
||||||
res_ext[idx] = self.extrusion_rate[idx]
|
|
||||||
res_feed[idx] = self.feedrate[idx]
|
|
||||||
for k in self.extra_attributes:
|
|
||||||
res_extra[k][idx] = self.extra_attributes[k][idx]
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Intersect each path in the layer with pre-sliced 2D segments
|
|
||||||
for idx in indices:
|
for idx in indices:
|
||||||
p = self[idx]
|
p = self[idx]
|
||||||
inter = intersect_path_with_2d_segments(p, mesh_segs, pz, mode=mode, mesh_center=mesh_center)
|
curr_ext = self.extrusion_rate[idx]
|
||||||
if inter is None:
|
curr_feed = self.feedrate[idx]
|
||||||
res_paths[idx] = p
|
|
||||||
res_ext[idx] = self.extrusion_rate[idx]
|
|
||||||
res_feed[idx] = self.feedrate[idx]
|
|
||||||
for k in self.extra_attributes:
|
|
||||||
res_extra[k][idx] = self.extra_attributes[k][idx]
|
|
||||||
continue
|
|
||||||
|
|
||||||
seg_idx, t, ipt = inter
|
|
||||||
curr_extra = {k: self.extra_attributes[k][idx] for k in self.extra_attributes} if self.extra_attributes else None
|
curr_extra = {k: self.extra_attributes[k][idx] for k in self.extra_attributes} if self.extra_attributes else None
|
||||||
p_mod, ext_mod, feed_mod, extra_mod = reposition_path_seam(
|
|
||||||
p, seg_idx, t, ipt,
|
# 1. Reposition to cutting mesh intersection if available
|
||||||
extrusion_rate=self.extrusion_rate[idx],
|
if len(mesh_segs) > 0:
|
||||||
feedrate=self.feedrate[idx],
|
inter = intersect_path_with_2d_segments(p, mesh_segs, pz, mode=mode, mesh_center=mesh_center)
|
||||||
extra_attributes=curr_extra
|
if inter is not None:
|
||||||
)
|
seg_idx, t, ipt = inter
|
||||||
res_paths[idx] = p_mod
|
p, curr_ext, curr_feed, curr_extra = reposition_path_seam(
|
||||||
res_ext[idx] = ext_mod if ext_mod is not None else self.extrusion_rate[idx]
|
p, seg_idx, t, ipt,
|
||||||
res_feed[idx] = feed_mod if feed_mod is not None else self.feedrate[idx]
|
extrusion_rate=curr_ext,
|
||||||
if extra_mod:
|
feedrate=curr_feed,
|
||||||
|
extra_attributes=curr_extra
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Shift seam along path if seam staggering is requested
|
||||||
|
if abs(layer_stagger) > 1e-6 and is_closed_path(p):
|
||||||
|
p, curr_ext, curr_feed, curr_extra = shift_path_seam_by_distance(
|
||||||
|
p, layer_stagger,
|
||||||
|
extrusion_rate=curr_ext,
|
||||||
|
feedrate=curr_feed,
|
||||||
|
extra_attributes=curr_extra
|
||||||
|
)
|
||||||
|
|
||||||
|
res_paths[idx] = p
|
||||||
|
res_ext[idx] = curr_ext if curr_ext is not None else self.extrusion_rate[idx]
|
||||||
|
res_feed[idx] = curr_feed if curr_feed is not None else self.feedrate[idx]
|
||||||
|
if curr_extra:
|
||||||
for k in self.extra_attributes:
|
for k in self.extra_attributes:
|
||||||
res_extra[k][idx] = extra_mod.get(k, self.extra_attributes[k][idx])
|
res_extra[k][idx] = curr_extra.get(k, self.extra_attributes[k][idx])
|
||||||
else:
|
else:
|
||||||
for k in self.extra_attributes:
|
for k in self.extra_attributes:
|
||||||
res_extra[k][idx] = self.extra_attributes[k][idx]
|
res_extra[k][idx] = self.extra_attributes[k][idx]
|
||||||
|
|
||||||
# Process any non-planar paths
|
# Process any non-planar paths
|
||||||
for idx in non_planar_indices:
|
for np_idx, idx in enumerate(non_planar_indices):
|
||||||
|
layer_idx = len(sorted_z_keys) + np_idx
|
||||||
|
layer_stagger = float(stagger_mm) if (layer_idx % 2 == 0) else -float(stagger_mm)
|
||||||
p = self[idx]
|
p = self[idx]
|
||||||
inter = find_path_mesh_intersection(p, tris, mode=mode, mesh_center=mesh_center)
|
curr_ext = self.extrusion_rate[idx]
|
||||||
if inter is None:
|
curr_feed = self.feedrate[idx]
|
||||||
res_paths[idx] = p
|
|
||||||
res_ext[idx] = self.extrusion_rate[idx]
|
|
||||||
res_feed[idx] = self.feedrate[idx]
|
|
||||||
for k in self.extra_attributes:
|
|
||||||
res_extra[k][idx] = self.extra_attributes[k][idx]
|
|
||||||
continue
|
|
||||||
|
|
||||||
seg_idx, t, ipt = inter
|
|
||||||
curr_extra = {k: self.extra_attributes[k][idx] for k in self.extra_attributes} if self.extra_attributes else None
|
curr_extra = {k: self.extra_attributes[k][idx] for k in self.extra_attributes} if self.extra_attributes else None
|
||||||
p_mod, ext_mod, feed_mod, extra_mod = reposition_path_seam(
|
|
||||||
p, seg_idx, t, ipt,
|
if len(tris) > 0:
|
||||||
extrusion_rate=self.extrusion_rate[idx],
|
inter = find_path_mesh_intersection(p, tris, mode=mode, mesh_center=mesh_center)
|
||||||
feedrate=self.feedrate[idx],
|
if inter is not None:
|
||||||
extra_attributes=curr_extra
|
seg_idx, t, ipt = inter
|
||||||
)
|
p, curr_ext, curr_feed, curr_extra = reposition_path_seam(
|
||||||
res_paths[idx] = p_mod
|
p, seg_idx, t, ipt,
|
||||||
res_ext[idx] = ext_mod if ext_mod is not None else self.extrusion_rate[idx]
|
extrusion_rate=curr_ext,
|
||||||
res_feed[idx] = feed_mod if feed_mod is not None else self.feedrate[idx]
|
feedrate=curr_feed,
|
||||||
if extra_mod:
|
extra_attributes=curr_extra
|
||||||
|
)
|
||||||
|
|
||||||
|
if abs(layer_stagger) > 1e-6 and is_closed_path(p):
|
||||||
|
p, curr_ext, curr_feed, curr_extra = shift_path_seam_by_distance(
|
||||||
|
p, layer_stagger,
|
||||||
|
extrusion_rate=curr_ext,
|
||||||
|
feedrate=curr_feed,
|
||||||
|
extra_attributes=curr_extra
|
||||||
|
)
|
||||||
|
|
||||||
|
res_paths[idx] = p
|
||||||
|
res_ext[idx] = curr_ext if curr_ext is not None else self.extrusion_rate[idx]
|
||||||
|
res_feed[idx] = curr_feed if curr_feed is not None else self.feedrate[idx]
|
||||||
|
if curr_extra:
|
||||||
for k in self.extra_attributes:
|
for k in self.extra_attributes:
|
||||||
res_extra[k][idx] = extra_mod.get(k, self.extra_attributes[k][idx])
|
res_extra[k][idx] = curr_extra.get(k, self.extra_attributes[k][idx])
|
||||||
else:
|
else:
|
||||||
for k in self.extra_attributes:
|
for k in self.extra_attributes:
|
||||||
res_extra[k][idx] = self.extra_attributes[k][idx]
|
res_extra[k][idx] = self.extra_attributes[k][idx]
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height. Automatically aligns path seams to the back (+Y) of each layer. 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 back (+Y) of each layer. Supports rotation for slicing at an angle.
|
||||||
- **Ignore Holes**: Boolean option to omit interior hole perimeters and generate only the outer shell boundaries.
|
- **Ignore Holes**: Boolean option to omit interior hole perimeters and generate only the outer shell boundaries.
|
||||||
- **Seam Placement**: Repositions the starting vertex (seam) of closed slice paths (including outer contours and interior holes) to the intersection points between the slice paths and a guide/seam mesh. If there is no intersection for a given path, its seam remains unchanged. Agnostic to mesh input type (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or lists).
|
- **Seam Placement**: Repositions the starting vertex (seam) of closed slice paths (including outer contours and interior holes) to the intersection points between the slice paths and a guide/seam mesh. If there is no intersection for a given path, its seam remains unchanged. Agnostic to mesh input type (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or lists).
|
||||||
|
- **Mesh**: Input cutting mesh socket.
|
||||||
|
- **Seam Staggering**: Float input socket specifying a distance in millimeters to shift seams along the perimeter alternating layer-by-layer (+stagger on even layers, -stagger on odd layers) to avoid vertical seam alignment weak spots. Can also be used standalone without a cutting mesh.
|
||||||
- **Mode**: Strategy for choosing intersection when multiple intersections occur (**Closest to Seam**, **First Intersection**, or **Mesh Centroid**).
|
- **Mode**: Strategy for choosing intersection when multiple intersections occur (**Closest to Seam**, **First Intersection**, or **Mesh Centroid**).
|
||||||
- **Acquire Transforms**: Checkbox to automatically apply upstream transformation metadata from `SlicePaths` (such as from Transform Geometry or Drop Geometry) to the cutting mesh so that the cutting geometry aligns with the paths as in the viewport.
|
- **Acquire Transforms**: Checkbox to automatically apply upstream transformation metadata from `SlicePaths` (such as from Transform Geometry or Drop Geometry) to the cutting mesh so that the cutting geometry aligns with the paths as in the viewport.
|
||||||
- **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.
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,143 @@ class TestSeamPlacement(unittest.TestCase):
|
||||||
# Transformed seam should NOT equal the unmodified transformed start point
|
# Transformed seam should NOT equal the unmodified transformed start point
|
||||||
self.assertFalse(np.allclose(res[0][0], sp[0][0]))
|
self.assertFalse(np.allclose(res[0][0], sp[0][0]))
|
||||||
|
|
||||||
|
def test_seam_staggering_alternating_layers_with_mesh(self):
|
||||||
|
# Create 3 layers of square paths: Z=0.0, Z=0.2, Z=0.4
|
||||||
|
# Path vertices: (-10,-10) -> (10,-10) -> (10,10) -> (-10,10) -> (-10,-10)
|
||||||
|
# Cutting plane at top edge (X=0, Y=10)
|
||||||
|
p0 = self.square_path.copy()
|
||||||
|
p0[:, 2] = 0.0
|
||||||
|
p1 = self.square_path.copy()
|
||||||
|
p1[:, 2] = 0.2
|
||||||
|
p2 = self.square_path.copy()
|
||||||
|
p2[:, 2] = 0.4
|
||||||
|
|
||||||
|
sp = SlicePaths([p0, p1, p2])
|
||||||
|
|
||||||
|
cutting_plane = create_quad_mesh(
|
||||||
|
[0.0, 5.0, -5.0],
|
||||||
|
[0.0, 15.0, -5.0],
|
||||||
|
[0.0, 15.0, 5.0],
|
||||||
|
[0.0, 5.0, 5.0]
|
||||||
|
)
|
||||||
|
|
||||||
|
stagger_dist = 2.0
|
||||||
|
res = sp.place_seams(cutting_plane, stagger_mm=stagger_dist)
|
||||||
|
|
||||||
|
self.assertEqual(len(res), 3)
|
||||||
|
for p in res:
|
||||||
|
self.assertTrue(is_closed_path(p))
|
||||||
|
|
||||||
|
# Intersection is at (0, 10, Z).
|
||||||
|
# On top edge from (10, 10) to (-10, 10), moving forward along path goes in -X direction.
|
||||||
|
# Layer 0 (even): +2.0 mm forward along path -> (0 - 2.0, 10, 0.0) = (-2.0, 10.0, 0.0)
|
||||||
|
np.testing.assert_allclose(res[0][0], [-2.0, 10.0, 0.0], atol=1e-4)
|
||||||
|
np.testing.assert_allclose(res[0][-1], [-2.0, 10.0, 0.0], atol=1e-4)
|
||||||
|
|
||||||
|
# Layer 1 (odd): -2.0 mm backward along path -> (0 + 2.0, 10, 0.2) = (+2.0, 10.0, 0.2)
|
||||||
|
np.testing.assert_allclose(res[1][0], [2.0, 10.0, 0.2], atol=1e-4)
|
||||||
|
np.testing.assert_allclose(res[1][-1], [2.0, 10.0, 0.2], atol=1e-4)
|
||||||
|
|
||||||
|
# Layer 2 (even): +2.0 mm forward along path -> (-2.0, 10.0, 0.4)
|
||||||
|
np.testing.assert_allclose(res[2][0], [-2.0, 10.0, 0.4], atol=1e-4)
|
||||||
|
np.testing.assert_allclose(res[2][-1], [-2.0, 10.0, 0.4], atol=1e-4)
|
||||||
|
|
||||||
|
def test_seam_staggering_without_mesh(self):
|
||||||
|
# Stagger seams directly without any cutting mesh
|
||||||
|
p0 = self.square_path.copy()
|
||||||
|
p0[:, 2] = 0.0
|
||||||
|
p1 = self.square_path.copy()
|
||||||
|
p1[:, 2] = 0.2
|
||||||
|
|
||||||
|
sp = SlicePaths([p0, p1])
|
||||||
|
stagger_dist = 3.0
|
||||||
|
|
||||||
|
res = sp.stagger_seams(stagger_dist)
|
||||||
|
|
||||||
|
self.assertEqual(len(res), 2)
|
||||||
|
self.assertTrue(is_closed_path(res[0]))
|
||||||
|
self.assertTrue(is_closed_path(res[1]))
|
||||||
|
|
||||||
|
# Original start was (-10, -10).
|
||||||
|
# First segment is from (-10, -10) to (10, -10) along +X.
|
||||||
|
# Layer 0 (even): +3.0 mm forward -> (-7.0, -10.0, 0.0)
|
||||||
|
np.testing.assert_allclose(res[0][0], [-7.0, -10.0, 0.0], atol=1e-4)
|
||||||
|
np.testing.assert_allclose(res[0][-1], [-7.0, -10.0, 0.0], atol=1e-4)
|
||||||
|
|
||||||
|
# Layer 1 (odd): -3.0 mm backward -> wraps to previous segment from (-10, 10) to (-10, -10) at (-10.0, -7.0, 0.2)
|
||||||
|
np.testing.assert_allclose(res[1][0], [-10.0, -7.0, 0.2], atol=1e-4)
|
||||||
|
np.testing.assert_allclose(res[1][-1], [-10.0, -7.0, 0.2], atol=1e-4)
|
||||||
|
|
||||||
|
def test_seam_staggering_preserves_attributes(self):
|
||||||
|
p0 = self.square_path.copy()
|
||||||
|
p0[:, 2] = 0.0
|
||||||
|
p1 = self.square_path.copy()
|
||||||
|
p1[:, 2] = 0.2
|
||||||
|
|
||||||
|
ext = [np.array([1.0, 2.0, 3.0, 4.0, 1.0]), np.array([5.0, 6.0, 7.0, 8.0, 5.0])]
|
||||||
|
feed = [np.array([10.0, 20.0, 30.0, 40.0, 10.0]), np.array([50.0, 60.0, 70.0, 80.0, 50.0])]
|
||||||
|
extra = {"temp": [np.array([200.0, 210.0, 220.0, 230.0, 200.0]), np.array([205.0, 215.0, 225.0, 235.0, 205.0])]}
|
||||||
|
|
||||||
|
sp = SlicePaths([p0, p1], extrusion_rate=ext, feedrate=feed, extra_attributes=extra)
|
||||||
|
res = sp.stagger_seams(2.0)
|
||||||
|
|
||||||
|
self.assertEqual(len(res), 2)
|
||||||
|
self.assertEqual(len(res.extrusion_rate), 2)
|
||||||
|
self.assertEqual(len(res.feedrate), 2)
|
||||||
|
self.assertIn("temp", res.extra_attributes)
|
||||||
|
self.assertEqual(len(res.extra_attributes["temp"]), 2)
|
||||||
|
|
||||||
|
for i in range(2):
|
||||||
|
self.assertEqual(len(res[i]), len(res.extrusion_rate[i]))
|
||||||
|
self.assertEqual(len(res[i]), len(res.feedrate[i]))
|
||||||
|
self.assertEqual(len(res[i]), len(res.extra_attributes["temp"][i]))
|
||||||
|
# Extrusion rate at start and end must match for closed loop
|
||||||
|
self.assertAlmostEqual(res.extrusion_rate[i][0], res.extrusion_rate[i][-1])
|
||||||
|
self.assertAlmostEqual(res.feedrate[i][0], res.feedrate[i][-1])
|
||||||
|
self.assertAlmostEqual(res.extra_attributes["temp"][i][0], res.extra_attributes["temp"][i][-1])
|
||||||
|
|
||||||
|
def test_seam_placement_node_socket_and_execution(self):
|
||||||
|
node = SLICENODE_SeamPlacement()
|
||||||
|
# Mock inputs
|
||||||
|
inputs_dict = {}
|
||||||
|
class MockSocket:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.value = 0.0
|
||||||
|
def mock_add_input(type_name, name=None):
|
||||||
|
s_name = name or ("Paths" if "Paths" in type_name else "Mesh")
|
||||||
|
sock = MockSocket(s_name)
|
||||||
|
inputs_dict[s_name] = sock
|
||||||
|
return sock
|
||||||
|
|
||||||
|
node.add_input = mock_add_input
|
||||||
|
node.add_output = MagicMock()
|
||||||
|
node.init(None)
|
||||||
|
|
||||||
|
self.assertIn("Seam Staggering", inputs_dict)
|
||||||
|
self.assertEqual(inputs_dict["Seam Staggering"].value, 0.0)
|
||||||
|
|
||||||
|
# Mock node.resolve
|
||||||
|
p0 = self.square_path.copy()
|
||||||
|
p0[:, 2] = 0.0
|
||||||
|
p1 = self.square_path.copy()
|
||||||
|
p1[:, 2] = 0.2
|
||||||
|
sp = SlicePaths([p0, p1])
|
||||||
|
|
||||||
|
def mock_resolve(name):
|
||||||
|
if name == "Paths": return sp
|
||||||
|
if name == "Mesh": return None
|
||||||
|
if name == "Seam Staggering": return 2.5
|
||||||
|
return None
|
||||||
|
|
||||||
|
node.resolve = mock_resolve
|
||||||
|
res = node.evaluate_output("Paths")
|
||||||
|
self.assertEqual(len(res), 2)
|
||||||
|
self.assertTrue(is_closed_path(res[0]))
|
||||||
|
self.assertTrue(is_closed_path(res[1]))
|
||||||
|
# Check layer 0 staggered forward by 2.5mm
|
||||||
|
np.testing.assert_allclose(res[0][0], [-7.5, -10.0, 0.0], atol=1e-4)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
@ -617,3 +754,4 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue