From 405e8bc5aaca685c38079758adc7839110896b9b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 11 Mar 2021 21:57:47 +1100 Subject: [PATCH] You can now automagically create arbitrary polygonal profiles with voids from meshes --- .../bim/module/geometry/add_representation.py | 17 +++ .../blenderbim/bim/module/geometry/helper.py | 131 +++++++++++++++++- .../blenderbim/bim/module/geometry/ui.py | 4 + 3 files changed, 147 insertions(+), 5 deletions(-) diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py index 1a9d85d5a8..ad0e32225c 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py @@ -25,6 +25,7 @@ class Usecase: # IfcExtrudedAreaSolid/IfcRectangleProfileDef # IfcExtrudedAreaSolid/IfcCircleProfileDef # IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef + # IfcExtrudedAreaSolid/IfcArbitraryProfileDef "ifc_representation_class": None, # Whether to cast a mesh into a particular class } self.ifc_vertices = [] @@ -139,6 +140,8 @@ class Usecase: return self.create_circle_extrusion_representation() elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef": return self.create_arbitrary_extrusion_representation() + elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids": + return self.create_arbitrary_void_extrusion_representation() return self.create_mesh_representation() def create_camera_block_representation(self): @@ -302,6 +305,20 @@ class Usecase: [item], ) + def create_arbitrary_void_extrusion_representation(self): + helper = Helper(self.file) + indices = helper.auto_detect_arbitrary_profile_with_voids_extruded_area_solid(self.settings["geometry"]) + profile_def = helper.create_arbitrary_profile_def_with_voids( + self.settings["geometry"], indices["profile"], indices["inner_curves"] + ) + item = helper.create_extruded_area_solid(self.settings["geometry"], indices["extrusion"], profile_def) + return self.file.createIfcShapeRepresentation( + self.settings["context"], + self.settings["context"].ContextIdentifier, + "SweptSolid", + [item], + ) + def create_mesh_representation(self): if self.file.schema == "IFC2X3" or self.settings["should_force_faceted_brep"]: return self.create_faceted_brep() diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/helper.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/helper.py index 7cf1bb78ac..d17b535c13 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/helper.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/helper.py @@ -18,7 +18,7 @@ class Helper: def auto_detect_rectangle_profile_extruded_area_solid(self, mesh): bm = bmesh.new() bm.from_mesh(mesh) - bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges) + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) bm.faces.ensure_lookup_table() face = None @@ -41,7 +41,7 @@ class Helper: def auto_detect_circle_profile_extruded_area_solid(self, mesh): bm = bmesh.new() bm.from_mesh(mesh) - bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges) + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) bm.faces.ensure_lookup_table() potential_faces = [] @@ -68,7 +68,7 @@ class Helper: def auto_detect_arbitrary_closed_profile_extruded_area_solid(self, mesh): bm = bmesh.new() bm.from_mesh(mesh) - bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges) + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) bm.faces.ensure_lookup_table() potential_faces = [] @@ -93,13 +93,119 @@ class Helper: return {"profile": profile, "extrusion": extrusion} + # An arbitrary closed profile with voids is similar to one without voids. + # We start the same way with any ngon (no tri), but instead of being the entire + # profile, it is only one of the possible faces that make up the end of our + # extrusion. Then we find all faces with the same normal. This creates a set + # of faces that define the end of our extrusion. From this set of faces, we + # need to then extract the outer loop and inner loops. First, all loops are + # detected from the faces. Any edge that is not shared between other faces + # in the set must be part of either an inner or outer loop. This gives us a + # set of edges. Then, connected edges (i.e. sharing a vertex) are joined to + # form distinct loops. Finally, the outer loop is distinguished by being + # the loop with the greatest area. + def auto_detect_arbitrary_profile_with_voids_extruded_area_solid(self, mesh): + bm = bmesh.new() + bm.from_mesh(mesh) + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) + + bm.faces.ensure_lookup_table() + potential_faces = [] + for face in bm.faces: + total_verts = len(face.verts) + if total_verts > 4: + potential_faces.append(face) + + for face in potential_faces: + if face.normal.z < -0.1: + break + + end_faces = [] + end_face_normal = face.normal + for face in bm.faces: + if (face.normal - end_face_normal).length < 0.001: + end_faces.append(face) + + loop_edges = set() + for face in end_faces: + potential_edges = set(face.edges) + for face2 in end_faces: + if face == face2: + continue + potential_edges -= set(face2.edges) + loop_edges |= potential_edges + + # Create loops from edges + loops = [] + while loop_edges: + edge = loop_edges.pop() + loop = [edge] + has_found_connected_edge = True + while has_found_connected_edge: + has_found_connected_edge = False + for edge in loop_edges.copy(): + edge_verts = set(edge.verts) + if edge_verts & set(loop[0].verts): + loop.insert(0, edge) + loop_edges.remove(edge) + has_found_connected_edge = True + elif edge_verts & set(loop[-1].verts): + loop.append(edge) + loop_edges.remove(edge) + has_found_connected_edge = True + loops.append(loop) + + # Determine outer loop + max_area = 0 + outer_loop = None + inner_loops = [] + + for loop in loops: + loop_vertices = [] + total_edges = len(loop) + for i, edge in enumerate(loop): + if i + 1 == total_edges and edge.verts[0] in loop[i - 1].verts: + loop_vertices.append(edge.verts[0]) + elif i + 1 == total_edges and edge.verts[1] in loop[i - 1].verts: + loop_vertices.append(edge.verts[1]) + elif edge.verts[0] in loop[i + 1].verts: + loop_vertices.append(edge.verts[1]) + elif edge.verts[1] in loop[i + 1].verts: + loop_vertices.append(edge.verts[0]) + + loop_bm = bmesh.new() + for vert in loop_vertices: + loop_bm.verts.new(vert.co) + face = loop_bm.faces.new(loop_bm.verts) + + loop_vertex_indices = [v.index for v in loop_vertices] + face_area = face.calc_area() + if face_area > max_area: + max_area = face_area + outer_loop = loop_vertex_indices + inner_loops.append(loop_vertex_indices) + loop_bm.free() + + inner_loops.remove(outer_loop) + + extrusion = self.detect_extrusion_edge(bm, end_faces[0]) + + bm.to_mesh(mesh) + mesh.update() + bm.free() + + return {"profile": outer_loop, "inner_curves": inner_loops, "extrusion": extrusion} + + # An extrusion edge is an edge that shares a single vertex with a profile + # face and is not parallel to the face. def detect_extrusion_edge(self, bm, profile_face): bm.edges.ensure_lookup_table() extrusion = None face_verts_set = set(profile_face.verts) for edge in bm.edges: + edge_vector = edge.verts[1].co - edge.verts[0].co unshared_verts = set(edge.verts) - face_verts_set - if len(unshared_verts) == 1: + if len(unshared_verts) == 1 and not (edge_vector.angle(profile_face.normal) - pi / 2 < 0.001): if unshared_verts.pop() == edge.verts[1]: return [edge.verts[0].index, edge.verts[1].index] return [edge.verts[1].index, edge.verts[0].index] @@ -123,6 +229,15 @@ class Helper: curve = self.file.createIfcArbitraryClosedProfileDef("AREA", None, outer_curve) return {"curve_ucs": curve_ucs, "curve": curve} + def create_arbitrary_profile_def_with_voids(self, mesh, profile_indices, inner_curve_indices): + curve_ucs = self.get_curve_profile_coordinate_system(mesh, profile_indices) + outer_curve = self.create_polyline_from_loop(mesh, profile_indices, curve_ucs) + inner_curves = [ + self.create_polyline_from_loop(mesh, curve_indices, curve_ucs) for curve_indices in inner_curve_indices + ] + curve = self.file.createIfcArbitraryProfileDefWithVoids("AREA", None, outer_curve, inner_curves) + return {"curve_ucs": curve_ucs, "curve": curve} + def create_rectangle_profile_def(self, mesh, profile_indices): curve_ucs = self.get_curve_profile_coordinate_system(mesh, profile_indices) xdim = self.convert_si_to_unit( @@ -137,7 +252,13 @@ class Helper: def create_circle_profile_def(self, mesh, profile_indices): curve_ucs = self.get_curve_profile_coordinate_system(mesh, profile_indices) radius = self.convert_si_to_unit( - abs((mesh.vertices[profile_indices[0]].co - mesh.vertices[profile_indices[int(len(profile_indices) / 2)]].co).length) / 2 + abs( + ( + mesh.vertices[profile_indices[0]].co + - mesh.vertices[profile_indices[int(len(profile_indices) / 2)]].co + ).length + ) + / 2 ) center = Vector((0, 0)) position = self.create_ifc_axis_2_placement_2d(center, Vector((1, 0))) diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py index 7f3762c1d4..62031bd38b 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py @@ -93,6 +93,10 @@ class BIM_PT_mesh(Panel): op = row.operator("bim.update_mesh_representation", text="Update Mesh As Arbitrary Extrusion") op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef" + row = layout.row() + op = row.operator("bim.update_mesh_representation", text="Update Mesh As Arbitrary Extrusion With Voids") + op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" + row = layout.row() row.operator("bim.get_representation_ifc_parameters") for index, ifc_parameter in enumerate(props.ifc_parameters):