This commit is contained in:
Nicolas Predella 2026-10-07 15:47:07 +03:00
parent a726825eed
commit 45d47161f8
3 changed files with 235 additions and 74 deletions

View file

@ -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;
}

View file

@ -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

View file

@ -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)