diff --git a/infra/blender/blender_mesh_capture.py b/infra/blender/blender_mesh_capture.py index f50d446..3b24cd0 100644 --- a/infra/blender/blender_mesh_capture.py +++ b/infra/blender/blender_mesh_capture.py @@ -3,6 +3,7 @@ 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 @@ -17,6 +18,74 @@ 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) + if m and len(m.vertices) > 0 and not is_hidden and (not obj.modifiers or len(m.vertices) != len(obj.data.vertices)): + 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 + bpy.context.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: + bpy.context.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: + bpy.context.scene.collection.objects.unlink(temp_obj) + bpy.data.objects.remove(temp_obj, do_unlink=True) + bpy.context.evaluated_depsgraph_get().update() + except Exception: + pass + class SlicingObject(): name: str parent: str @@ -34,7 +103,6 @@ class SlicingObject(): self.attributes = {} self.modifiers = [] - depsgraph = bpy.context.evaluated_depsgraph_get() assert bpy.context.scene.unit_settings scene_scale: float = bpy.context.scene.unit_settings.scale_length @@ -44,10 +112,7 @@ class SlicingObject(): self.modifiers = [{'param_id': m.field, 'param_value': m.value_str} for m in ns.modifiers] if obj: - if obj.mode == 'EDIT': - obj.update_from_editmode() - eval_objects = obj.evaluated_get(depsgraph) - self.mesh, self.attributes = objects_to_tris([eval_objects], 1000 * scene_scale) + self.mesh, self.attributes = objects_to_tris([obj], 1000 * scene_scale) else: self.mesh = np.empty((0, 4, 3)) self.attributes = {} @@ -347,19 +412,17 @@ class SlicingGroup(): def _extract_metadata(self, objs) -> None: import math - depsgraph = bpy.context.evaluated_depsgraph_get() + assert bpy.context.scene and bpy.context.scene.unit_settings scale_tx = 1000. * bpy.context.scene.unit_settings.scale_length #type: ignore self.wipe_tower_xy = self.center_xy[0:2] for obj in objs: - if obj.mode == 'EDIT': - obj.update_from_editmode() - eval_obj = obj.evaluated_get(depsgraph) - if hasattr(eval_obj, 'nodeslicer'): - if eval_obj.nodeslicer.object_type == 'WipeTower': - self.wipe_tower_xy = np.array(eval_obj.location[0:2]) * scale_tx - self.wipe_tower_rotation_deg = round(eval_obj.rotation_euler[2]*180/math.pi,5) - break + with get_evaluated_mesh(obj) as (eval_obj, _m): + if eval_obj and hasattr(eval_obj, 'nodeslicer'): + if eval_obj.nodeslicer.object_type == 'WipeTower': + self.wipe_tower_xy = np.array(eval_obj.location[0:2]) * scale_tx + self.wipe_tower_rotation_deg = round(eval_obj.rotation_euler[2]*180/math.pi,5) + break return @@ -434,139 +497,135 @@ class TriMesh(): 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]]: - # Implementation remains largely the same but uses names to fetch objects tris_count: int = 0 meshes: list[TriMesh] = [] - for name in obj_names: - obj = bpy.data.objects.get(name) - if not obj: continue - - try: - depsgraph = bpy.context.evaluated_depsgraph_get() - # 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() + 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 + 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_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) + 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 + # 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) + blender_type_map = { + 'BOOLEAN': 'bool', + 'INT': 'int', + 'FLOAT': 'float', + 'FLOAT_VECTOR': 'float_vector', + 'FLOAT_COLOR': 'color', + 'BYTE_COLOR': 'color', } - 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 - - homogeneous_norm: NDArray[float64] = np.hstack((tris_v_n, np.ones((tris_v_n.shape[0], 1)))) # type: ignore - tx_norm = homogeneous_norm @ trimesh.matrix_world.T - tx_norm = tx_norm[:, :3] - tx_norm = tx_norm / np.linalg.norm(tx_norm, axis=1, keepdims=True) - tx_norm = tx_norm[:, 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) + attributes_data = {} + for name, data_type in face_attributes_info.items(): + if data_type not in blender_type_map: continue - 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) + 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 + + homogeneous_norm: NDArray[float64] = np.hstack((tris_v_n, np.ones((tris_v_n.shape[0], 1)))) # type: ignore + tx_norm = homogeneous_norm @ trimesh.matrix_world.T + tx_norm = tx_norm[:, :3] + tx_norm = tx_norm / np.linalg.norm(tx_norm, axis=1, keepdims=True) + tx_norm = tx_norm[:, 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) - attr_dict["values"][col_idx:col_idx + trimesh.length_tris] = raw_values[poly_indices] + 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 + col_idx += trimesh.length_tris - return tris, attributes_data + return tris, attributes_data def objects_to_tris(objects: list[Object], scale) -> tuple[NDArray[float64], dict[str, dict]]: obj_names = tuple(o.name for o in objects) @@ -584,17 +643,10 @@ def _cached_capture_mesh_edges( obj = bpy.data.objects.get(obj_name) if not obj: return [], [], [], {} - 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 [], [], [], {} + with get_evaluated_mesh(obj) as (eval_obj, mesh): + if not mesh or not mesh.vertices: + return [], [], [], {} - if not mesh or not mesh.vertices: - return [], [], [], {} - - try: world_mat = np.array(matrix_tuple).reshape(4, 4) num_verts = len(mesh.vertices) verts = np.empty((num_verts, 3), dtype=np.float64) @@ -655,10 +707,6 @@ def _cached_capture_mesh_edges( return final_paths, final_e_mults, final_f_rates, final_extras - finally: - if hasattr(obj_eval, "to_mesh_clear"): - obj_eval.to_mesh_clear() - 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 [], [], [], {}