693 lines
No EOL
26 KiB
Python
693 lines
No EOL
26 KiB
Python
from __future__ import annotations
|
|
from bpy.types import Object, Mesh
|
|
import bpy
|
|
import zlib, struct
|
|
from functools import cached_property
|
|
from contextlib import contextmanager, ExitStack
|
|
|
|
import numpy as np
|
|
from numpy import dtype, float64
|
|
from numpy.typing import NDArray
|
|
|
|
from typing import Any
|
|
|
|
from ...core.geometry import crc32_array, euler_rotation_matrix, update_transform
|
|
from .blender_bridge import get_all_children
|
|
|
|
TYPES_NAME = 'DEFAULT'
|
|
|
|
class MeshCaptureException(Exception): pass
|
|
|
|
@contextmanager
|
|
def get_evaluated_mesh(obj: Object):
|
|
"""
|
|
Context manager yielding (eval_obj, mesh) for a Blender object.
|
|
Applies all modifiers even if the object is hidden in viewport or in a hidden/disabled collection,
|
|
WITHOUT changing any visibility settings on the original object or its collections.
|
|
"""
|
|
if obj.mode == 'EDIT':
|
|
obj.update_from_editmode()
|
|
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
is_hidden = obj.hide_viewport or obj.hide_get()
|
|
|
|
mesh = None
|
|
eval_obj = None
|
|
is_temp = False
|
|
temp_obj = None
|
|
|
|
# Try direct evaluation first
|
|
try:
|
|
eval_obj = obj.evaluated_get(depsgraph)
|
|
m = eval_obj.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
|
orig_verts = getattr(obj.data, "vertices", None)
|
|
if m and len(m.vertices) > 0 and not is_hidden and (not obj.modifiers or (orig_verts is not None and len(m.vertices) != len(orig_verts))):
|
|
mesh = m
|
|
elif m:
|
|
eval_obj.to_mesh_clear()
|
|
except Exception:
|
|
pass
|
|
|
|
if mesh is None:
|
|
# Fallback for hidden object / excluded collection
|
|
try:
|
|
temp_obj = obj.copy()
|
|
temp_obj.hide_viewport = False
|
|
scene = bpy.context.scene
|
|
if scene and scene.collection:
|
|
scene.collection.objects.link(temp_obj)
|
|
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
depsgraph.update()
|
|
|
|
eval_obj = temp_obj.evaluated_get(depsgraph)
|
|
mesh = eval_obj.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
|
is_temp = True
|
|
except Exception:
|
|
if temp_obj is not None:
|
|
try:
|
|
scene = bpy.context.scene
|
|
if scene and scene.collection:
|
|
scene.collection.objects.unlink(temp_obj)
|
|
bpy.data.objects.remove(temp_obj, do_unlink=True)
|
|
except Exception:
|
|
pass
|
|
mesh = None
|
|
eval_obj = None
|
|
|
|
try:
|
|
yield eval_obj, mesh
|
|
finally:
|
|
if mesh is not None and eval_obj is not None:
|
|
try:
|
|
eval_obj.to_mesh_clear()
|
|
except Exception:
|
|
pass
|
|
if is_temp and temp_obj is not None:
|
|
try:
|
|
scene = bpy.context.scene
|
|
if scene and scene.collection:
|
|
scene.collection.objects.unlink(temp_obj)
|
|
bpy.data.objects.remove(temp_obj, do_unlink=True)
|
|
bpy.context.evaluated_depsgraph_get().update()
|
|
except Exception:
|
|
pass
|
|
|
|
import copy
|
|
from dataclasses import dataclass, field
|
|
|
|
@dataclass
|
|
class PdfProperties:
|
|
line_width: float = 0.0
|
|
line_color: tuple[float, float, float] | None = None
|
|
show_name: bool = False
|
|
|
|
|
|
class SlicingObject():
|
|
name: str
|
|
parent: str
|
|
mesh: NDArray[float64]
|
|
attributes: dict[str, Any]
|
|
pdf_properties: PdfProperties
|
|
pdf_object_properties: list[tuple[str, str]]
|
|
|
|
def __init__(self, obj: Object | None = None, parent: str = "") -> None:
|
|
if not bpy.context.scene: raise Exception('No scene currently open!')
|
|
|
|
self.name = str(obj.name) if obj else ""
|
|
self.parent = parent
|
|
self.attributes = {}
|
|
self.transforms = []
|
|
self.pdf_properties = PdfProperties()
|
|
self.pdf_object_properties = []
|
|
|
|
assert bpy.context.scene.unit_settings
|
|
scene_scale: float = bpy.context.scene.unit_settings.scale_length
|
|
|
|
if obj:
|
|
self.mesh, self.attributes = objects_to_tris([obj], 1000 * scene_scale)
|
|
else:
|
|
self.mesh = np.empty((0, 4, 3))
|
|
self.attributes = {}
|
|
|
|
@staticmethod
|
|
def merge(objects: list[SlicingObject]) -> SlicingObject:
|
|
merged = SlicingObject()
|
|
if not objects: return merged
|
|
|
|
# Take metadata from first object if available
|
|
merged.name = objects[0].name + "_merged"
|
|
merged.parent = objects[0].parent
|
|
merged.pdf_properties = copy.deepcopy(objects[0].pdf_properties) if hasattr(objects[0], "pdf_properties") and objects[0].pdf_properties else PdfProperties()
|
|
merged.pdf_object_properties = list(getattr(objects[0], "pdf_object_properties", []))
|
|
merged.transforms = [dict(t) for t in getattr(objects[0], "transforms", [])] if objects else []
|
|
|
|
valid_meshes = [o.mesh for o in objects if o.mesh.size]
|
|
merged.mesh = np.concatenate(valid_meshes, axis=0) if valid_meshes else np.empty((0, 4, 3))
|
|
|
|
all_attr_names = set()
|
|
for o in objects:
|
|
all_attr_names.update(o.attributes.keys())
|
|
|
|
for name in all_attr_names:
|
|
template_obj = next(o for o in objects if name in o.attributes)
|
|
template = template_obj.attributes[name]
|
|
|
|
val_list = []
|
|
for o in objects:
|
|
if name in o.attributes:
|
|
val_list.append(o.attributes[name]["values"])
|
|
else:
|
|
shape = (len(o.mesh),) + template["values"].shape[1:]
|
|
val_list.append(np.zeros(shape, dtype=template["values"].dtype))
|
|
|
|
merged.attributes[name] = {
|
|
"domain": template["domain"],
|
|
"type": template["type"],
|
|
"values": np.concatenate(val_list, axis=0)
|
|
}
|
|
return merged
|
|
|
|
def offset(self, offset: NDArray[Any]):
|
|
self.mesh[:, :3, :] += offset
|
|
|
|
def scale(self, factor: float | NDArray[Any]):
|
|
self.mesh[:, :3, :] *= factor
|
|
if isinstance(factor, (list, tuple, np.ndarray)):
|
|
factor_arr = np.array(factor, dtype=np.float64)
|
|
if factor_arr.shape == (3,):
|
|
# For non-uniform scaling, normals transform by the inverse transpose.
|
|
safe_factor = np.where(factor_arr == 0, 1.0, factor_arr)
|
|
self.mesh[:, 3, :] /= safe_factor
|
|
norms = self.mesh[:, 3, :]
|
|
norms_len = np.linalg.norm(norms, axis=-1, keepdims=True)
|
|
norms_len = np.where(norms_len == 0, 1.0, norms_len)
|
|
self.mesh[:, 3, :] = norms / norms_len
|
|
|
|
def rotate(self, angles: NDArray[Any]):
|
|
# angles: array-like of 3 floats (rx, ry, rz) in radians
|
|
angles_arr = np.array(angles, dtype=np.float64)
|
|
R = euler_rotation_matrix(angles_arr[0], angles_arr[1], angles_arr[2])
|
|
|
|
# Apply to vertices: shape (N, 3, 3)
|
|
self.mesh[:, :3, :] = self.mesh[:, :3, :] @ R.T
|
|
|
|
# Apply to normals: shape (N, 3)
|
|
self.mesh[:, 3, :] = self.mesh[:, 3, :] @ R.T
|
|
# Normalize the normals
|
|
norms = self.mesh[:, 3, :]
|
|
norms_len = np.linalg.norm(norms, axis=-1, keepdims=True)
|
|
norms_len = np.where(norms_len == 0, 1.0, norms_len)
|
|
self.mesh[:, 3, :] = norms / norms_len
|
|
|
|
@property
|
|
def checksum(self) -> int:
|
|
import json
|
|
|
|
buf = bytearray()
|
|
buf.extend(struct.pack(">I", crc32_array(self.mesh)))
|
|
buf.extend(struct.pack(">I", zlib.crc32(self.name.encode("utf-8"))))
|
|
buf.extend(struct.pack(">I", zlib.crc32(self.parent.encode("utf-8"))))
|
|
|
|
return zlib.crc32(buf) & 0xFFFFFFFF
|
|
|
|
@property
|
|
def height(self) -> float: return self.mesh[:, :3, 2].max()
|
|
|
|
@property
|
|
def min_x(self) -> float: return self.mesh[:, :3, 0].min()
|
|
|
|
@property
|
|
def max_x(self) -> float: return self.mesh[:, :3, 0].max()
|
|
|
|
@property
|
|
def min_y(self) -> float: return self.mesh[:, :3, 1].min()
|
|
|
|
@property
|
|
def max_y(self) -> float: return self.mesh[:, :3, 1].max()
|
|
|
|
@property
|
|
def min_z(self) -> float: return self.mesh[:, :3, 2].min()
|
|
|
|
@property
|
|
def max_z(self) -> float: return self.mesh[:, :3, 2].max()
|
|
|
|
@property
|
|
def min_xy(self) -> NDArray[Any]: return np.array([self.min_x, self.min_y, 0.0])
|
|
|
|
@property
|
|
def max_xy(self) -> NDArray[Any]: return np.array([self.max_x, self.max_y, 0.0])
|
|
|
|
@property
|
|
def center_xy(self) -> NDArray[Any]: return (self.min_xy + self.max_xy) / 2.0
|
|
|
|
class SlicingCollection():
|
|
name: str
|
|
objects: list[SlicingObject]
|
|
transforms: list[dict[str, Any]]
|
|
|
|
def __init__(self, objs: list[Object], parent: str):
|
|
self.objects = [SlicingObject(obj, parent) for obj in objs]
|
|
self.objects = [o for o in self.objects if o.mesh.size]
|
|
self.name = parent
|
|
self.transforms = []
|
|
|
|
def _clear_cache(self):
|
|
for prop in ["all_verts", "unique_verts"]:
|
|
if prop in self.__dict__:
|
|
del self.__dict__[prop]
|
|
|
|
def offset(self, offset: NDArray[Any]):
|
|
for so in self.objects: so.offset(offset)
|
|
self._clear_cache()
|
|
|
|
def scale(self, factor: float | NDArray[Any]):
|
|
for so in self.objects: so.scale(factor)
|
|
self._clear_cache()
|
|
|
|
def rotate(self, angles: NDArray[Any]):
|
|
for so in self.objects: so.rotate(angles)
|
|
self._clear_cache()
|
|
|
|
@property
|
|
def checksum(self) -> int:
|
|
return crc32_array(np.array([o.checksum for o in self.objects]))
|
|
|
|
@property
|
|
def height(self) -> float:
|
|
height_all = [so.height for so in self.objects if np.any(so.mesh)]
|
|
if not height_all: return 0
|
|
return max(height_all)
|
|
|
|
@property
|
|
def meshes(self) -> list[NDArray[Any]]:
|
|
return [o.mesh for o in self.objects]
|
|
|
|
@cached_property
|
|
def mesh_lengths_ids(self) -> NDArray[Any]:
|
|
return np.array([len(m) for m in self.meshes])
|
|
|
|
@cached_property
|
|
def mesh_start_ids(self) -> NDArray[Any]:
|
|
lengths = self.mesh_lengths_ids
|
|
return np.insert(np.cumsum(lengths)[:-1], 0, 0)
|
|
|
|
@cached_property
|
|
def mesh_end_ids(self) -> NDArray[Any]:
|
|
starts = self.mesh_start_ids
|
|
lengths = self.mesh_lengths_ids
|
|
return starts + lengths - 1
|
|
|
|
@cached_property
|
|
def all_verts(self) -> NDArray[Any]:
|
|
all_verts = np.vstack(self.meshes)[:, :3, :]
|
|
return all_verts.reshape(-1, 3)
|
|
|
|
@cached_property
|
|
def unique_verts(self) -> tuple[np.ndarray, np.ndarray]:
|
|
verts_list = []
|
|
idxs = []
|
|
offset = 0
|
|
|
|
for mesh in self.meshes:
|
|
verts = mesh[:, :3, :].reshape(-1, 3)
|
|
|
|
void_dtype = np.dtype((np.void, verts.dtype.itemsize * verts.shape[1]))
|
|
verts_void = verts.view(void_dtype).ravel()
|
|
|
|
unique_voids, inv = np.unique(verts_void, return_inverse=True)
|
|
|
|
unique_verts = unique_voids.view(verts.dtype).reshape(-1, 3)
|
|
|
|
verts_list.append(unique_verts)
|
|
idxs.append(inv.reshape(-1, 3) + offset)
|
|
|
|
offset += unique_verts.shape[0]
|
|
|
|
all_verts = np.concatenate(verts_list, axis=0)
|
|
all_idxs = np.vstack(idxs)
|
|
return all_verts, all_idxs
|
|
|
|
@property
|
|
def min_x(self) -> float | None:
|
|
return min([so.min_x for so in self.objects if np.any(so.mesh)], default=None)
|
|
|
|
@property
|
|
def max_x(self) -> float | None:
|
|
return max([so.max_x for so in self.objects if np.any(so.mesh)], default=None)
|
|
|
|
@property
|
|
def min_y(self) -> float | None:
|
|
return min([so.min_y for so in self.objects if np.any(so.mesh)], default=None)
|
|
|
|
@property
|
|
def max_y(self) -> float | None:
|
|
return max([so.max_y for so in self.objects if np.any(so.mesh)], default=None)
|
|
|
|
@property
|
|
def min_z(self) -> float | None:
|
|
return min([so.min_z for so in self.objects if np.any(so.mesh)], default=None)
|
|
|
|
@property
|
|
def max_z(self) -> float | None:
|
|
return max([so.max_z for so in self.objects if np.any(so.mesh)], default=None)
|
|
|
|
@property
|
|
def min_xy(self) -> NDArray[Any] | None:
|
|
if self.min_x is None or self.min_y is None: return None
|
|
return np.array([self.min_x, self.min_y, 0.0])
|
|
|
|
@property
|
|
def max_xy(self) -> NDArray[Any] | None:
|
|
if self.max_x is None or self.max_y is None: return None
|
|
return np.array([self.max_x, self.max_y, 0.0])
|
|
|
|
@property
|
|
def center_xy(self) -> NDArray[Any] | None:
|
|
if self.min_xy is None or self.max_xy is None: return None
|
|
return (self.min_xy + self.max_xy) / 2.0
|
|
|
|
class SlicingGroup():
|
|
|
|
collections: dict[str, SlicingCollection]
|
|
transforms: list[dict[str, Any]]
|
|
|
|
def __init__(self, selected_objs: list[Object]) -> None:
|
|
self.transforms = []
|
|
|
|
family = []
|
|
for obj in selected_objs:
|
|
family.append(obj)
|
|
family.extend(get_all_children(obj))
|
|
|
|
filtered_objs = [o for o in family]
|
|
|
|
def find_top_parent(o):
|
|
current = o
|
|
while current.parent is not None:
|
|
current = current.parent
|
|
return current
|
|
|
|
parents: dict[str, list[Object]] = {}
|
|
[parents.setdefault(find_top_parent(o).name, []).append(o) for o in filtered_objs]
|
|
|
|
self.collections = {}
|
|
for k, objects in parents.items():
|
|
self.collections[k] = SlicingCollection(objects, k)
|
|
|
|
def offset(self, offset: NDArray[Any]):
|
|
for k, so in self.collections.items(): so.offset(offset)
|
|
|
|
def scale(self, factor: float):
|
|
for k, so in self.collections.items(): so.scale(factor)
|
|
|
|
@property
|
|
def checksum(self):
|
|
buf = bytearray()
|
|
for key, col in sorted(self.collections.items(), key=lambda kv: kv[0]):
|
|
key_bytes = key.encode('utf-8')
|
|
buf.extend(struct.pack(">I", len(key_bytes)))
|
|
buf.extend(key_bytes)
|
|
buf.extend(struct.pack(">I", col.checksum))
|
|
|
|
return zlib.crc32(buf) & 0xFFFFFFFF
|
|
|
|
@property
|
|
def height(self) -> float: return max([so.height for k, so in self.collections.items() if so.height], default=0)
|
|
|
|
@property
|
|
def min_x(self) -> float: return min([so.min_x for k, so in self.collections.items() if so.min_x], default=0)
|
|
|
|
@property
|
|
def max_x(self) -> float: return max([so.max_x for k, so in self.collections.items() if so.max_x], default=0)
|
|
|
|
@property
|
|
def min_y(self) -> float: return min([so.min_y for k, so in self.collections.items() if so.min_y], default=0)
|
|
|
|
@property
|
|
def max_y(self) -> float: return max([so.max_y for k, so in self.collections.items() if so.max_y is not None], default=0)
|
|
|
|
@property
|
|
def min_z(self) -> float: return min([so.min_z for k, so in self.collections.items() if so.min_z is not None], default=0)
|
|
|
|
@property
|
|
def max_z(self) -> float: return max([so.max_z for k, so in self.collections.items() if so.max_z is not None], default=0)
|
|
|
|
@property
|
|
def min_xy(self) -> NDArray[Any]:
|
|
if self.min_x is None or self.min_y is None: return np.array([.0, .0, .0])
|
|
return np.array([self.min_x, self.min_y, 0.0])
|
|
|
|
@property
|
|
def max_xy(self) -> NDArray[Any]:
|
|
if self.max_x is None or self.max_y is None: return np.array([.0, .0, .0])
|
|
return np.array([self.max_x, self.max_y, 0.0])
|
|
|
|
@property
|
|
def center_xy(self) -> NDArray[Any]:
|
|
if self.min_xy is None or self.max_xy is None: return np.array([.0, .0, .0])
|
|
return (self.min_xy + self.max_xy) / 2.0
|
|
|
|
class TriMesh():
|
|
mesh: Mesh
|
|
length_verts: int
|
|
length_tris: int
|
|
matrix_world: Any
|
|
|
|
def __init__(self, mesh: Mesh, v: int, t: int, m) -> None:
|
|
self.mesh = mesh
|
|
self.length_verts = v
|
|
self.length_tris = t
|
|
self.matrix_world = m
|
|
|
|
def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[float, ...], ...], scale: float) -> tuple[NDArray[float64], dict[str, dict[str, Any]]]:
|
|
tris_count: int = 0
|
|
meshes: list[TriMesh] = []
|
|
|
|
with ExitStack() as stack:
|
|
for name in obj_names:
|
|
obj = bpy.data.objects.get(name)
|
|
if not obj: continue
|
|
|
|
eval_obj, mesh = stack.enter_context(get_evaluated_mesh(obj))
|
|
if not mesh: continue
|
|
mesh.calc_loop_triangles()
|
|
|
|
cur_length = len(mesh.loop_triangles)
|
|
if cur_length == 0: continue
|
|
|
|
tris_count += cur_length
|
|
|
|
meshes += [TriMesh(
|
|
mesh,
|
|
len(mesh.vertices),
|
|
len(mesh.loop_triangles),
|
|
np.array(obj.matrix_world.transposed())
|
|
)]
|
|
|
|
tris_flat: np.ndarray = np.empty(tris_count * 4 * 3, dtype=np.float64)
|
|
tris: np.ndarray = tris_flat.reshape(-1, 4, 3)
|
|
|
|
# Identify all FACE domain attributes
|
|
face_attributes_info = {}
|
|
for trimesh in meshes:
|
|
for attr in trimesh.mesh.attributes:
|
|
if attr.domain == 'FACE':
|
|
face_attributes_info[attr.name] = attr.data_type
|
|
|
|
blender_type_map = {
|
|
'BOOLEAN': 'bool',
|
|
'INT': 'int',
|
|
'FLOAT': 'float',
|
|
'FLOAT_VECTOR': 'float_vector',
|
|
'FLOAT_COLOR': 'color',
|
|
'BYTE_COLOR': 'color',
|
|
}
|
|
|
|
attributes_data = {}
|
|
for name, data_type in face_attributes_info.items():
|
|
if data_type not in blender_type_map: continue
|
|
|
|
my_type = blender_type_map[data_type]
|
|
|
|
if data_type == 'BOOLEAN':
|
|
dtype, shape_ext = np.bool_, ()
|
|
elif data_type == 'INT':
|
|
dtype, shape_ext = np.int32, ()
|
|
elif data_type == 'FLOAT':
|
|
dtype, shape_ext = np.float32, ()
|
|
elif data_type == 'FLOAT_VECTOR':
|
|
dtype, shape_ext = np.float32, (3,)
|
|
elif data_type in ('BYTE_COLOR', 'FLOAT_COLOR'):
|
|
dtype, shape_ext = np.float32, (4,)
|
|
else: continue
|
|
|
|
attributes_data[name] = {
|
|
"domain": "face",
|
|
"type": my_type,
|
|
"values": np.zeros((tris_count,) + shape_ext, dtype=dtype)
|
|
}
|
|
|
|
col_idx = 0
|
|
for trimesh in meshes:
|
|
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 = 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
|
|
tx_verts = (tx_verts[:, :3]) * scale
|
|
|
|
tx_norm = tris_v_n @ trimesh.matrix_world[:3, :3]
|
|
norms_len = np.linalg.norm(tx_norm, axis=1, keepdims=True)
|
|
norms_len = np.where(norms_len == 0, 1.0, norms_len)
|
|
tx_norm = (tx_norm / norms_len)[:, np.newaxis, :]
|
|
|
|
tx_tris = tx_verts[tris_v_i]
|
|
|
|
tris_coords_and_norm: NDArray[float64] = np.concatenate((tx_tris, tx_norm), axis=1)
|
|
|
|
tris[col_idx:col_idx + trimesh.length_tris,:] = tris_coords_and_norm
|
|
|
|
# Attributes
|
|
if attributes_data:
|
|
poly_indices = np.empty(trimesh.length_tris, dtype=np.int32)
|
|
trimesh.mesh.loop_triangles.foreach_get("polygon_index", poly_indices)
|
|
|
|
for name, attr_dict in attributes_data.items():
|
|
if name in trimesh.mesh.attributes:
|
|
attr = trimesh.mesh.attributes[name]
|
|
data_len = len(attr.data)
|
|
|
|
if attr.data_type == 'FLOAT_VECTOR':
|
|
raw_values = np.empty(data_len * 3, dtype=np.float32)
|
|
attr.data.foreach_get("vector", raw_values)
|
|
raw_values = raw_values.reshape(-1, 3)
|
|
elif attr.data_type == 'FLOAT_COLOR':
|
|
raw_values = np.empty(data_len * 4, dtype=np.float32)
|
|
attr.data.foreach_get("color", raw_values)
|
|
raw_values = raw_values.reshape(-1, 4)
|
|
elif attr.data_type == 'BYTE_COLOR':
|
|
temp_values = np.empty(data_len * 4, dtype=np.uint8)
|
|
attr.data.foreach_get("color", temp_values)
|
|
raw_values = temp_values.astype(np.float32) / 255.0
|
|
raw_values = raw_values.reshape(-1, 4)
|
|
else:
|
|
raw_values = np.empty(data_len, dtype=attr_dict["values"].dtype)
|
|
attr.data.foreach_get("value", raw_values)
|
|
|
|
attr_dict["values"][col_idx:col_idx + trimesh.length_tris] = raw_values[poly_indices]
|
|
|
|
col_idx += trimesh.length_tris
|
|
|
|
return tris, attributes_data
|
|
|
|
def objects_to_tris(objects: list[Object], scale: float) -> tuple[NDArray[float64], dict[str, dict[str, Any]]]:
|
|
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)
|
|
|
|
def _cached_capture_mesh_edges(
|
|
obj_name: str,
|
|
matrix_tuple: tuple[float, ...],
|
|
scene_scale: float,
|
|
e_attr_name: str = "e_mult",
|
|
f_attr_name: str = "f_rate",
|
|
extra_attrs: dict[str, str] | None = None
|
|
) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
|
obj = bpy.data.objects.get(obj_name)
|
|
if not obj: return [], [], [], {}
|
|
|
|
with get_evaluated_mesh(obj) as (eval_obj, mesh):
|
|
if not mesh or not mesh.vertices:
|
|
return [], [], [], {}
|
|
|
|
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())
|
|
|
|
e_mults = np.ones(num_verts, dtype=np.float64)
|
|
if e_attr_name in mesh.attributes:
|
|
mesh.attributes[e_attr_name].data.foreach_get("value", e_mults)
|
|
|
|
f_rates = np.zeros(num_verts, dtype=np.float64)
|
|
if f_attr_name in mesh.attributes:
|
|
mesh.attributes[f_attr_name].data.foreach_get("value", f_rates)
|
|
|
|
captured_extras = {}
|
|
if extra_attrs:
|
|
for attr_key, prefix in extra_attrs.items():
|
|
vals = np.zeros(num_verts, dtype=np.float64)
|
|
if attr_key in mesh.attributes:
|
|
mesh.attributes[attr_key].data.foreach_get("value", vals)
|
|
captured_extras[prefix] = vals
|
|
|
|
tx_verts = (verts @ world_mat[:3, :3].T + world_mat[:3, 3]) * scene_scale
|
|
|
|
edges_set = {tuple(sorted(e.vertices)) for e in mesh.edges}
|
|
connected = np.array([(i, i + 1) in edges_set for i in range(num_verts - 1)])
|
|
|
|
split_at = np.where(~connected)[0] + 1
|
|
raw_paths = np.split(tx_verts, split_at)
|
|
raw_e_mults = np.split(e_mults, split_at)
|
|
raw_f_rates = np.split(f_rates, split_at)
|
|
raw_extras = {prefix: np.split(vals, split_at) for prefix, vals in captured_extras.items()}
|
|
indices_split = np.split(np.arange(num_verts), split_at)
|
|
|
|
final_paths = []
|
|
final_e_mults = []
|
|
final_f_rates = []
|
|
final_extras = {prefix: [] for prefix in captured_extras}
|
|
for i, path_coords in enumerate(raw_paths):
|
|
if path_coords.shape[0] == 0:
|
|
continue
|
|
idx = indices_split[i]
|
|
e_vals = raw_e_mults[i]
|
|
f_vals = raw_f_rates[i]
|
|
p_extras = {prefix: raw_extras[prefix][i] for prefix in captured_extras}
|
|
|
|
if len(idx) > 2 and tuple(sorted((idx[0], idx[-1]))) in edges_set:
|
|
path_coords = np.vstack((path_coords, path_coords[0]))
|
|
e_vals = np.concatenate((e_vals, [e_vals[0]]))
|
|
f_vals = np.concatenate((f_vals, [f_vals[0]]))
|
|
for prefix in captured_extras:
|
|
p_extras[prefix] = np.concatenate((p_extras[prefix], [p_extras[prefix][0]]))
|
|
|
|
final_paths.append(path_coords)
|
|
final_e_mults.append(e_vals)
|
|
final_f_rates.append(f_vals)
|
|
for prefix in captured_extras:
|
|
final_extras[prefix].append(p_extras[prefix])
|
|
|
|
return final_paths, final_e_mults, final_f_rates, final_extras
|
|
|
|
|
|
def capture_mesh_edges(obj: Object, e_attr_name: str = "e_mult", f_attr_name: str = "f_rate", extra_attrs: dict[str, str] | None = None) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
|
if not obj or not obj.data: return [], [], [], {}
|
|
|
|
if obj.mode == 'EDIT':
|
|
obj.update_from_editmode()
|
|
|
|
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,
|
|
matrix_tuple,
|
|
scene_scale,
|
|
e_attr_name,
|
|
f_attr_name,
|
|
extra_attrs
|
|
) |