up
This commit is contained in:
parent
a6f2e47884
commit
ce6c88e994
3 changed files with 136 additions and 54 deletions
|
|
@ -1,8 +1,59 @@
|
|||
import numpy as np
|
||||
import zlib
|
||||
|
||||
import numpy as np
|
||||
import zlib
|
||||
import struct
|
||||
from typing import Any, Callable
|
||||
|
||||
def crc32_array(a: np.ndarray) -> int:
|
||||
if not a.size: return 0
|
||||
ac = np.ascontiguousarray(a)
|
||||
mv = memoryview(ac.data).cast("B")
|
||||
return zlib.adler32(mv)
|
||||
|
||||
def crc32_list(l: list[np.ndarray]) -> int:
|
||||
if not l: return 0
|
||||
h = 0
|
||||
for a in l:
|
||||
h = zlib.adler32(struct.pack(">I", crc32_array(a)), h)
|
||||
return h
|
||||
|
||||
def ndarray_lru_cache(maxsize=128):
|
||||
def decorator(func: Callable):
|
||||
cache = {}
|
||||
order = []
|
||||
def wrapper(*args, **kwargs):
|
||||
# Create a key by hashing any ndarray arguments
|
||||
key_parts = []
|
||||
for arg in args:
|
||||
if isinstance(arg, np.ndarray):
|
||||
key_parts.append(crc32_array(arg))
|
||||
elif isinstance(arg, list) and arg and isinstance(arg[0], np.ndarray):
|
||||
key_parts.append(crc32_list(arg))
|
||||
else:
|
||||
key_parts.append(arg)
|
||||
for k, v in sorted(kwargs.items()):
|
||||
if isinstance(v, np.ndarray):
|
||||
key_parts.append((k, crc32_array(v)))
|
||||
elif isinstance(v, list) and v and isinstance(v[0], np.ndarray):
|
||||
key_parts.append((k, crc32_list(v)))
|
||||
else:
|
||||
key_parts.append((k, v))
|
||||
|
||||
key = tuple(key_parts)
|
||||
if key in cache:
|
||||
# Move to end for LRU
|
||||
if key in order: order.remove(key)
|
||||
order.append(key)
|
||||
return cache[key]
|
||||
|
||||
result = func(*args, **kwargs)
|
||||
cache[key] = result
|
||||
order.append(key)
|
||||
if len(order) > maxsize:
|
||||
oldest = order.pop(0)
|
||||
del cache[oldest]
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
from __future__ import annotations
|
||||
from typing import Any
|
||||
from numpy import ndarray
|
||||
import numpy as np
|
||||
import pyclipper
|
||||
|
||||
from .geometry import ndarray_lru_cache
|
||||
|
||||
# Scaling factor for pyclipper (it works with integers)
|
||||
SCALE = 1_000_000
|
||||
|
||||
|
|
@ -163,6 +166,7 @@ def link_segments(segments: np.ndarray, tolerance=1e-6) -> list[np.ndarray]:
|
|||
paths.append(res)
|
||||
return paths
|
||||
|
||||
@ndarray_lru_cache(maxsize=128)
|
||||
def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
|
||||
|
||||
linked = link_segments(segments)
|
||||
|
|
@ -192,10 +196,11 @@ def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
|
|||
res.append(fpath_3d)
|
||||
return res
|
||||
|
||||
@ndarray_lru_cache(maxsize=128)
|
||||
def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation: str = 'UNION') -> list[np.ndarray]:
|
||||
all_z_keys: set[int] = set()
|
||||
subject_dict: dict[int, list[ndarray]] = {}
|
||||
object_dict: dict[int, list[ndarray]] = {}
|
||||
subject_dict: dict[int, list[np.ndarray]] = {}
|
||||
object_dict: dict[int, list[np.ndarray]] = {}
|
||||
|
||||
for s in subject:
|
||||
z = int(round(s[0, 2] * 1000))
|
||||
|
|
@ -214,14 +219,12 @@ def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation
|
|||
object_slices = object_dict.get(k)
|
||||
|
||||
if subject_slices is None:
|
||||
assert object_slices
|
||||
if operation == 'UNION':
|
||||
if object_slices and operation == 'UNION':
|
||||
for s in object_slices: results.append(s)
|
||||
continue
|
||||
|
||||
if object_slices is None:
|
||||
assert subject_slices
|
||||
if operation == 'INTERSECTION': continue
|
||||
if operation != 'INTERSECTION':
|
||||
for s in subject_slices: results.append(s)
|
||||
continue
|
||||
|
||||
|
|
@ -241,7 +244,7 @@ def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation
|
|||
scaled = (path_pts * SCALE).astype(np.int64).tolist()
|
||||
pc.AddPath(scaled, pyclipper.PT_CLIP, True) # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
# Determine operation type. For now only UNION is supported.
|
||||
# Determine operation type
|
||||
op_type = pyclipper.CT_UNION # pyright: ignore[reportAttributeAccessIssue]
|
||||
if operation == 'UNION': op_type = pyclipper.CT_UNION # pyright: ignore[reportAttributeAccessIssue]
|
||||
elif operation == 'INTERSECTION': op_type = pyclipper.CT_INTERSECTION # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
|
@ -259,6 +262,7 @@ def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation
|
|||
|
||||
return results
|
||||
|
||||
@ndarray_lru_cache(maxsize=64)
|
||||
def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round', tolerance=.01) -> tuple[list[np.ndarray], list[int]]:
|
||||
|
||||
if not slice_paths: return [], []
|
||||
|
|
@ -319,6 +323,7 @@ def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round',
|
|||
|
||||
return paths_list, groups_list
|
||||
|
||||
@ndarray_lru_cache(maxsize=32)
|
||||
def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float = 0.0) -> tuple[list[np.ndarray], list[int]]:
|
||||
"""Generates a grid infill clipped by the provided paths. Consistent across layers."""
|
||||
if not slice_paths or density <= 0: return [], []
|
||||
|
|
@ -339,7 +344,6 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
|||
rot_mat = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
|
||||
|
||||
# Determine grid bounds in the rotated space to ensure full coverage
|
||||
# We rotate the corners of the global bounding box to find the new required extent
|
||||
corners = np.array([
|
||||
[g_min_x, g_min_y, 0], [g_max_x, g_min_y, 0],
|
||||
[g_min_x, g_max_y, 0], [g_max_x, g_max_y, 0]
|
||||
|
|
@ -349,7 +353,6 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
|||
rg_max_x, rg_max_y = rotated_corners[:, 0].max(), rotated_corners[:, 1].max()
|
||||
|
||||
# Generate the global static grid lines in de-rotated space
|
||||
# Add a small buffer to ensure we don't miss edges
|
||||
rg_min_x -= density
|
||||
rg_max_x += density
|
||||
rg_min_y -= density
|
||||
|
|
@ -357,7 +360,7 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
|||
|
||||
scaled_grid_paths = []
|
||||
|
||||
# Vertical lines (constant X, varies Y)
|
||||
# Vertical lines
|
||||
x = rg_min_x
|
||||
while x <= rg_max_x:
|
||||
scaled_grid_paths.append([
|
||||
|
|
@ -366,7 +369,7 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
|||
])
|
||||
x += density
|
||||
|
||||
# Horizontal lines (constant Y, varies X)
|
||||
# Horizontal lines
|
||||
y = rg_min_y
|
||||
while y <= rg_max_y:
|
||||
scaled_grid_paths.append([
|
||||
|
|
@ -377,8 +380,14 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
|||
|
||||
# Group by Z
|
||||
groups: dict[int, list[np.ndarray]] = {}
|
||||
if hasattr(slice_paths, 'groups'):
|
||||
for p, g in zip(slice_paths, slice_paths.groups):
|
||||
groups.setdefault(g, []).append(p)
|
||||
else:
|
||||
# Fallback if it's just a list
|
||||
for p in slice_paths:
|
||||
z_key = int(round(p[0, 2] * 1000))
|
||||
groups.setdefault(z_key, []).append(p)
|
||||
|
||||
output_paths: list[np.ndarray] = []
|
||||
output_groups: list[int] = []
|
||||
|
|
|
|||
|
|
@ -287,14 +287,23 @@ class TriMesh():
|
|||
self.length_tris = t
|
||||
self.matrix_world = m
|
||||
|
||||
def objects_to_tris(objects: list[Object], scale) -> (NDArray[tuple[int, int, int], dtype[float]], NDArray[tuple[int, int], dtype[float]]): # pyright: ignore[reportInvalidTypeForm]
|
||||
from ..core.geometry import ndarray_lru_cache
|
||||
|
||||
@ndarray_lru_cache(maxsize=32)
|
||||
def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[float, ...], ...], scale: float) -> NDArray[float64]:
|
||||
# Implementation remains largely the same but uses names to fetch objects
|
||||
tris_count: int = 0
|
||||
meshes: list[TriMesh] = []
|
||||
|
||||
for obj in objects:
|
||||
for name in obj_names:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj: continue
|
||||
|
||||
try:
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
mesh: Mesh = obj.to_mesh(depsgraph=depsgraph, preserve_all_data_layers=True)
|
||||
# We must use the evaluated object to get modifiers
|
||||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
mesh: Mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||
except: continue
|
||||
mesh.calc_loop_triangles()
|
||||
|
||||
|
|
@ -310,22 +319,22 @@ def objects_to_tris(objects: list[Object], scale) -> (NDArray[tuple[int, int, in
|
|||
np.array(obj.matrix_world.transposed())
|
||||
)]
|
||||
|
||||
tris_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(tris_count * 4 * 3, dtype=dtype(np.float64))
|
||||
tris: np.ndarray[tuple[int, int, int], dtype[np.float64]] = tris_flat.reshape(-1, 4, 3)
|
||||
tris_flat: np.ndarray = np.empty(tris_count * 4 * 3, dtype=np.float64)
|
||||
tris: np.ndarray = tris_flat.reshape(-1, 4, 3)
|
||||
|
||||
col_idx = 0
|
||||
for trimesh in meshes:
|
||||
tris_v_i_flat: np.ndarray[tuple[int], dtype[np.int32]] = np.empty(trimesh.length_tris * 3, dtype=dtype(np.int32))
|
||||
tris_v_n_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(trimesh.length_tris * 3, dtype=dtype(np.float64))
|
||||
tris_verts_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(trimesh.length_verts * 3, dtype=dtype(np.float64))
|
||||
tris_v_i_flat: np.ndarray = np.empty(trimesh.length_tris * 3, dtype=np.int32)
|
||||
tris_v_n_flat: np.ndarray = np.empty(trimesh.length_tris * 3, dtype=np.float64)
|
||||
tris_verts_flat: np.ndarray = np.empty(trimesh.length_verts * 3, dtype=np.float64)
|
||||
|
||||
trimesh.mesh.loop_triangles.foreach_get("vertices", tris_v_i_flat)
|
||||
trimesh.mesh.loop_triangles.foreach_get("normal", tris_v_n_flat)
|
||||
trimesh.mesh.vertices.foreach_get("co", tris_verts_flat)
|
||||
|
||||
tris_v_i: np.ndarray[tuple[int, int], dtype[np.int32]] = tris_v_i_flat.reshape((-1, 3))
|
||||
tris_v_n: np.ndarray[tuple[int, int], dtype[np.float64]] = tris_v_n_flat.reshape((-1, 3))
|
||||
tris_verts: np.ndarray[tuple[int, int], dtype[np.float64]] = tris_verts_flat.reshape((-1, 3))
|
||||
tris_v_i: np.ndarray = tris_v_i_flat.reshape((-1, 3))
|
||||
tris_v_n: np.ndarray = tris_v_n_flat.reshape((-1, 3))
|
||||
tris_verts: np.ndarray = tris_verts_flat.reshape((-1, 3))
|
||||
|
||||
homogeneous_verts: NDArray[float64] = np.hstack((tris_verts, np.ones((tris_verts.shape[0], 1)))) # type: ignore
|
||||
tx_verts = homogeneous_verts @ trimesh.matrix_world
|
||||
|
|
@ -347,12 +356,31 @@ def objects_to_tris(objects: list[Object], scale) -> (NDArray[tuple[int, int, in
|
|||
|
||||
return tris
|
||||
|
||||
def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
||||
def objects_to_tris(objects: list[Object], scale) -> NDArray[float64]:
|
||||
obj_names = tuple(o.name for o in objects)
|
||||
matrices = tuple(tuple(val for row in o.matrix_world for val in row) for o in objects)
|
||||
return _cached_objects_to_tris(obj_names, matrices, scale)
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
@lru_cache(maxsize=64)
|
||||
def _cached_capture_mesh_edges(
|
||||
obj_name: str,
|
||||
mesh_name: str,
|
||||
matrix_tuple: tuple[float, ...],
|
||||
scene_scale: float
|
||||
) -> list[NDArray[float64]]:
|
||||
# This helper expects that the object and mesh exist and are accessible via bpy.data
|
||||
obj = bpy.data.objects.get(obj_name)
|
||||
if not obj: return []
|
||||
|
||||
# We don't use obj_eval here directly as it's not hashable,
|
||||
# but we assume this is called when we know we need to re-capture.
|
||||
# The actual capture logic stays the same but is now inside this cached function.
|
||||
try:
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
|
@ -360,32 +388,16 @@ def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
|||
return []
|
||||
|
||||
try:
|
||||
if not mesh.vertices:
|
||||
return []
|
||||
|
||||
# 1. Scaling & Matrix Setup
|
||||
assert bpy.context.scene
|
||||
assert bpy.context.scene.unit_settings
|
||||
scale = 1000.0 * bpy.context.scene.unit_settings.scale_length
|
||||
world_mat = np.array(obj.matrix_world)
|
||||
|
||||
# 2. Extract and Transform Coordinates
|
||||
world_mat = np.array(matrix_tuple).reshape(4, 4)
|
||||
num_verts = len(mesh.vertices)
|
||||
verts = np.empty((num_verts, 3), dtype=np.float64)
|
||||
mesh.vertices.foreach_get("co", verts.ravel())
|
||||
|
||||
# World transform: (V * R.T) + T
|
||||
tx_verts = (verts @ world_mat[:3, :3].T + world_mat[:3, 3]) * scale
|
||||
tx_verts = (verts @ world_mat[:3, :3].T + world_mat[:3, 3]) * scene_scale
|
||||
|
||||
# 3. Build connectivity lookup
|
||||
# Only need edges where v2 = v1 + 1 or loop-back
|
||||
edges_set = {tuple(sorted(e.vertices)) for e in mesh.edges}
|
||||
|
||||
# Identify gaps in sequential connectivity
|
||||
# connected[i] is True if vertex i is connected to i+1
|
||||
connected = np.array([(i, i + 1) in edges_set for i in range(num_verts - 1)])
|
||||
|
||||
# 4. Split into segments (Maintains original index order)
|
||||
split_at = np.where(~connected)[0] + 1
|
||||
raw_paths = np.split(tx_verts, split_at)
|
||||
indices_split = np.split(np.arange(num_verts), split_at)
|
||||
|
|
@ -394,19 +406,29 @@ def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
|||
for i, path_coords in enumerate(raw_paths):
|
||||
if path_coords.shape[0] == 0:
|
||||
continue
|
||||
|
||||
# 5. Handle Loop Closure
|
||||
# Only check if the segment could potentially be a loop (indices > 2)
|
||||
idx = indices_split[i]
|
||||
if len(idx) > 2 and tuple(sorted((idx[0], idx[-1]))) in edges_set:
|
||||
path_coords = np.vstack((path_coords, path_coords[0]))
|
||||
|
||||
final_paths.append(path_coords)
|
||||
|
||||
return final_paths
|
||||
|
||||
finally:
|
||||
# Compatibility with Blender 2.8+ to avoid memory leaks
|
||||
# We check if the attribute exists as it's not used elsewhere in this file
|
||||
if hasattr(obj, "to_mesh_clear"):
|
||||
obj.to_mesh_clear()
|
||||
if hasattr(obj_eval, "to_mesh_clear"):
|
||||
obj_eval.to_mesh_clear()
|
||||
|
||||
def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
||||
if not obj or not obj.data: return []
|
||||
|
||||
assert bpy.context.scene and bpy.context.scene.unit_settings
|
||||
scene_scale = 1000.0 * bpy.context.scene.unit_settings.scale_length
|
||||
|
||||
# Create a hashable key from object state
|
||||
matrix_tuple = tuple(val for row in obj.matrix_world for val in row) # Flattened tuple
|
||||
|
||||
return _cached_capture_mesh_edges(
|
||||
obj.name,
|
||||
obj.data.name,
|
||||
matrix_tuple,
|
||||
scene_scale
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue