From 90a2c61fb41030db9c2fbd81ef6bc59a2b32c086 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Wed, 3 Jun 2026 19:33:20 +0300 Subject: [PATCH] capture attributes of face domain --- core/nodeslicer/nodes/path.py | 4 +- infra/blender/blender_mesh_capture.py | 117 +++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 7 deletions(-) diff --git a/core/nodeslicer/nodes/path.py b/core/nodeslicer/nodes/path.py index 4622d0d..cf31ca0 100644 --- a/core/nodeslicer/nodes/path.py +++ b/core/nodeslicer/nodes/path.py @@ -50,9 +50,7 @@ class SLICENODE_SliceShells(SliceNode): if not coll: return SlicePaths() - merged_object = SlicingObject() - for obj in coll.objects: - merged_object.mesh = np.concatenate([merged_object.mesh, obj.mesh], axis=0) + merged_object = SlicingObject.merge(coll.objects) if rotation and any(rotation): from mathutils import Euler diff --git a/infra/blender/blender_mesh_capture.py b/infra/blender/blender_mesh_capture.py index 1b2a2d9..c259e9e 100644 --- a/infra/blender/blender_mesh_capture.py +++ b/infra/blender/blender_mesh_capture.py @@ -21,6 +21,7 @@ class SlicingObject(): name: str parent: str mesh: NDArray[float64] + attributes: dict[str, dict] object_type: str = "ModelPart" modifiers: list[dict] = [] extruder: int = 1 @@ -30,6 +31,7 @@ class SlicingObject(): if obj: self.name = str(obj.name) self.parent = parent + self.attributes = {} depsgraph = bpy.context.evaluated_depsgraph_get() assert bpy.context.scene.unit_settings @@ -44,9 +46,47 @@ class SlicingObject(): if obj.mode == 'EDIT': obj.update_from_editmode() eval_objects = obj.evaluated_get(depsgraph) - self.mesh = objects_to_tris([eval_objects], 1000 * scene_scale) + self.mesh, self.attributes = objects_to_tris([eval_objects], 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.object_type = objects[0].object_type + merged.extruder = objects[0].extruder + + 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): self.mesh[:, :3, :] += offset @@ -334,7 +374,7 @@ class TriMesh(): self.length_tris = t self.matrix_world = m -def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[float, ...], ...], scale: float) -> NDArray[float64]: +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] = [] @@ -366,6 +406,46 @@ def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[fl 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) @@ -396,11 +476,40 @@ def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[fl 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 + return tris, attributes_data -def objects_to_tris(objects: list[Object], scale) -> NDArray[float64]: +def objects_to_tris(objects: list[Object], scale) -> tuple[NDArray[float64], dict[str, dict]]: 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)