perf
This commit is contained in:
parent
8ba3ea16c4
commit
8ceec61e76
8 changed files with 367 additions and 69 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -5,3 +5,6 @@ release/
|
||||||
*.blend
|
*.blend
|
||||||
*.blend1
|
*.blend1
|
||||||
*.bgcode
|
*.bgcode
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
|
||||||
16
build.sh
16
build.sh
|
|
@ -8,18 +8,32 @@ TMPDIR="$(mktemp -d)"
|
||||||
cleanup() { rm -rf "$TMPDIR"; }
|
cleanup() { rm -rf "$TMPDIR"; }
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if command -v gcc >/dev/null 2>&1; then
|
||||||
|
gcc -O3 -fPIC -shared "$SCRIPT_DIR/core/c_seam.c" -o "$SCRIPT_DIR/core/libseam_opt.so" -lm 2>/dev/null || true
|
||||||
|
elif command -v clang >/dev/null 2>&1; then
|
||||||
|
clang -O3 -fPIC -shared "$SCRIPT_DIR/core/c_seam.c" -o "$SCRIPT_DIR/core/libseam_opt.so" -lm 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
mapfile -t FILES < <(git ls-files -co --exclude-standard \
|
mapfile -t FILES < <(git ls-files -co --exclude-standard \
|
||||||
| grep -v -xE '(\.gitignore|build\.sh)' \
|
| grep -v -xE '(\.gitignore|build\.sh)' \
|
||||||
| while read -r f; do [ -e "$f" ] && echo "$f"; done)
|
| while read -r f; do [ -e "$f" ] && echo "$f"; done)
|
||||||
|
|
||||||
tar -cf - "${FILES[@]}" | tar -xf - -C "$TMPDIR"
|
tar -cf - "${FILES[@]}" | tar -xf - -C "$TMPDIR"
|
||||||
|
if [ -f "$SCRIPT_DIR/core/libseam_opt.so" ]; then
|
||||||
|
cp "$SCRIPT_DIR/core/libseam_opt.so" "$TMPDIR/core/libseam_opt.so"
|
||||||
|
fi
|
||||||
if [ -d wheels ]; then
|
if [ -d wheels ]; then
|
||||||
mkdir -p "$TMPDIR/wheels"
|
mkdir -p "$TMPDIR/wheels"
|
||||||
cp -r wheels/* "$TMPDIR/wheels/" 2>/dev/null || true
|
cp -r wheels/* "$TMPDIR/wheels/" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
BLENDER="${BLENDER:-/home/nicolas/Applications/blender-5.1.0-linux-x64/blender}"
|
||||||
|
if [ ! -x "$BLENDER" ]; then
|
||||||
|
BLENDER="$(command -v blender)"
|
||||||
|
fi
|
||||||
|
|
||||||
cd "$TMPDIR"
|
cd "$TMPDIR"
|
||||||
/home/nicolas/Applications/blender-5.1.0-linux-x64/blender \
|
"$BLENDER" \
|
||||||
--command extension build --split-platforms \
|
--command extension build --split-platforms \
|
||||||
--output-filepath "$SCRIPT_DIR/release/NodeSlicer_0.0.1.zip"
|
--output-filepath "$SCRIPT_DIR/release/NodeSlicer_0.0.1.zip"
|
||||||
|
|
||||||
|
|
|
||||||
88
core/c_seam.c
Normal file
88
core/c_seam.c
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#define EXPORT __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define EXPORT __attribute__((visibility("default")))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int best_idx = 0;
|
||||||
|
double min_cos_concave = 2.0;
|
||||||
|
int best_concave_idx = -1;
|
||||||
|
double min_cos_overall = 2.0;
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
int64_t l2_in = vin_x * vin_x + vin_y * vin_y;
|
||||||
|
int64_t l2_out = vout_x * vout_x + vout_y * vout_y;
|
||||||
|
|
||||||
|
if (l2_in == 0 || l2_out == 0) continue;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cos_ang < min_cos_overall) {
|
||||||
|
min_cos_overall = cos_ang;
|
||||||
|
best_idx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (M > 1024) { free(x); free(y); }
|
||||||
|
|
||||||
|
if (best_concave_idx >= 0) return best_concave_idx;
|
||||||
|
return best_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPORT void align_path_seam_sharpest_c(const double* in_pts, int M, int stride, double* out_pts, double scale) {
|
||||||
|
int best_idx = find_sharpest_seam_c(in_pts, M, stride, scale);
|
||||||
|
if (best_idx == 0) {
|
||||||
|
memcpy(out_pts, in_pts, (M + 1) * stride * sizeof(double));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int split = M - best_idx;
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,9 @@ def crc32_list(l: list[np.ndarray]) -> int:
|
||||||
if not l: return 0
|
if not l: return 0
|
||||||
h = 0
|
h = 0
|
||||||
for a in l:
|
for a in l:
|
||||||
h = zlib.adler32(struct.pack(">I", crc32_array(a)), h)
|
if a.size:
|
||||||
|
ac = np.ascontiguousarray(a)
|
||||||
|
h = zlib.adler32(ac.data, h)
|
||||||
return h
|
return h
|
||||||
|
|
||||||
def euler_rotation_matrix(rx: float, ry: float, rz: float) -> np.ndarray:
|
def euler_rotation_matrix(rx: float, ry: float, rz: float) -> np.ndarray:
|
||||||
|
|
|
||||||
297
core/slice.py
297
core/slice.py
|
|
@ -20,7 +20,8 @@ def is_closed_path(path: np.ndarray) -> bool:
|
||||||
"""Checks if a 2D or 3D path forms a closed loop in integer micron space (1 micron unit)."""
|
"""Checks if a 2D or 3D path forms a closed loop in integer micron space (1 micron unit)."""
|
||||||
if len(path) < 3:
|
if len(path) < 3:
|
||||||
return False
|
return False
|
||||||
return bool(np.all(np.round(path[0, :2] * SCALE) == np.round(path[-1, :2] * SCALE)))
|
return (int(round(float(path[0, 0]) * SCALE)) == int(round(float(path[-1, 0]) * SCALE)) and
|
||||||
|
int(round(float(path[0, 1]) * SCALE)) == int(round(float(path[-1, 1]) * SCALE)))
|
||||||
|
|
||||||
def _transfer_path_attr(src_attr: np.ndarray, orig_pts: np.ndarray, new_pts: np.ndarray) -> np.ndarray:
|
def _transfer_path_attr(src_attr: np.ndarray, orig_pts: np.ndarray, new_pts: np.ndarray) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
|
|
@ -34,10 +35,10 @@ def _transfer_path_attr(src_attr: np.ndarray, orig_pts: np.ndarray, new_pts: np.
|
||||||
return np.zeros(0, dtype=src_attr.dtype if hasattr(src_attr, 'dtype') else float)
|
return np.zeros(0, dtype=src_attr.dtype if hasattr(src_attr, 'dtype') else float)
|
||||||
if N == 0:
|
if N == 0:
|
||||||
return np.zeros(M, dtype=src_attr.dtype if hasattr(src_attr, 'dtype') else float)
|
return np.zeros(M, dtype=src_attr.dtype if hasattr(src_attr, 'dtype') else float)
|
||||||
if N == M and is_closed_path(orig_pts) == is_closed_path(new_pts) and np.all(np.round(orig_pts * SCALE) == np.round(new_pts * SCALE)):
|
if len(src_attr) > 0 and (len(src_attr) == 1 or np.all(src_attr == src_attr[0])):
|
||||||
return src_attr.copy()
|
|
||||||
if len(src_attr) > 0 and np.all(src_attr == src_attr[0]):
|
|
||||||
return np.full(M, src_attr[0], dtype=src_attr.dtype if hasattr(src_attr, 'dtype') else float)
|
return np.full(M, src_attr[0], dtype=src_attr.dtype if hasattr(src_attr, 'dtype') else float)
|
||||||
|
if N == M and is_closed_path(orig_pts) == is_closed_path(new_pts) and np.all(np.rint(orig_pts * SCALE) == np.rint(new_pts * SCALE)):
|
||||||
|
return src_attr.copy()
|
||||||
|
|
||||||
orig_diffs = np.diff(orig_pts[:, :3], axis=0)
|
orig_diffs = np.diff(orig_pts[:, :3], axis=0)
|
||||||
orig_dists = np.sqrt((orig_diffs ** 2).sum(axis=1))
|
orig_dists = np.sqrt((orig_diffs ** 2).sum(axis=1))
|
||||||
|
|
@ -75,12 +76,16 @@ def _transfer_group_attributes(src_sp: SlicePaths, res_paths: list[np.ndarray],
|
||||||
for idx, g in enumerate(src_groups):
|
for idx, g in enumerate(src_groups):
|
||||||
src_group_map.setdefault(g, []).append(idx)
|
src_group_map.setdefault(g, []).append(idx)
|
||||||
|
|
||||||
|
src_z_keys: list[int] | None = None
|
||||||
|
if len(src_sp) > 0:
|
||||||
|
src_z_keys = [int(round(float(src_sp[i][0, 2]) * SCALE)) if len(src_sp[i]) > 0 and src_sp[i].shape[1] > 2 else 0 for i in range(len(src_sp))]
|
||||||
|
|
||||||
for idx, (p, g) in enumerate(zip(res_paths, res_groups)):
|
for idx, (p, g) in enumerate(zip(res_paths, res_groups)):
|
||||||
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 and src_z_keys is not None:
|
||||||
z_um = int(round(p[0, 2] * SCALE)) if len(p) > 0 else 0
|
z_um = int(round(float(p[0, 2]) * SCALE)) if len(p) > 0 and p.shape[1] > 2 else 0
|
||||||
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'))]
|
src_indices = [min(range(len(src_sp)), key=lambda i: abs(src_z_keys[i] - z_um))]
|
||||||
|
|
||||||
if src_indices:
|
if src_indices:
|
||||||
src_i = src_indices[0]
|
src_i = src_indices[0]
|
||||||
|
|
@ -100,6 +105,36 @@ def _transfer_group_attributes(src_sp: SlicePaths, res_paths: list[np.ndarray],
|
||||||
return out_ext, out_feed, out_extra
|
return out_ext, out_feed, out_extra
|
||||||
|
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import os
|
||||||
|
|
||||||
|
_c_seam_lib = None
|
||||||
|
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):
|
||||||
|
import subprocess
|
||||||
|
for compiler in ("gcc", "clang"):
|
||||||
|
try:
|
||||||
|
subprocess.run([compiler, "-O3", "-fPIC", "-shared", _c_src, "-o", _so_path, "-lm"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
if os.path.exists(_so_path):
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if os.path.exists(_so_path):
|
||||||
|
_c_seam_lib = ctypes.CDLL(_so_path)
|
||||||
|
_c_seam_lib.find_sharpest_seam_c.argtypes = [
|
||||||
|
ctypes.POINTER(ctypes.c_double),
|
||||||
|
ctypes.c_int,
|
||||||
|
ctypes.c_int,
|
||||||
|
ctypes.c_double
|
||||||
|
]
|
||||||
|
_c_seam_lib.find_sharpest_seam_c.restype = ctypes.c_int
|
||||||
|
except Exception:
|
||||||
|
_c_seam_lib = None
|
||||||
|
|
||||||
|
|
||||||
def align_path_seam_sharpest(path: np.ndarray) -> np.ndarray:
|
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)
|
Reorders a closed 2D/3D polygon path (N, 3) or (N, 2) so that its starting vertex (seam)
|
||||||
|
|
@ -120,40 +155,62 @@ def align_path_seam_sharpest(path: np.ndarray) -> np.ndarray:
|
||||||
if M < 3:
|
if M < 3:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
pts_um = np.round(pts[:, :2] * SCALE).astype(np.int64)
|
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
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
pts_um = np.rint(pts[:, :2] * SCALE).astype(np.int64)
|
||||||
x = pts_um[:, 0]
|
x = pts_um[:, 0]
|
||||||
y = pts_um[:, 1]
|
y = pts_um[:, 1]
|
||||||
x_next = np.roll(x, -1)
|
signed_area_um = np.sum(x[:-1] * y[1:] - x[1:] * y[:-1]) + (x[-1] * y[0] - x[0] * y[-1])
|
||||||
y_next = np.roll(y, -1)
|
|
||||||
signed_area_um = np.sum(x * y_next - x_next * y)
|
|
||||||
if signed_area_um == 0:
|
if signed_area_um == 0:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
prev_pts = np.roll(pts_um, 1, axis=0)
|
v_in = np.empty((M, 2), dtype=np.int64)
|
||||||
next_pts = np.roll(pts_um, -1, axis=0)
|
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]
|
||||||
|
|
||||||
v_in = pts_um - prev_pts
|
l2_in = v_in[:, 0]**2 + v_in[:, 1]**2
|
||||||
v_out = next_pts - pts_um
|
valid_in = l2_in > 0
|
||||||
|
valid_out = np.empty_like(valid_in)
|
||||||
l2_in = np.sum(v_in**2, axis=1)
|
valid_out[:-1] = valid_in[1:]
|
||||||
l2_out = np.sum(v_out**2, axis=1)
|
valid_out[-1] = valid_in[0]
|
||||||
|
valid = valid_in & valid_out
|
||||||
valid = (l2_in > 0) & (l2_out > 0)
|
|
||||||
if not np.any(valid):
|
if not np.any(valid):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
cross_2d = v_in[:, 0] * v_out[:, 1] - v_in[:, 1] * v_out[:, 0]
|
cross_2d = np.empty(M, dtype=np.int64)
|
||||||
if signed_area_um > 0:
|
cross_2d[:-1] = v_in[:-1, 0] * v_in[1:, 1] - v_in[:-1, 1] * v_in[1:, 0]
|
||||||
concave_mask = valid & (cross_2d < 0)
|
cross_2d[-1] = v_in[-1, 0] * v_in[0, 1] - v_in[-1, 1] * v_in[0, 0]
|
||||||
else:
|
|
||||||
concave_mask = valid & (cross_2d > 0)
|
|
||||||
|
|
||||||
l_in = np.sqrt(l2_in.astype(np.float64))
|
concave_mask = valid & (cross_2d < 0) if signed_area_um > 0 else valid & (cross_2d > 0)
|
||||||
l_out = np.sqrt(l2_out.astype(np.float64))
|
|
||||||
|
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)
|
cos_angles = np.full(M, 1.0)
|
||||||
dot_prods = np.sum(v_in[valid] * v_out[valid], axis=1).astype(np.float64)
|
denom = l_in[valid] * l_out[valid]
|
||||||
cos_angles[valid] = dot_prods / (l_in[valid] * l_out[valid])
|
cos_angles[valid] = dot_prods[valid] / denom
|
||||||
|
|
||||||
if np.any(concave_mask):
|
if np.any(concave_mask):
|
||||||
concave_indices = np.where(concave_mask)[0]
|
concave_indices = np.where(concave_mask)[0]
|
||||||
|
|
@ -164,7 +221,11 @@ def align_path_seam_sharpest(path: np.ndarray) -> np.ndarray:
|
||||||
if best_idx == 0:
|
if best_idx == 0:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
return np.vstack([path[best_idx:-1], path[:best_idx + 1]])
|
res = np.empty_like(path)
|
||||||
|
split = M - best_idx
|
||||||
|
res[:split] = path[best_idx:-1]
|
||||||
|
res[split:] = path[:best_idx + 1]
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
class SlicePaths(list[np.ndarray]):
|
class SlicePaths(list[np.ndarray]):
|
||||||
|
|
@ -260,12 +321,21 @@ class SlicePaths(list[np.ndarray]):
|
||||||
if not self or count <= 0:
|
if not self or count <= 0:
|
||||||
return self.copy_with([])
|
return self.copy_with([])
|
||||||
|
|
||||||
all_buffers: list[SlicePaths] = []
|
p = self
|
||||||
for i in range(count):
|
if rotation and any(rotation):
|
||||||
dist = base_distance + (i * width_step)
|
from mathutils import Euler
|
||||||
all_buffers.append(self.buffer(dist, miter=miter, rotation=rotation))
|
mat = Euler(rotation).to_matrix().inverted()
|
||||||
|
p = self.transform(np.array(mat))
|
||||||
|
|
||||||
return SlicePaths.zip_paths(all_buffers, primary_source=self)
|
res_paths, res_groups = buffer_multiple_slice(p, base_distance, count, width_step, miter=miter)
|
||||||
|
new_paths = self.copy_with(res_paths, res_groups)
|
||||||
|
|
||||||
|
if rotation and any(rotation):
|
||||||
|
from mathutils import Euler
|
||||||
|
mat_inv = Euler(rotation).to_matrix()
|
||||||
|
new_paths = new_paths.transform(np.array(mat_inv))
|
||||||
|
|
||||||
|
return new_paths
|
||||||
|
|
||||||
def buffer_curve(self, distance: float, cap_style: str = 'round', join_style: str = 'round', single_sided: bool = False, rotation: Any = None) -> SlicePaths:
|
def buffer_curve(self, distance: float, cap_style: str = 'round', join_style: str = 'round', single_sided: bool = False, rotation: Any = None) -> SlicePaths:
|
||||||
"""Buffers open curves using shapely. Handles optional rotation for non-planar buffering."""
|
"""Buffers open curves using shapely. Handles optional rotation for non-planar buffering."""
|
||||||
|
|
@ -709,13 +779,19 @@ def _slice_single_mesh(mesh_array: np.ndarray, z_values: np.ndarray | list[float
|
||||||
cs = m.slice(slice_z)
|
cs = m.slice(slice_z)
|
||||||
polys = cs.to_polygons()
|
polys = cs.to_polygons()
|
||||||
for poly in polys:
|
for poly in polys:
|
||||||
if len(poly) < 3:
|
N = len(poly)
|
||||||
|
if N < 3:
|
||||||
continue
|
continue
|
||||||
pts_scaled = (poly * SCALE).round().astype(np.int64).tolist()
|
if ignore_holes:
|
||||||
if ignore_holes and not pyclipper.Orientation(pts_scaled):
|
p_x = poly[:, 0]
|
||||||
continue
|
p_y = poly[:, 1]
|
||||||
p3d = np.column_stack([poly.astype(np.float64), np.full(len(poly), zf, dtype=np.float64)])
|
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])
|
||||||
p3d = np.vstack([p3d, p3d[0]])
|
if signed_area <= 0:
|
||||||
|
continue
|
||||||
|
p3d = np.empty((N + 1, 3), dtype=np.float64)
|
||||||
|
p3d[:N, :2] = poly
|
||||||
|
p3d[:N, 2] = zf
|
||||||
|
p3d[N] = p3d[0]
|
||||||
p3d = align_path_seam_sharpest(p3d)
|
p3d = align_path_seam_sharpest(p3d)
|
||||||
all_paths.append(p3d)
|
all_paths.append(p3d)
|
||||||
return all_paths
|
return all_paths
|
||||||
|
|
@ -1590,12 +1666,18 @@ def geometry_to_paths(geom: Any, z: float) -> list[np.ndarray]:
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
if isinstance(geom, Polygon):
|
if isinstance(geom, Polygon):
|
||||||
ext_coords = np.array(geom.exterior.coords)
|
ext_coords = np.asarray(geom.exterior.coords, dtype=np.float64)
|
||||||
ext_coords_3d = np.column_stack([ext_coords, np.full(len(ext_coords), z)])
|
N = len(ext_coords)
|
||||||
|
ext_coords_3d = np.empty((N, 3), dtype=np.float64)
|
||||||
|
ext_coords_3d[:, :2] = ext_coords
|
||||||
|
ext_coords_3d[:, 2] = z
|
||||||
paths.append(ext_coords_3d)
|
paths.append(ext_coords_3d)
|
||||||
for interior in geom.interiors:
|
for interior in geom.interiors:
|
||||||
int_coords = np.array(interior.coords)
|
int_coords = np.asarray(interior.coords, dtype=np.float64)
|
||||||
int_coords_3d = np.column_stack([int_coords, np.full(len(int_coords), z)])
|
M = len(int_coords)
|
||||||
|
int_coords_3d = np.empty((M, 3), dtype=np.float64)
|
||||||
|
int_coords_3d[:, :2] = int_coords
|
||||||
|
int_coords_3d[:, 2] = z
|
||||||
paths.append(int_coords_3d)
|
paths.append(int_coords_3d)
|
||||||
elif isinstance(geom, (MultiPolygon, GeometryCollection)):
|
elif isinstance(geom, (MultiPolygon, GeometryCollection)):
|
||||||
for poly in geom.geoms:
|
for poly in geom.geoms:
|
||||||
|
|
@ -1663,13 +1745,19 @@ def offset_geometry_to_paths(geom: Any, z: float) -> list[np.ndarray]:
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
if isinstance(geom, (LineString, LinearRing)):
|
if isinstance(geom, (LineString, LinearRing)):
|
||||||
coords = np.array(geom.coords)
|
coords = np.asarray(geom.coords, dtype=np.float64)
|
||||||
coords_3d = np.column_stack([coords, np.full(len(coords), z)])
|
N = len(coords)
|
||||||
|
coords_3d = np.empty((N, 3), dtype=np.float64)
|
||||||
|
coords_3d[:, :2] = coords
|
||||||
|
coords_3d[:, 2] = z
|
||||||
paths.append(coords_3d)
|
paths.append(coords_3d)
|
||||||
elif isinstance(geom, MultiLineString):
|
elif isinstance(geom, MultiLineString):
|
||||||
for line in geom.geoms:
|
for line in geom.geoms:
|
||||||
coords = np.array(line.coords)
|
coords = np.asarray(line.coords, dtype=np.float64)
|
||||||
coords_3d = np.column_stack([coords, np.full(len(coords), z)])
|
N = len(coords)
|
||||||
|
coords_3d = np.empty((N, 3), dtype=np.float64)
|
||||||
|
coords_3d[:, :2] = coords
|
||||||
|
coords_3d[:, 2] = z
|
||||||
paths.append(coords_3d)
|
paths.append(coords_3d)
|
||||||
elif isinstance(geom, GeometryCollection):
|
elif isinstance(geom, GeometryCollection):
|
||||||
for sub_geom in geom.geoms:
|
for sub_geom in geom.geoms:
|
||||||
|
|
@ -1929,36 +2017,41 @@ def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round',
|
||||||
paths_list: list[np.ndarray] = []
|
paths_list: list[np.ndarray] = []
|
||||||
groups_list: list[int] = []
|
groups_list: list[int] = []
|
||||||
pco = pyclipper.PyclipperOffset(arc_tolerance=int(round(tolerance * SCALE))) # pyright: ignore[reportAttributeAccessIssue]
|
pco = pyclipper.PyclipperOffset(arc_tolerance=int(round(tolerance * SCALE))) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
scale_inv = 1.0 / SCALE
|
||||||
|
delta_scaled = int(round(distance * SCALE))
|
||||||
|
|
||||||
# Buffer each layer independently
|
# Buffer each layer independently
|
||||||
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 = float(z_key) / SCALE
|
z = float(z_key) * scale_inv
|
||||||
|
|
||||||
pco.Clear()
|
pco.Clear()
|
||||||
scaled_paths = []
|
scaled_paths = []
|
||||||
for p in layer_paths:
|
for p in layer_paths:
|
||||||
path_pts = p[:, :2]
|
if len(p) < 3:
|
||||||
# Strip redundant closing points
|
continue
|
||||||
if is_closed_path(p):
|
path_pts = p[:-1, :2] if is_closed_path(p) else p[:, :2]
|
||||||
path_pts = path_pts[:-1]
|
if len(path_pts) < 3:
|
||||||
|
continue
|
||||||
if len(path_pts) < 3: continue
|
scaled_paths.append(np.rint(path_pts * SCALE).astype(np.int64).tolist())
|
||||||
scaled_paths.append((path_pts * SCALE).round().astype(np.int64).tolist())
|
|
||||||
|
|
||||||
if not scaled_paths: continue
|
if not scaled_paths:
|
||||||
|
continue
|
||||||
|
|
||||||
# Add all paths for this specific Z layer
|
# Add all paths for this specific Z layer
|
||||||
pco.AddPaths(scaled_paths, jt, pyclipper.ET_CLOSEDPOLYGON) # pyright: ignore[reportAttributeAccessIssue]
|
pco.AddPaths(scaled_paths, jt, pyclipper.ET_CLOSEDPOLYGON) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
solution = pco.Execute(int(round(distance * SCALE)))
|
solution = pco.Execute(delta_scaled)
|
||||||
|
|
||||||
for path in solution:
|
for path in solution:
|
||||||
if not path: continue
|
if not path:
|
||||||
fpath = np.array(path, dtype=np.float64) / SCALE
|
continue
|
||||||
fpath_3d = np.column_stack([fpath, np.full(len(fpath), z)])
|
N = len(path)
|
||||||
# Re-close loop
|
fpath_3d = np.empty((N + 1, 3), dtype=np.float64)
|
||||||
fpath_3d = np.vstack([fpath_3d, fpath_3d[0]])
|
arr = np.asarray(path, dtype=np.float64)
|
||||||
|
fpath_3d[:N, :2] = arr * scale_inv
|
||||||
|
fpath_3d[:N, 2] = z
|
||||||
|
fpath_3d[N] = fpath_3d[0]
|
||||||
fpath_3d = align_path_seam_sharpest(fpath_3d)
|
fpath_3d = align_path_seam_sharpest(fpath_3d)
|
||||||
paths_list.append(fpath_3d)
|
paths_list.append(fpath_3d)
|
||||||
groups_list.append(group_id)
|
groups_list.append(group_id)
|
||||||
|
|
@ -1967,6 +2060,80 @@ def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round',
|
||||||
|
|
||||||
return paths_list, groups_list
|
return paths_list, groups_list
|
||||||
|
|
||||||
|
|
||||||
|
@ndarray_lru_cache(maxsize=64)
|
||||||
|
def buffer_multiple_slice(
|
||||||
|
slice_paths: list[np.ndarray],
|
||||||
|
base_distance: float,
|
||||||
|
count: int,
|
||||||
|
width_step: float,
|
||||||
|
miter: str = 'round',
|
||||||
|
tolerance: float = 0.01
|
||||||
|
) -> tuple[list[np.ndarray], list[int]]:
|
||||||
|
"""Generates multiple concentric buffers per layer in a single pass."""
|
||||||
|
if not slice_paths or count <= 0:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
groups: dict[int, list[np.ndarray]] = {}
|
||||||
|
for p in slice_paths:
|
||||||
|
z = p[0, 2]
|
||||||
|
z_key = int(round(z * SCALE))
|
||||||
|
groups.setdefault(z_key, []).append(p)
|
||||||
|
|
||||||
|
join_types = {
|
||||||
|
'round': pyclipper.JT_ROUND, # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
'square': pyclipper.JT_SQUARE, # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
'flat': pyclipper.JT_SQUARE, # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
'miter': pyclipper.JT_MITER # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
}
|
||||||
|
jt = join_types.get(miter, pyclipper.JT_ROUND) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
|
paths_list: list[np.ndarray] = []
|
||||||
|
groups_list: list[int] = []
|
||||||
|
pco = pyclipper.PyclipperOffset(arc_tolerance=int(round(tolerance * SCALE))) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
|
scaled_dists = [int(round((base_distance + i * width_step) * SCALE)) for i in range(count)]
|
||||||
|
scale_inv = 1.0 / SCALE
|
||||||
|
|
||||||
|
group_id = 0
|
||||||
|
for z_key in sorted(groups.keys()):
|
||||||
|
layer_paths = groups[z_key]
|
||||||
|
z = float(z_key) * scale_inv
|
||||||
|
|
||||||
|
pco.Clear()
|
||||||
|
scaled_paths = []
|
||||||
|
for p in layer_paths:
|
||||||
|
if len(p) < 3:
|
||||||
|
continue
|
||||||
|
path_pts = p[:-1, :2] if is_closed_path(p) else p[:, :2]
|
||||||
|
if len(path_pts) < 3:
|
||||||
|
continue
|
||||||
|
scaled_paths.append(np.rint(path_pts * SCALE).astype(np.int64).tolist())
|
||||||
|
|
||||||
|
if not scaled_paths:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pco.AddPaths(scaled_paths, jt, pyclipper.ET_CLOSEDPOLYGON) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
|
for d_scaled in scaled_dists:
|
||||||
|
solution = pco.Execute(d_scaled)
|
||||||
|
for path in solution:
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
N = len(path)
|
||||||
|
fpath_3d = np.empty((N + 1, 3), dtype=np.float64)
|
||||||
|
arr = np.asarray(path, dtype=np.float64)
|
||||||
|
fpath_3d[:N, :2] = arr * scale_inv
|
||||||
|
fpath_3d[:N, 2] = z
|
||||||
|
fpath_3d[N] = fpath_3d[0]
|
||||||
|
fpath_3d = align_path_seam_sharpest(fpath_3d)
|
||||||
|
paths_list.append(fpath_3d)
|
||||||
|
groups_list.append(group_id)
|
||||||
|
|
||||||
|
group_id += 1
|
||||||
|
|
||||||
|
return paths_list, groups_list
|
||||||
|
|
||||||
@ndarray_lru_cache(maxsize=32)
|
@ndarray_lru_cache(maxsize=32)
|
||||||
def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float = 0.0, infill_type: str = 'GRID') -> tuple[list[np.ndarray], list[int]]:
|
def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float = 0.0, infill_type: str = 'GRID') -> tuple[list[np.ndarray], list[int]]:
|
||||||
"""Generates a grid or rectilinear infill clipped by the provided paths. Consistent across layers using 1-micron integer grid."""
|
"""Generates a grid or rectilinear infill clipped by the provided paths. Consistent across layers using 1-micron integer grid."""
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
- `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).
|
- `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.
|
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
||||||
- `buffer_slice()`: Offset utility using PyclipperOffset. Supports rotated offsetting.
|
- `buffer_slice()`: Offset utility using PyclipperOffset. Supports rotated offsetting.
|
||||||
|
- `buffer_multiple_slice()`: High-performance single-pass multi-shell concentric offset utility for slicing layers.
|
||||||
- `infill_slice()`: Generates rotated grid or rectilinear infill patterns, ensuring layer-to-layer consistency.
|
- `infill_slice()`: Generates rotated grid or rectilinear infill patterns, ensuring layer-to-layer consistency.
|
||||||
- `boolean_slice()`: Performs 2D boolean operations (Union, Intersection, Subtract, XOR) on slice sets with hole support.
|
- `boolean_slice()`: Performs 2D boolean operations (Union, Intersection, Subtract, XOR) on slice sets with hole support.
|
||||||
- `path_operations_slice()`: Unified dispatcher for multi-input path operations (`ZIP`, `EXTEND`, `MAX_COVERAGE`, `MIN_COVERAGE`, `SUBTRACT`, `XOR`).
|
- `path_operations_slice()`: Unified dispatcher for multi-input path operations (`ZIP`, `EXTEND`, `MAX_COVERAGE`, `MIN_COVERAGE`, `SUBTRACT`, `XOR`).
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
M73 P0 R19
|
M73 P0 R19
|
||||||
EXCLUDE_OBJECT_DEFINE NAME=model CENTER=509.338,496.519 POLYGON=[[300.000,300.000],[700.000,300.000],[700.000,700.000],[300.000,700.000]]
|
EXCLUDE_OBJECT_DEFINE NAME=model CENTER=509.338,496.519 POLYGON=[[300.000,300.000],[700.000,300.000],[700.000,700.000],[300.000,700.000]]
|
||||||
START_PRINT BED_TEMPERATURE=80 KAMP_LEVELING=1 EXTRUDER_ROTATION_VOLUME=755 MIXING_STEPPER_ROTATION_VOLUME=12000 PURGE_LAYER_HEIGHT=2 PURGE_PARKING_SPEED=10000 PURGE_LENGHT=500 PURGE_SPEED=500 PURGE_MATERIAL_QUANTITY=10000 EXTRUDER_TEMPERATURE=240 EXTRUDER_TEMPERATURE_INITIAL_LAYER=240 PRESSURE_ADVANCE=0.2 PRESSURE_ADVANCE_SMOOTH_TIME=0.5 ZONE_1_TEMPERATURE=210 ZONE_2_TEMPERATURE=220 ZONE_3_TEMPERATURE=230
|
START_PRINT BED_TEMPERATURE=80 KAMP_LEVELING=1 EXTRUDER_ROTATION_VOLUME=755 MIXING_STEPPER_ROTATION_VOLUME=12000 PURGE_LAYER_HEIGHT=2 PURGE_PARKING_SPEED=10000 PURGE_LENGHT=500 PURGE_SPEED=500 PURGE_MATERIAL_QUANTITY=10000 EXTRUDER_TEMPERATURE=220 EXTRUDER_TEMPERATURE_INITIAL_LAYER=220 PRESSURE_ADVANCE=0.2 PRESSURE_ADVANCE_SMOOTH_TIME=0.5 ZONE_1_TEMPERATURE=210 ZONE_2_TEMPERATURE=210 ZONE_3_TEMPERATURE=220
|
||||||
G90
|
G90
|
||||||
G21
|
G21
|
||||||
M83
|
M83
|
||||||
|
|
@ -19,4 +19,4 @@ M140 S80
|
||||||
G1 E-135.000 F1800
|
G1 E-135.000 F1800
|
||||||
G1 X351.485016 Y677.991119 Z1.848387 F9000
|
G1 X351.485016 Y677.991119 Z1.848387 F9000
|
||||||
G1 E135.000 F1800
|
G1 E135.000 F1800
|
||||||
M106 S255
|
M106 S170
|
||||||
|
|
|
||||||
|
|
@ -1968,6 +1968,29 @@ class TestCodeReviewBugFixes(unittest.TestCase):
|
||||||
self.assertEqual(len(res[0]), 1)
|
self.assertEqual(len(res[0]), 1)
|
||||||
self.assertEqual(len(res[0][0]), 2) # 1 line segment with 2 endpoints
|
self.assertEqual(len(res[0][0]), 2) # 1 line segment with 2 endpoints
|
||||||
|
|
||||||
|
def test_buffer_multiple_multilayer(self):
|
||||||
|
from NodeSlicer.core.slice import SlicePaths, buffer_multiple_slice
|
||||||
|
# Two layers: z=0.2 (square 10x10) and z=0.4 (square 10x10)
|
||||||
|
p1 = np.array([[0, 0, 0.2], [10, 0, 0.2], [10, 10, 0.2], [0, 10, 0.2], [0, 0, 0.2]])
|
||||||
|
p2 = np.array([[0, 0, 0.4], [10, 0, 0.4], [10, 10, 0.4], [0, 10, 0.4], [0, 0, 0.4]])
|
||||||
|
sp = SlicePaths([p1, p2], groups=[0, 1])
|
||||||
|
|
||||||
|
# 3 shells with base -1.0, step -1.0
|
||||||
|
buffered = sp.buffer_multiple(-1.0, count=3, width_step=-1.0)
|
||||||
|
self.assertEqual(len(buffered), 6) # 3 shells on layer 0, 3 shells on layer 1
|
||||||
|
self.assertEqual(buffered.groups, [0, 0, 0, 1, 1, 1])
|
||||||
|
|
||||||
|
# Check Z height consistency
|
||||||
|
for i in range(3):
|
||||||
|
self.assertAlmostEqual(buffered[i][0, 2], 0.2)
|
||||||
|
for i in range(3, 6):
|
||||||
|
self.assertAlmostEqual(buffered[i][0, 2], 0.4)
|
||||||
|
|
||||||
|
# Check buffer_multiple_slice directly
|
||||||
|
raw_paths, raw_groups = buffer_multiple_slice([p1, p2], -1.0, 3, -1.0)
|
||||||
|
self.assertEqual(len(raw_paths), 6)
|
||||||
|
self.assertEqual(raw_groups, [0, 0, 0, 1, 1, 1])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue