seams on the back
|
|
@ -12,67 +12,60 @@
|
|||
EXPORT int find_sharpest_seam_c(const double* pts, int M, int stride, double scale) {
|
||||
if (M < 3) return 0;
|
||||
|
||||
int64_t x_buf[1024];
|
||||
int64_t y_buf[1024];
|
||||
int64_t* x = (M <= 1024) ? x_buf : (int64_t*)malloc(M * sizeof(int64_t));
|
||||
int64_t* y = (M <= 1024) ? y_buf : (int64_t*)malloc(M * sizeof(int64_t));
|
||||
|
||||
for (int i = 0; i < M; ++i) {
|
||||
x[i] = (int64_t)llround(pts[i * stride + 0] * scale);
|
||||
y[i] = (int64_t)llround(pts[i * stride + 1] * scale);
|
||||
}
|
||||
|
||||
int64_t signed_area = (x[M - 1] * y[0] - x[0] * y[M - 1]);
|
||||
for (int i = 0; i < M - 1; ++i) {
|
||||
signed_area += (x[i] * y[i + 1] - x[i + 1] * y[i]);
|
||||
}
|
||||
|
||||
if (signed_area == 0) {
|
||||
if (M > 1024) { free(x); free(y); }
|
||||
return 0;
|
||||
// Find Y bounding range
|
||||
double min_y = pts[1];
|
||||
double max_y = pts[1];
|
||||
for (int i = 1; i < M; ++i) {
|
||||
double y = pts[i * stride + 1];
|
||||
if (y < min_y) min_y = y;
|
||||
if (y > max_y) max_y = y;
|
||||
}
|
||||
double y_range = max_y - min_y;
|
||||
double corner_bonus = (y_range > 0.0) ? (0.1 * y_range < 2.0 ? 0.1 * y_range : 2.0) : 2.0;
|
||||
|
||||
int best_idx = 0;
|
||||
double min_cos_concave = 2.0;
|
||||
int best_concave_idx = -1;
|
||||
double min_cos_overall = 2.0;
|
||||
double best_score = -1e30;
|
||||
double best_x = 1e30;
|
||||
|
||||
for (int i = 0; i < M; ++i) {
|
||||
int prev = (i == 0) ? M - 1 : i - 1;
|
||||
int next = (i == M - 1) ? 0 : i + 1;
|
||||
|
||||
int64_t vin_x = x[i] - x[prev];
|
||||
int64_t vin_y = y[i] - y[prev];
|
||||
int64_t vout_x = x[next] - x[i];
|
||||
int64_t vout_y = y[next] - y[i];
|
||||
double xi = pts[i * stride + 0];
|
||||
double yi = pts[i * stride + 1];
|
||||
|
||||
int64_t l2_in = vin_x * vin_x + vin_y * vin_y;
|
||||
int64_t l2_out = vout_x * vout_x + vout_y * vout_y;
|
||||
double vin_x = xi - pts[prev * stride + 0];
|
||||
double vin_y = yi - pts[prev * stride + 1];
|
||||
double vout_x = pts[next * stride + 0] - xi;
|
||||
double vout_y = pts[next * stride + 1] - yi;
|
||||
|
||||
if (l2_in == 0 || l2_out == 0) continue;
|
||||
double l_in = sqrt(vin_x * vin_x + vin_y * vin_y);
|
||||
double l_out = sqrt(vout_x * vout_x + vout_y * vout_y);
|
||||
|
||||
int64_t cross = vin_x * vout_y - vin_y * vout_x;
|
||||
int is_concave = (signed_area > 0) ? (cross < 0) : (cross > 0);
|
||||
|
||||
double dot = (double)(vin_x * vout_x + vin_y * vout_y);
|
||||
double denom = sqrt((double)l2_in * (double)l2_out);
|
||||
double cos_ang = dot / denom;
|
||||
|
||||
if (is_concave) {
|
||||
if (cos_ang < min_cos_concave) {
|
||||
min_cos_concave = cos_ang;
|
||||
best_concave_idx = i;
|
||||
}
|
||||
double sharpness = 0.0;
|
||||
if (l_in > 1e-7 && l_out > 1e-7) {
|
||||
double dot = vin_x * vout_x + vin_y * vout_y;
|
||||
double cos_ang = dot / (l_in * l_out);
|
||||
if (cos_ang > 1.0) cos_ang = 1.0;
|
||||
if (cos_ang < -1.0) cos_ang = -1.0;
|
||||
sharpness = 1.0 - cos_ang; // 0 for straight line, 1 for 90 deg corner, 2 for 180 deg turnaround
|
||||
}
|
||||
if (cos_ang < min_cos_overall) {
|
||||
min_cos_overall = cos_ang;
|
||||
|
||||
double score = yi + sharpness * corner_bonus;
|
||||
|
||||
// Highest score wins (back of the object). Tie-break: smallest X for deterministic consistency across layers
|
||||
if (score > best_score + 1e-6) {
|
||||
best_score = score;
|
||||
best_x = xi;
|
||||
best_idx = i;
|
||||
} else if (fabs(score - best_score) <= 1e-6) {
|
||||
if (xi < best_x) {
|
||||
best_x = xi;
|
||||
best_idx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (M > 1024) { free(x); free(y); }
|
||||
|
||||
if (best_concave_idx >= 0) return best_concave_idx;
|
||||
return best_idx;
|
||||
}
|
||||
|
||||
|
|
|
|||
179
core/slice.py
|
|
@ -190,89 +190,68 @@ def weld_vertices(tris: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|||
return unique_verts, faces
|
||||
|
||||
|
||||
def align_path_seam_sharpest(path: np.ndarray) -> np.ndarray:
|
||||
def find_path_seam(path: np.ndarray) -> int:
|
||||
"""
|
||||
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 exact 1-micron integer arithmetic for orientation and angle selection.
|
||||
Finds the optimal starting vertex index (seam) positioned at the back of the object (+Y).
|
||||
Uses a weighted heuristic prioritizing maximum Y with a sharpness bonus for corners,
|
||||
with deterministic tie-breaking for consistent vertical layer alignment.
|
||||
"""
|
||||
if len(path) < 4:
|
||||
return path
|
||||
|
||||
if not is_closed_path(path):
|
||||
return path
|
||||
if len(path) < 4 or not is_closed_path(path):
|
||||
return 0
|
||||
|
||||
pts = path[:-1]
|
||||
M = len(pts)
|
||||
if M < 3:
|
||||
return path
|
||||
return 0
|
||||
|
||||
if _c_seam_lib is not None:
|
||||
try:
|
||||
c_pts = np.ascontiguousarray(pts, dtype=np.float64)
|
||||
stride = int(c_pts.shape[1])
|
||||
ptr = c_pts.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
|
||||
best_idx = int(_c_seam_lib.find_sharpest_seam_c(ptr, M, stride, float(SCALE)))
|
||||
if best_idx == 0:
|
||||
return path
|
||||
res = np.empty_like(path)
|
||||
split = M - best_idx
|
||||
res[:split] = path[best_idx:-1]
|
||||
res[split:] = path[:best_idx + 1]
|
||||
return res
|
||||
return int(_c_seam_lib.find_sharpest_seam_c(ptr, M, stride, float(SCALE)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pts_um = np.rint(pts[:, :2] * SCALE).astype(np.int64)
|
||||
x = pts_um[:, 0]
|
||||
y = pts_um[:, 1]
|
||||
signed_area_um = np.sum(x[:-1] * y[1:] - x[1:] * y[:-1]) + (x[-1] * y[0] - x[0] * y[-1])
|
||||
if signed_area_um == 0:
|
||||
# NumPy fallback
|
||||
x = pts[:, 0]
|
||||
y = pts[:, 1]
|
||||
y_min, y_max = float(np.min(y)), float(np.max(y))
|
||||
y_range = y_max - y_min
|
||||
bonus = min(2.0, 0.1 * y_range) if y_range > 0 else 2.0
|
||||
|
||||
prev_pts = np.roll(pts[:, :2], 1, axis=0)
|
||||
next_pts = np.roll(pts[:, :2], -1, axis=0)
|
||||
|
||||
v_in = pts[:, :2] - prev_pts
|
||||
v_out = next_pts - pts[:, :2]
|
||||
|
||||
l_in = np.linalg.norm(v_in, axis=1)
|
||||
l_out = np.linalg.norm(v_out, axis=1)
|
||||
|
||||
valid = (l_in > 1e-7) & (l_out > 1e-7)
|
||||
cos_ang = np.ones(M, dtype=np.float64)
|
||||
cos_ang[valid] = np.sum(v_in[valid] * v_out[valid], axis=1) / (l_in[valid] * l_out[valid])
|
||||
cos_ang = np.clip(cos_ang, -1.0, 1.0)
|
||||
sharpness = 1.0 - cos_ang
|
||||
|
||||
scores = y + sharpness * bonus
|
||||
# Deterministic tie-break: highest score, smallest x
|
||||
return int(np.lexsort((x, -scores))[0])
|
||||
|
||||
|
||||
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 back of the object (highest Y with corner preference).
|
||||
|
||||
If path is open or has fewer than 3 distinct vertices, returns path unchanged.
|
||||
"""
|
||||
if len(path) < 4 or not is_closed_path(path):
|
||||
return path
|
||||
|
||||
v_in = np.empty((M, 2), dtype=np.int64)
|
||||
v_in[0, 0] = x[0] - x[-1]
|
||||
v_in[0, 1] = y[0] - y[-1]
|
||||
v_in[1:, 0] = x[1:] - x[:-1]
|
||||
v_in[1:, 1] = y[1:] - y[:-1]
|
||||
|
||||
l2_in = v_in[:, 0]**2 + v_in[:, 1]**2
|
||||
valid_in = l2_in > 0
|
||||
valid_out = np.empty_like(valid_in)
|
||||
valid_out[:-1] = valid_in[1:]
|
||||
valid_out[-1] = valid_in[0]
|
||||
valid = valid_in & valid_out
|
||||
if not np.any(valid):
|
||||
return path
|
||||
|
||||
cross_2d = np.empty(M, dtype=np.int64)
|
||||
cross_2d[:-1] = v_in[:-1, 0] * v_in[1:, 1] - v_in[:-1, 1] * v_in[1:, 0]
|
||||
cross_2d[-1] = v_in[-1, 0] * v_in[0, 1] - v_in[-1, 1] * v_in[0, 0]
|
||||
|
||||
concave_mask = valid & (cross_2d < 0) if signed_area_um > 0 else valid & (cross_2d > 0)
|
||||
|
||||
l_in = np.sqrt(l2_in, dtype=np.float64)
|
||||
l_out = np.empty_like(l_in)
|
||||
l_out[:-1] = l_in[1:]
|
||||
l_out[-1] = l_in[0]
|
||||
|
||||
dot_prods = np.empty(M, dtype=np.float64)
|
||||
dot_prods[:-1] = v_in[:-1, 0] * v_in[1:, 0] + v_in[:-1, 1] * v_in[1:, 1]
|
||||
dot_prods[-1] = v_in[-1, 0] * v_in[0, 0] + v_in[-1, 1] * v_in[0, 1]
|
||||
|
||||
cos_angles = np.full(M, 1.0)
|
||||
denom = l_in[valid] * l_out[valid]
|
||||
cos_angles[valid] = dot_prods[valid] / denom
|
||||
|
||||
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))
|
||||
|
||||
M = len(path) - 1
|
||||
best_idx = find_path_seam(path)
|
||||
if best_idx == 0:
|
||||
return path
|
||||
|
||||
|
|
@ -283,6 +262,7 @@ def align_path_seam_sharpest(path: np.ndarray) -> np.ndarray:
|
|||
return res
|
||||
|
||||
|
||||
|
||||
class SlicePaths(list[np.ndarray]):
|
||||
groups: list[int]
|
||||
extrusion_rate: list[np.ndarray]
|
||||
|
|
@ -681,11 +661,10 @@ class SlicePaths(list[np.ndarray]):
|
|||
|
||||
def align_seams(self, mode: str = "sharpest") -> SlicePaths:
|
||||
"""
|
||||
Aligns the seams (starting point of closed paths) in SlicePaths.
|
||||
Supported modes: 'sharpest'.
|
||||
Uses 1-micron integer math.
|
||||
Aligns the seams (starting point of closed paths) in SlicePaths to the back (+Y).
|
||||
Preserves all per-path and per-vertex attributes.
|
||||
"""
|
||||
if not self or mode != "sharpest":
|
||||
if not self:
|
||||
return self
|
||||
|
||||
new_paths: list[np.ndarray] = []
|
||||
|
|
@ -702,9 +681,9 @@ class SlicePaths(list[np.ndarray]):
|
|||
new_extra[k].append(self.extra_attributes[k][i])
|
||||
continue
|
||||
|
||||
pts = p[:-1]
|
||||
M = len(pts)
|
||||
if M < 3:
|
||||
M = len(p) - 1
|
||||
best_idx = find_path_seam(p)
|
||||
if best_idx == 0:
|
||||
new_paths.append(p)
|
||||
new_ext.append(self.extrusion_rate[i])
|
||||
new_feed.append(self.feedrate[i])
|
||||
|
|
@ -712,60 +691,6 @@ class SlicePaths(list[np.ndarray]):
|
|||
new_extra[k].append(self.extra_attributes[k][i])
|
||||
continue
|
||||
|
||||
if _c_seam_lib is not None:
|
||||
try:
|
||||
c_pts = np.ascontiguousarray(pts, dtype=np.float64)
|
||||
stride = int(c_pts.shape[1])
|
||||
ptr = c_pts.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
|
||||
best_idx = int(_c_seam_lib.find_sharpest_seam_c(ptr, M, stride, float(SCALE)))
|
||||
except Exception:
|
||||
best_idx = 0
|
||||
else:
|
||||
pts_um = np.round(pts[:, :2] * SCALE).astype(np.int64)
|
||||
x = pts_um[:, 0]
|
||||
y = pts_um[:, 1]
|
||||
x_next = np.roll(x, -1)
|
||||
y_next = np.roll(y, -1)
|
||||
signed_area_um = np.sum(x * y_next - x_next * y)
|
||||
|
||||
prev_pts = np.roll(pts_um, 1, axis=0)
|
||||
next_pts = np.roll(pts_um, -1, axis=0)
|
||||
|
||||
v_in = pts_um - prev_pts
|
||||
v_out = next_pts - pts_um
|
||||
|
||||
l2_in = np.sum(v_in**2, axis=1)
|
||||
l2_out = np.sum(v_out**2, axis=1)
|
||||
|
||||
valid = (l2_in > 0) & (l2_out > 0)
|
||||
if not np.any(valid) or signed_area_um == 0:
|
||||
best_idx = 0
|
||||
else:
|
||||
cross_2d = v_in[:, 0] * v_out[:, 1] - v_in[:, 1] * v_out[:, 0]
|
||||
if signed_area_um > 0:
|
||||
concave_mask = valid & (cross_2d < 0)
|
||||
else:
|
||||
concave_mask = valid & (cross_2d > 0)
|
||||
|
||||
l_in = np.sqrt(l2_in.astype(np.float64))
|
||||
l_out = np.sqrt(l2_out.astype(np.float64))
|
||||
|
||||
cos_angles = np.full(M, 1.0)
|
||||
dot_prods = np.sum(v_in[valid] * v_out[valid], axis=1).astype(np.float64)
|
||||
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.empty_like(p)
|
||||
split = M - best_idx
|
||||
|
|
|
|||
|
|
@ -28,8 +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 using `manifold3d` for guaranteed watertight solid slicing (resolving coplanar and floating-point degeneracies) with a fallback to `trimesh` multiplane slicing for open sheets (supports `ignore_holes` option).
|
||||
- `_slice_single_mesh()`: Mesh slicer implementing `manifold3d` solid slicing and `trimesh` multiplane intersection with micro-epsilon perturbation at boundary faces.
|
||||
- `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.
|
||||
- `align_path_seam_sharpest()`: High-performance vectorized heuristic that positions the start vertex (seam) of closed polygon loops at the back of the object (+Y) with corner sharpness preference.
|
||||
- `SlicePaths.align_seams()`: Method on `SlicePaths` to align path seams to the back (+Y) 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, supports `ignore_holes`, open paths remain open).
|
||||
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
|||
- **Extrusion Multiplier Attribute**: Mesh attribute to use for per-segment extrusion rates.
|
||||
- **Feedrate Attribute**: Mesh attribute to use for per-segment feedrates.
|
||||
- **Extra Attributes**: Dictionary mapping mesh attribute names to custom G-code prefixes (e.g., `opening_size` -> `S`).
|
||||
- **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.
|
||||
- **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.
|
||||
- **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types.
|
||||
- **Buffer Shells Multiple**: Offsets 2D `SlicePaths` with multiple buffer passes.
|
||||
|
|
|
|||
|
|
@ -725,9 +725,7 @@ class TestSlice(unittest.TestCase):
|
|||
|
||||
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)
|
||||
# Right triangle with acute vertex at (10,0) and back vertex at (0, 5.7735)
|
||||
path = np.array([
|
||||
[0.0, 0.0, 0.0],
|
||||
[0.0, 5.7735, 0.0],
|
||||
|
|
@ -736,8 +734,8 @@ class TestSlice(unittest.TestCase):
|
|||
])
|
||||
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)
|
||||
# Back seam should be placed at the rear-most vertex (+Y) at (0.0, 5.7735)
|
||||
np.testing.assert_allclose(aligned[0, :2], [0.0, 5.7735], atol=1e-3)
|
||||
|
||||
def test_slicepaths_align_seams(self):
|
||||
from NodeSlicer.core.slice import SlicePaths
|
||||
|
|
@ -777,8 +775,9 @@ class TestSlice(unittest.TestCase):
|
|||
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])
|
||||
# Back seam should be placed on the rear corner (+Y) at (0.0, 10.0)
|
||||
np.testing.assert_allclose(aligned[0, :2], [0.0, 10.0])
|
||||
|
||||
|
||||
def test_intrinsics_retraction_feedrate(self):
|
||||
from NodeSlicer.core.nodeslicer.nodes.base import SliceIntrinsics
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |