diff --git a/core/c_seam.c b/core/c_seam.c index 72c5edb..897de47 100644 --- a/core/c_seam.c +++ b/core/c_seam.c @@ -86,3 +86,78 @@ EXPORT void align_path_seam_sharpest_c(const double* in_pts, int M, int stride, memcpy(out_pts, in_pts + (best_idx * stride), split * stride * sizeof(double)); memcpy(out_pts + (split * stride), in_pts, (best_idx + 1) * stride * sizeof(double)); } + +// Fast 64-bit hash for 3 float32 values (12 bytes) +static inline uint64_t hash_v3(const float* v) { + const uint32_t* u = (const uint32_t*)v; + uint64_t h = (uint64_t)u[0] * 0x9e3779b97f4a7c15ULL; + h ^= (uint64_t)u[1] * 0xbf58476d1ce4e5b9ULL; + h = (h ^ (h >> 30)) * 0x94d049bb133111ebULL; + h ^= (uint64_t)u[2] * 0x4bfb2f35a09a447fULL; + h = (h ^ (h >> 27)) * 0x94d049bb133111ebULL; + return h ^ (h >> 31); +} + +static inline int v3_eq(const float* a, const float* b) { + const uint32_t* ua = (const uint32_t*)a; + const uint32_t* ub = (const uint32_t*)b; + return (ua[0] == ub[0]) && (ua[1] == ub[1]) && (ua[2] == ub[2]); +} + +EXPORT int weld_vertices_c( + const float* in_verts, + int num_in_verts, + float* out_unique_verts, + uint32_t* out_inv_indices +) { + if (num_in_verts <= 0) return 0; + + uint32_t table_size = 1024; + while (table_size < (uint32_t)num_in_verts * 2) { + table_size <<= 1; + } + uint32_t mask = table_size - 1; + + uint32_t* table = (uint32_t*)calloc(table_size, sizeof(uint32_t)); + if (!table) return -1; + + int unique_count = 0; + + for (int i = 0; i < num_in_verts; ++i) { + const float* v = in_verts + (i * 3); + uint64_t h = hash_v3(v); + uint32_t slot = (uint32_t)(h & mask); + + while (1) { + uint32_t entry = table[slot]; + if (entry == 0) { + int uid = unique_count++; + out_unique_verts[uid * 3 + 0] = v[0]; + out_unique_verts[uid * 3 + 1] = v[1]; + out_unique_verts[uid * 3 + 2] = v[2]; + table[slot] = (uint32_t)(uid + 1); + out_inv_indices[i] = (uint32_t)uid; + break; + } + uint32_t uid = entry - 1; + const float* existing_v = out_unique_verts + (uid * 3); + if (v3_eq(v, existing_v)) { + out_inv_indices[i] = (uint32_t)uid; + break; + } + slot = (slot + 1) & mask; + } + } + + free(table); + return unique_count; +} + +EXPORT double polygon_signed_area_2d_c(const double* pts, int N, int stride) { + if (N < 3) return 0.0; + double area = pts[(N - 1) * stride + 0] * pts[1] - pts[0] * pts[(N - 1) * stride + 1]; + for (int i = 0; i < N - 1; ++i) { + area += pts[i * stride + 0] * pts[(i + 1) * stride + 1] - pts[(i + 1) * stride + 0] * pts[i * stride + 1]; + } + return area * 0.5; +} diff --git a/core/nodeslicer/nodes/path.py b/core/nodeslicer/nodes/path.py index 1ddb437..5363ebd 100644 --- a/core/nodeslicer/nodes/path.py +++ b/core/nodeslicer/nodes/path.py @@ -118,16 +118,17 @@ class SLICENODE_SliceShells(SliceNode): if merged_object.mesh is None or merged_object.mesh.size == 0: return SlicePaths() - if rotation and any(rotation): + has_rot = bool(rotation and any(rotation)) + if has_rot: from mathutils import Euler mat = np.array(Euler(rotation).to_matrix().inverted()) merged_object.mesh[:, :3, :] = merged_object.mesh[:, :3, :] @ mat.T merged_object.mesh[:, 3:4, :] = merged_object.mesh[:, 3:4, :] @ mat.T - min_z = float(merged_object.mesh[:, 0:3, 2:3].min()) + min_z = float(merged_object.mesh[:, :3, 2].min()) if abs(min_z) < 1e-6: min_z = 0.0 - max_z = float(merged_object.mesh[:, 0:3, 2:3].max()) + max_z = float(merged_object.mesh[:, :3, 2].max()) h_first = fl_height if fl_height is not None else layer_height @@ -141,8 +142,8 @@ class SLICENODE_SliceShells(SliceNode): if z_first_um <= max_z_um: if z_rest_start_um <= max_z_um and lh_um > 0: - z_um_values = list(range(z_rest_start_um, max_z_um + 1, lh_um)) - z_values = np.array([z_first_um] + z_um_values, dtype=np.float64) / 1000.0 + z_um_values = np.arange(z_rest_start_um, max_z_um + 1, lh_um, dtype=np.int64) + z_values = np.concatenate(([z_first_um], z_um_values)) / 1000.0 else: z_values = np.array([z_first_um], dtype=np.float64) / 1000.0 else: @@ -197,13 +198,14 @@ class SLICENODE_SliceShells(SliceNode): for i, p in enumerate(slices): if len(p) > 0 and int(round(p[0, 2] * 1000)) == z_first_um: slices.extrusion_rate[i] = slices.extrusion_rate[i] * mult + else: + break - if rotation and any(rotation): + if has_rot: from mathutils import Euler mat_inv = np.array(Euler(rotation).to_matrix()) slices = slices.transform(mat_inv) - - slices = slices.align_seams(mode="sharpest") + slices = slices.align_seams(mode="sharpest") return slices diff --git a/core/slice.py b/core/slice.py index 246070e..ecefb7b 100644 --- a/core/slice.py +++ b/core/slice.py @@ -113,7 +113,11 @@ try: _so_dir = os.path.dirname(os.path.abspath(__file__)) _so_path = os.path.join(_so_dir, "libseam_opt.so") _c_src = os.path.join(_so_dir, "c_seam.c") - if not os.path.exists(_so_path) and os.path.exists(_c_src): + rebuild = False + if os.path.exists(_c_src): + if not os.path.exists(_so_path) or os.path.getmtime(_so_path) < os.path.getmtime(_c_src): + rebuild = True + if rebuild: import subprocess for compiler in ("gcc", "clang"): try: @@ -131,10 +135,61 @@ try: ctypes.c_double ] _c_seam_lib.find_sharpest_seam_c.restype = ctypes.c_int + + if hasattr(_c_seam_lib, "weld_vertices_c"): + _c_seam_lib.weld_vertices_c.argtypes = [ + ctypes.POINTER(ctypes.c_float), + ctypes.c_int, + ctypes.POINTER(ctypes.c_float), + ctypes.POINTER(ctypes.c_uint32) + ] + _c_seam_lib.weld_vertices_c.restype = ctypes.c_int + + if hasattr(_c_seam_lib, "polygon_signed_area_2d_c"): + _c_seam_lib.polygon_signed_area_2d_c.argtypes = [ + ctypes.POINTER(ctypes.c_double), + ctypes.c_int, + ctypes.c_int + ] + _c_seam_lib.polygon_signed_area_2d_c.restype = ctypes.c_double except Exception: _c_seam_lib = None +def weld_vertices(tris: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """ + Given an array of triangles of shape (F, 3, 3) or (F, 4, 3), welds identical vertices. + Returns (unique_verts, faces) where unique_verts is (V, 3) float32 and faces is (F, 3) uint32. + """ + flat = np.ascontiguousarray(tris[:, :3, :].reshape(-1, 3), dtype=np.float32) + num_verts = len(flat) + if num_verts == 0: + return np.empty((0, 3), dtype=np.float32), np.empty((0, 3), dtype=np.uint32) + + if _c_seam_lib is not None and hasattr(_c_seam_lib, "weld_vertices_c"): + try: + out_unique = np.empty((num_verts, 3), dtype=np.float32) + out_inv = np.empty(num_verts, dtype=np.uint32) + unique_count = int(_c_seam_lib.weld_vertices_c( + flat.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), + num_verts, + out_unique.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), + out_inv.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)) + )) + if unique_count >= 0: + return out_unique[:unique_count], out_inv.reshape(-1, 3) + except Exception: + pass + + # NumPy fallback + void_dtype = np.dtype((np.void, flat.dtype.itemsize * flat.shape[1])) + flat_void = flat.view(void_dtype).ravel() + unique_voids, inv = np.unique(flat_void, return_inverse=True) + unique_verts = unique_voids.view(flat.dtype).reshape(-1, 3) + faces = inv.reshape(-1, 3).astype(np.uint32) + return unique_verts, faces + + 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) @@ -657,44 +712,53 @@ class SlicePaths(list[np.ndarray]): new_extra[k].append(self.extra_attributes[k][i]) continue - 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 + 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: - 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) + 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: - concave_mask = valid & (cross_2d > 0) + 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)) + 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]) + 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 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) @@ -703,25 +767,37 @@ class SlicePaths(list[np.ndarray]): for k in self.extra_attributes: new_extra[k].append(self.extra_attributes[k][i]) else: - new_p = np.vstack([p[best_idx:-1], p[:best_idx + 1]]) + new_p = np.empty_like(p) + split = M - best_idx + new_p[:split] = p[best_idx:-1] + new_p[split:] = p[:best_idx + 1] new_paths.append(new_p) ext = self.extrusion_rate[i] if len(ext) == len(p): - new_ext.append(np.concatenate([ext[best_idx:-1], ext[:best_idx + 1]])) + new_e = np.empty_like(ext) + new_e[:split] = ext[best_idx:-1] + new_e[split:] = ext[:best_idx + 1] + new_ext.append(new_e) else: new_ext.append(ext) feed = self.feedrate[i] if len(feed) == len(p): - new_feed.append(np.concatenate([feed[best_idx:-1], feed[:best_idx + 1]])) + new_f = np.empty_like(feed) + new_f[:split] = feed[best_idx:-1] + new_f[split:] = feed[:best_idx + 1] + new_feed.append(new_f) else: new_feed.append(feed) for k in self.extra_attributes: attr = self.extra_attributes[k][i] if len(attr) == len(p): - new_extra[k].append(np.concatenate([attr[best_idx:-1], attr[:best_idx + 1]])) + new_a = np.empty_like(attr) + new_a[:split] = attr[best_idx:-1] + new_a[split:] = attr[:best_idx + 1] + new_extra[k].append(new_a) else: new_extra[k].append(attr) @@ -740,13 +816,9 @@ def _slice_single_mesh(mesh_array: np.ndarray, z_values: np.ndarray | list[float if len(z_arr) == 0: return [] - tris = mesh_array[:, :3, :] # Shape (F, 3, 3) - flat = np.ascontiguousarray(tris.reshape(-1, 3), dtype=np.float32) - void_dtype = np.dtype((np.void, flat.dtype.itemsize * flat.shape[1])) - flat_void = flat.view(void_dtype).ravel() - unique_voids, inv = np.unique(flat_void, return_inverse=True) - unique_verts = unique_voids.view(flat.dtype).reshape(-1, 3) - faces = inv.reshape(-1, 3).astype(np.uint32) + unique_verts, faces = weld_vertices(mesh_array) + if len(unique_verts) == 0: + return [] all_paths: list[np.ndarray] = [] @@ -783,11 +855,17 @@ def _slice_single_mesh(mesh_array: np.ndarray, z_values: np.ndarray | list[float if N < 3: continue if ignore_holes: - p_x = poly[:, 0] - p_y = poly[:, 1] - signed_area = np.sum(p_x[:-1] * p_y[1:] - p_x[1:] * p_y[:-1]) + (p_x[-1] * p_y[0] - p_x[0] * p_y[-1]) - if signed_area <= 0: - continue + if _c_seam_lib is not None and hasattr(_c_seam_lib, "polygon_signed_area_2d_c"): + poly_c = np.ascontiguousarray(poly, dtype=np.float64) + area = _c_seam_lib.polygon_signed_area_2d_c(poly_c.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), N, 2) + if area <= 0: + continue + else: + p_x = poly[:, 0] + p_y = poly[:, 1] + signed_area = np.sum(p_x[:-1] * p_y[1:] - p_x[1:] * p_y[:-1]) + (p_x[-1] * p_y[0] - p_x[0] * p_y[-1]) + if signed_area <= 0: + continue p3d = np.empty((N + 1, 3), dtype=np.float64) p3d[:N, :2] = poly p3d[:N, 2] = zf @@ -849,13 +927,15 @@ def link_segments(segments: np.ndarray) -> list[np.ndarray]: z = segments[0, 0, 2] pts = segments[:, :, :2] - # Quantize to integer micron tuples: (x_um, y_um) + # Quantize to integer micron: (x_um, y_um) packed into 64-bit scalar key pts_um = np.round(pts * SCALE).astype(np.int64) - - adj: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = {} + k1_arr = (pts_um[:, 0, 0] << 32) | (pts_um[:, 0, 1] & 0xFFFFFFFF) + k2_arr = (pts_um[:, 1, 0] << 32) | (pts_um[:, 1, 1] & 0xFFFFFFFF) + + adj: dict[int, list[tuple[int, int]]] = {} for i in range(len(segments)): - k1 = (int(pts_um[i, 0, 0]), int(pts_um[i, 0, 1])) - k2 = (int(pts_um[i, 1, 0]), int(pts_um[i, 1, 1])) + k1 = int(k1_arr[i]) + k2 = int(k2_arr[i]) if k1 == k2: continue adj.setdefault(k1, []).append((k2, i)) @@ -866,12 +946,13 @@ def link_segments(segments: np.ndarray) -> list[np.ndarray]: for i in range(len(segments)): if i in used: continue - k1 = (int(pts_um[i, 0, 0]), int(pts_um[i, 0, 1])) - k2 = (int(pts_um[i, 1, 0]), int(pts_um[i, 1, 1])) + k1 = int(k1_arr[i]) + k2 = int(k2_arr[i]) if k1 == k2: continue - path_pts = [pts[i, 0], pts[i, 1]] + fwd_pts = [pts[i, 0], pts[i, 1]] + bwd_pts = [] used.add(i) # Forward @@ -880,9 +961,9 @@ def link_segments(segments: np.ndarray) -> list[np.ndarray]: found = False for next_k, idx in adj.get(curr_k, []): if idx not in used: - pk1 = (int(pts_um[idx, 0, 0]), int(pts_um[idx, 0, 1])) + pk1 = int(k1_arr[idx]) other_pt = pts[idx, 1] if pk1 == curr_k else pts[idx, 0] - path_pts.append(other_pt) + fwd_pts.append(other_pt) used.add(idx) curr_k = next_k found = True @@ -890,15 +971,15 @@ def link_segments(segments: np.ndarray) -> list[np.ndarray]: if not found: break - # Backward + # Backward (O(1) append to bwd_pts) curr_k = k1 while True: found = False for next_k, idx in adj.get(curr_k, []): if idx not in used: - pk1 = (int(pts_um[idx, 0, 0]), int(pts_um[idx, 0, 1])) + pk1 = int(k1_arr[idx]) other_pt = pts[idx, 1] if pk1 == curr_k else pts[idx, 0] - path_pts.insert(0, other_pt) + bwd_pts.append(other_pt) used.add(idx) curr_k = next_k found = True @@ -906,12 +987,15 @@ def link_segments(segments: np.ndarray) -> list[np.ndarray]: if not found: break - p_arr = np.array(path_pts) + all_pts = bwd_pts[::-1] + fwd_pts if bwd_pts else fwd_pts + p_arr = np.empty((len(all_pts), 3), dtype=np.float64) + p_arr[:, :2] = all_pts + p_arr[:, 2] = z + if len(p_arr) > 2 and is_closed_path(p_arr): p_arr[-1] = p_arr[0] - res = np.column_stack([p_arr, np.full(len(p_arr), z)]) - paths.append(res) + paths.append(p_arr) return paths @ndarray_lru_cache(maxsize=128)