diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 35c4b12dfa..bcf9f31e3d 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -111,7 +111,7 @@ class MaterialCreator: material = self.styles[style_or_material_id] if coords := self.get_ifc_coordinate(material): - tool.Loader.load_indexed_texture_map(coords, self.mesh) + tool.Loader.load_indexed_map(coords, self.mesh) def assign_material_slots_to_faces(self) -> None: if not self.mesh["ios_materials"]: @@ -1382,6 +1382,12 @@ class IfcImporter: mesh.polygons.foreach_set("loop_total", loop_total) mesh.polygons.foreach_set("use_smooth", [0] * total_faces) mesh.update() + + # TODO: geometry id is not always an int. + rep_id = geometry.id + if rep_id.isdigit(): + rep = self.file.by_id(int(rep_id)) + tool.Loader.load_indexed_colour_map(rep, mesh) else: e = geometry.edges v = verts diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 30a064bbfd..875371eab5 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -648,6 +648,7 @@ class Geometry(bonsai.core.tool.Geometry): ifc_importer.material_creator.load_existing_materials() ifc_importer.material_creator.create(element, obj, mesh) mesh.BIMMeshProperties.has_openings_applied = apply_openings + tool.Loader.load_indexed_colour_map(representation, mesh) return mesh diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 4a86a754dc..dd2a4acb53 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -472,20 +472,47 @@ class Loader(bonsai.core.tool.Loader): blender_material.node_tree.links.new(coord.outputs["UV"], node.inputs["Vector"]) @classmethod - def load_indexed_texture_map(cls, coordinates: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> None: + def load_indexed_colour_map(cls, representation: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> None: + if representation.RepresentationType != "Tessellation": + return + + colours = [] + for item in representation.Items: + if not item.is_a("IfcTessellatedFaceSet"): + continue + colours.extend(item.HasColours) + + if not colours: + return + + for colour in colours: + cls.load_indexed_map(colour, mesh) + + @classmethod + def load_indexed_map(cls, index_map: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> None: + """Add data from index map as blender mesh attribute. + + :param index_map: IfcIndexedTextureMap or IfcIndexedColourMap + """ + + map_type = "UV" if index_map.is_a("IfcIndexedTextureMap") else "Color" + # Get a BMesh representation bm = bmesh.new() bm.from_mesh(mesh) - # constistent naming with how Blender does it - uv_layer = bm.loops.layers.uv.active or bm.loops.layers.uv.new("UVMap") + if map_type == "UV": + # constistent naming with how Blender does it + layer = bm.loops.layers.uv.active or bm.loops.layers.uv.new("UVMap") + else: + layer = bm.loops.layers.float_color.new("Color") # remap the faceset CoordList index to the vertices in blender mesh coordinates_remap = [] si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - faceset = coordinates.MappedTo + faceset = index_map.MappedTo for co in faceset.Coordinates.CoordList: co = Vector(co) * si_conversion - index = next(v.index for v in bm.verts if (v.co - co).length_squared < 1e-5) + index = min(bm.verts, key=lambda v: (v.co - co).length_squared).index coordinates_remap.append(index) # ifc indices start with 1 @@ -493,37 +520,61 @@ class Loader(bonsai.core.tool.Loader): # faces_remap - ifc faces described using blender verts indices # IFC4.3+ - if coordinates.is_a("IfcIndexedPolygonalTextureMap"): + if index_map.is_a("IfcIndexedPolygonalTextureMap"): faces_remap = [ remap_verts_to_blender(tex_coord_index.TexCoordsOf.CoordIndex) - for tex_coord_index in coordinates.TexCoordIndices + for tex_coord_index in index_map.TexCoordIndices ] - texture_map = [tex_coord_index.TexCoordIndex for tex_coord_index in coordinates.TexCoordIndices] - else: # IfcIndexedTriangleTextureMap + texture_map = [tex_coord_index.TexCoordIndex for tex_coord_index in index_map.TexCoordIndices] + else: # IfcIndexedTriangleTextureMap or IfcIndexedColourMap if faceset.is_a("IfcTriangulatedFaceSet"): faces_remap = [remap_verts_to_blender(triangle_face) for triangle_face in faceset.CoordIndex] else: # IfcPolygonalFaceSet - faces_remap = [remap_verts_to_blender(triangle_face.CoordIndex) for triangle_face in faceset.Faces] - texture_map = coordinates.TexCoordIndex + faces_remap = [remap_verts_to_blender(face.CoordIndex) for face in faceset.Faces] + if index_map.is_a("IfcIndexedTriangleTextureMap"): + texture_map = index_map.TexCoordIndex + else: + texture_map = index_map.ColourIndex - # apply uv to each face + if map_type == "UV": + data_list = index_map.TexCoords.TexCoordsList + else: + data_list = index_map.Colours.ColourList + opacity = index_map.Opacity + opacity = opacity if opacity is not None else 1.0 + data_list = [d + (opacity,) for d in data_list] + + # Apply attribute to each face for bface in bm.faces: face = [loop.vert.index for loop in bface.loops] - # find the corresponding TexCoordIndex by matching ifc faceset with blender face - # remap TexCoordIndex as the loop start may different from blender face - texCoordIndex = next( - [tex_coord_index[face_remap.index(i)] for i in face] - for tex_coord_index, face_remap in zip(texture_map, faces_remap, strict=True) - if all(i in face_remap for i in face) - ) + # Find the corresponding index in data list by matching ifc faceset with blender face. + data_index = None + for tex_coord_index, face_remap in zip(texture_map, faces_remap, strict=True): + if not all(i in face_remap for i in face): + continue + # Subtract 1 as tex_coord_index starts with 1. + if map_type == "UV": + data_index = [tex_coord_index[face_remap.index(i)] - 1 for i in face] + else: + data_index = [tex_coord_index - 1 for i in face] + break + assert data_index is not None + # apply uv to each loop - for loop, i in zip(bface.loops, texCoordIndex): - loop[uv_layer].uv = coordinates.TexCoords.TexCoordsList[i - 1] + for loop, i in zip(bface.loops, data_index): + if map_type == "UV": + loop[layer].uv = data_list[i] + else: + loop[layer] = data_list[i] # Finish up, write the bmesh back to the mesh bm.to_mesh(mesh) bm.free() + if map_type == "Color": + # Couldn't find a way to do it from bmesh. + mesh.color_attributes.active_color_index = 0 + @classmethod def is_point_far_away( cls, point: Union[ifcopenshell.entity_instance, npt.NDArray[np.float64]], is_meters: bool = True diff --git a/src/bonsai/test/tool/test_loader.py b/src/bonsai/test/tool/test_loader.py index 7fa66955d4..27826cd303 100644 --- a/src/bonsai/test/tool/test_loader.py +++ b/src/bonsai/test/tool/test_loader.py @@ -391,12 +391,11 @@ class TestCreatingStyles(NewFile): ), f"Failed to match pixels for {n_components}.\nBlender pixel_data: {image_node.image.pixels[:]}.\nExpected data: {expected_pixel_data}" -class TestLoadingIndexedTextureMap(NewFile): - def test_run(self): +class TestLoadingIndexedMap(NewFile): + def test_load_texture_map(self): bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" bpy.ops.bim.create_project() - # TODO: replace with loading geometry from IFC # create a cube without uv and triangulate it mesh = bpy.data.meshes.new("Cube") bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True) @@ -408,31 +407,7 @@ class TestLoadingIndexedTextureMap(NewFile): builder = ShapeBuilder(ifc_file) # tesselated cube - points = ( - V(-1000, -1000, -1000), - V(-1000, -1000, 1000), - V(-1000, 1000, -1000), - V(-1000, 1000, 1000), - V(1000, -1000, -1000), - V(1000, -1000, 1000), - V(1000, 1000, -1000), - V(1000, 1000, 1000), - ) - faces = ( - (1, 2, 0), - (3, 6, 2), - (7, 4, 6), - (5, 0, 4), - (6, 0, 2), - (3, 5, 7), - (1, 3, 2), - (3, 7, 6), - (7, 5, 4), - (5, 1, 0), - (6, 4, 0), - (3, 1, 5), - ) - face_set = builder.polygonal_face_set(points, faces) + face_set = builder.polygonal_face_set([v.co for v in mesh.vertices], [p.vertices[:] for p in mesh.polygons]) uv_indices = ( (1, 2, 3), @@ -493,9 +468,45 @@ class TestLoadingIndexedTextureMap(NewFile): texture_coord.TexCoordIndex = uv_indices texture_coord.TexCoords = uv_verts_list - subject.load_indexed_texture_map(texture_coord, mesh) + subject.load_indexed_map(texture_coord, mesh) uv_layer = mesh.uv_layers.active assert uv_layer is not None for ifc_uv, blender_uv in zip(uv_verts, uv_layer.uv, strict=True): assert tool.Cad.are_vectors_equal(Vector(ifc_uv), blender_uv.vector) + + def test_load_color_map(self): + bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" + bpy.ops.bim.create_project() + + # create a cube without uv and triangulate it + mesh = bpy.data.meshes.new("Cube") + bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True) + bmesh.ops.create_cube(bm, size=2.0, calc_uvs=False) + bmesh.ops.triangulate(bm, faces=bm.faces[:]) + tool.Blender.apply_bmesh(mesh, bm) + + ifc_file = tool.Ifc.get() + builder = ShapeBuilder(ifc_file) + + # tesselated cube + face_set = builder.polygonal_face_set([v.co for v in mesh.vertices], [p.vertices[:] for p in mesh.polygons]) + + colors = ((1.0, 0.0, 0.0), (0.0, 0.5, 0.0), (1.0, 1.0, 0.0)) + color_index = (0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 2, 1) + + ifc_colors_list = ifc_file.create_entity("IfcColourRgbList", colors) + colour_map = ifc_file.create_entity("IfcIndexedColourMap") + colour_map.MappedTo = face_set + colour_map.Colours = ifc_colors_list + colour_map.ColourIndex = [i + 1 for i in color_index] + + subject.load_indexed_map(colour_map, mesh) + + layer = mesh.color_attributes.active_color + assert layer is not None + for face_i, face in enumerate(mesh.polygons): + for loop_i in face.loop_indices: + blender_color = Vector(layer.data[loop_i].color) + ifc_color = Vector(colors[color_index[face_i]] + (1.0,)) + assert tool.Cad.are_vectors_equal(blender_color, ifc_color)