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 numpy as np
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import zlib
|
||||||
|
import struct
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
def crc32_array(a: np.ndarray) -> int:
|
def crc32_array(a: np.ndarray) -> int:
|
||||||
if not a.size: return 0
|
if not a.size: return 0
|
||||||
ac = np.ascontiguousarray(a)
|
ac = np.ascontiguousarray(a)
|
||||||
mv = memoryview(ac.data).cast("B")
|
mv = memoryview(ac.data).cast("B")
|
||||||
return zlib.adler32(mv)
|
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 __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
from numpy import ndarray
|
from numpy import ndarray
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyclipper
|
import pyclipper
|
||||||
|
|
||||||
|
from .geometry import ndarray_lru_cache
|
||||||
|
|
||||||
# Scaling factor for pyclipper (it works with integers)
|
# Scaling factor for pyclipper (it works with integers)
|
||||||
SCALE = 1_000_000
|
SCALE = 1_000_000
|
||||||
|
|
||||||
|
|
@ -163,6 +166,7 @@ def link_segments(segments: np.ndarray, tolerance=1e-6) -> list[np.ndarray]:
|
||||||
paths.append(res)
|
paths.append(res)
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
@ndarray_lru_cache(maxsize=128)
|
||||||
def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
|
def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
|
||||||
|
|
||||||
linked = link_segments(segments)
|
linked = link_segments(segments)
|
||||||
|
|
@ -192,10 +196,11 @@ def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
|
||||||
res.append(fpath_3d)
|
res.append(fpath_3d)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ndarray_lru_cache(maxsize=128)
|
||||||
def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation: str = 'UNION') -> list[np.ndarray]:
|
def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation: str = 'UNION') -> list[np.ndarray]:
|
||||||
all_z_keys: set[int] = set()
|
all_z_keys: set[int] = set()
|
||||||
subject_dict: dict[int, list[ndarray]] = {}
|
subject_dict: dict[int, list[np.ndarray]] = {}
|
||||||
object_dict: dict[int, list[ndarray]] = {}
|
object_dict: dict[int, list[np.ndarray]] = {}
|
||||||
|
|
||||||
for s in subject:
|
for s in subject:
|
||||||
z = int(round(s[0, 2] * 1000))
|
z = int(round(s[0, 2] * 1000))
|
||||||
|
|
@ -214,15 +219,13 @@ def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation
|
||||||
object_slices = object_dict.get(k)
|
object_slices = object_dict.get(k)
|
||||||
|
|
||||||
if subject_slices is None:
|
if subject_slices is None:
|
||||||
assert object_slices
|
if object_slices and operation == 'UNION':
|
||||||
if operation == 'UNION':
|
|
||||||
for s in object_slices: results.append(s)
|
for s in object_slices: results.append(s)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if object_slices is None:
|
if object_slices is None:
|
||||||
assert subject_slices
|
if operation != 'INTERSECTION':
|
||||||
if operation == 'INTERSECTION': continue
|
for s in subject_slices: results.append(s)
|
||||||
for s in subject_slices: results.append(s)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
z = subject_slices[0][0, 2]
|
z = subject_slices[0][0, 2]
|
||||||
|
|
@ -241,7 +244,7 @@ def boolean_slice(subject: list[np.ndarray], object: list[np.ndarray], operation
|
||||||
scaled = (path_pts * SCALE).astype(np.int64).tolist()
|
scaled = (path_pts * SCALE).astype(np.int64).tolist()
|
||||||
pc.AddPath(scaled, pyclipper.PT_CLIP, True) # pyright: ignore[reportAttributeAccessIssue]
|
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]
|
op_type = pyclipper.CT_UNION # pyright: ignore[reportAttributeAccessIssue]
|
||||||
if operation == 'UNION': 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]
|
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
|
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]]:
|
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 [], []
|
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
|
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]]:
|
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."""
|
"""Generates a grid infill clipped by the provided paths. Consistent across layers."""
|
||||||
if not slice_paths or density <= 0: return [], []
|
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]])
|
rot_mat = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
|
||||||
|
|
||||||
# Determine grid bounds in the rotated space to ensure full coverage
|
# 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([
|
corners = np.array([
|
||||||
[g_min_x, g_min_y, 0], [g_max_x, g_min_y, 0],
|
[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]
|
[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()
|
rg_max_x, rg_max_y = rotated_corners[:, 0].max(), rotated_corners[:, 1].max()
|
||||||
|
|
||||||
# Generate the global static grid lines in de-rotated space
|
# 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_min_x -= density
|
||||||
rg_max_x += density
|
rg_max_x += density
|
||||||
rg_min_y -= density
|
rg_min_y -= density
|
||||||
|
|
@ -357,7 +360,7 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
||||||
|
|
||||||
scaled_grid_paths = []
|
scaled_grid_paths = []
|
||||||
|
|
||||||
# Vertical lines (constant X, varies Y)
|
# Vertical lines
|
||||||
x = rg_min_x
|
x = rg_min_x
|
||||||
while x <= rg_max_x:
|
while x <= rg_max_x:
|
||||||
scaled_grid_paths.append([
|
scaled_grid_paths.append([
|
||||||
|
|
@ -366,7 +369,7 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
||||||
])
|
])
|
||||||
x += density
|
x += density
|
||||||
|
|
||||||
# Horizontal lines (constant Y, varies X)
|
# Horizontal lines
|
||||||
y = rg_min_y
|
y = rg_min_y
|
||||||
while y <= rg_max_y:
|
while y <= rg_max_y:
|
||||||
scaled_grid_paths.append([
|
scaled_grid_paths.append([
|
||||||
|
|
@ -377,8 +380,14 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float
|
||||||
|
|
||||||
# Group by Z
|
# Group by Z
|
||||||
groups: dict[int, list[np.ndarray]] = {}
|
groups: dict[int, list[np.ndarray]] = {}
|
||||||
for p, g in zip(slice_paths, slice_paths.groups):
|
if hasattr(slice_paths, 'groups'):
|
||||||
groups.setdefault(g, []).append(p)
|
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_paths: list[np.ndarray] = []
|
||||||
output_groups: list[int] = []
|
output_groups: list[int] = []
|
||||||
|
|
|
||||||
|
|
@ -287,14 +287,23 @@ class TriMesh():
|
||||||
self.length_tris = t
|
self.length_tris = t
|
||||||
self.matrix_world = m
|
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
|
tris_count: int = 0
|
||||||
meshes: list[TriMesh] = []
|
meshes: list[TriMesh] = []
|
||||||
|
|
||||||
for obj in objects:
|
for name in obj_names:
|
||||||
|
obj = bpy.data.objects.get(name)
|
||||||
|
if not obj: continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
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
|
except: continue
|
||||||
mesh.calc_loop_triangles()
|
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())
|
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_flat: np.ndarray = np.empty(tris_count * 4 * 3, dtype=np.float64)
|
||||||
tris: np.ndarray[tuple[int, int, int], dtype[np.float64]] = tris_flat.reshape(-1, 4, 3)
|
tris: np.ndarray = tris_flat.reshape(-1, 4, 3)
|
||||||
|
|
||||||
col_idx = 0
|
col_idx = 0
|
||||||
for trimesh in meshes:
|
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_i_flat: np.ndarray = np.empty(trimesh.length_tris * 3, 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_v_n_flat: np.ndarray = np.empty(trimesh.length_tris * 3, dtype=np.float64)
|
||||||
tris_verts_flat: np.ndarray[tuple[int], dtype[np.float64]] = np.empty(trimesh.length_verts * 3, dtype=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("vertices", tris_v_i_flat)
|
||||||
trimesh.mesh.loop_triangles.foreach_get("normal", tris_v_n_flat)
|
trimesh.mesh.loop_triangles.foreach_get("normal", tris_v_n_flat)
|
||||||
trimesh.mesh.vertices.foreach_get("co", tris_verts_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_i: np.ndarray = 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_v_n: np.ndarray = tris_v_n_flat.reshape((-1, 3))
|
||||||
tris_verts: np.ndarray[tuple[int, int], dtype[np.float64]] = tris_verts_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
|
homogeneous_verts: NDArray[float64] = np.hstack((tris_verts, np.ones((tris_verts.shape[0], 1)))) # type: ignore
|
||||||
tx_verts = homogeneous_verts @ trimesh.matrix_world
|
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
|
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:
|
try:
|
||||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
obj_eval = obj.evaluated_get(depsgraph)
|
obj_eval = obj.evaluated_get(depsgraph)
|
||||||
mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -360,32 +388,16 @@ def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not mesh.vertices:
|
world_mat = np.array(matrix_tuple).reshape(4, 4)
|
||||||
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
|
|
||||||
num_verts = len(mesh.vertices)
|
num_verts = len(mesh.vertices)
|
||||||
verts = np.empty((num_verts, 3), dtype=np.float64)
|
verts = np.empty((num_verts, 3), dtype=np.float64)
|
||||||
mesh.vertices.foreach_get("co", verts.ravel())
|
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]) * scene_scale
|
||||||
tx_verts = (verts @ world_mat[:3, :3].T + world_mat[:3, 3]) * 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}
|
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)])
|
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
|
split_at = np.where(~connected)[0] + 1
|
||||||
raw_paths = np.split(tx_verts, split_at)
|
raw_paths = np.split(tx_verts, split_at)
|
||||||
indices_split = np.split(np.arange(num_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):
|
for i, path_coords in enumerate(raw_paths):
|
||||||
if path_coords.shape[0] == 0:
|
if path_coords.shape[0] == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 5. Handle Loop Closure
|
|
||||||
# Only check if the segment could potentially be a loop (indices > 2)
|
|
||||||
idx = indices_split[i]
|
idx = indices_split[i]
|
||||||
if len(idx) > 2 and tuple(sorted((idx[0], idx[-1]))) in edges_set:
|
if len(idx) > 2 and tuple(sorted((idx[0], idx[-1]))) in edges_set:
|
||||||
path_coords = np.vstack((path_coords, path_coords[0]))
|
path_coords = np.vstack((path_coords, path_coords[0]))
|
||||||
|
|
||||||
final_paths.append(path_coords)
|
final_paths.append(path_coords)
|
||||||
|
|
||||||
return final_paths
|
return final_paths
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Compatibility with Blender 2.8+ to avoid memory leaks
|
if hasattr(obj_eval, "to_mesh_clear"):
|
||||||
# We check if the attribute exists as it's not used elsewhere in this file
|
obj_eval.to_mesh_clear()
|
||||||
if hasattr(obj, "to_mesh_clear"):
|
|
||||||
obj.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