From 36cfb0d3f58a74d57c4fc58c0b8305cead33ff59 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 28 Feb 2021 12:04:30 +1100 Subject: [PATCH] Meshes can now be automagically converted into circular and arbitrary profile extrusions (swept solids) --- .../bim/module/geometry/add_representation.py | 44 ++++- .../blenderbim/bim/module/geometry/helper.py | 151 +++++++++++------- .../bim/module/geometry/operator.py | 21 ++- .../blenderbim/bim/module/geometry/ui.py | 12 +- 4 files changed, 164 insertions(+), 64 deletions(-) diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py index 0d8498e160..0310719317 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py @@ -21,7 +21,11 @@ class Usecase: "is_wireframe": False, # If the geometry is a wireframe "is_curve": False, # If the geometry is a Blender curve "is_point_cloud": False, # If the geometry is a point cloud - "is_rectangular_extrusion": False, + # Possible IFC representation classes: + # IfcExtrudedAreaSolid/IfcRectangleProfileDef + # IfcExtrudedAreaSolid/IfcCircleProfileDef + # IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef + "ifc_representation_class": None, # Whether to cast a mesh into a particular class } self.ifc_vertices = [] for key, value in settings.items(): @@ -126,8 +130,12 @@ class Usecase: return self.create_curve_representation() elif self.settings["is_point_cloud"]: return self.create_point_cloud_representation() - elif self.settings["is_rectangular_extrusion"]: - return self.create_rectangular_extrusion_representation() + elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcRectangleProfileDef": + return self.create_rectangle_extrusion_representation() + elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcCircleProfileDef": + return self.create_circle_extrusion_representation() + elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef": + return self.create_arbitrary_extrusion_representation() return self.create_mesh_representation() def create_curve3d_representation(self): @@ -219,11 +227,35 @@ class Usecase: results.append(self.file.createIfcPolyline(points)) return results - def create_rectangular_extrusion_representation(self): + def create_rectangle_extrusion_representation(self): helper = Helper(self.file) indices = helper.auto_detect_rectangle_profile_extruded_area_solid(self.settings["geometry"]) - profile_def = helper.create_rectangle_profile_def(self.settings["blender_object"], indices["profile"]) - item = helper.create_extruded_area_solid(self.settings["blender_object"], indices["extrusion"], profile_def) + profile_def = helper.create_rectangle_profile_def(self.settings["geometry"], indices["profile"]) + 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_circle_extrusion_representation(self): + helper = Helper(self.file) + indices = helper.auto_detect_circle_profile_extruded_area_solid(self.settings["geometry"]) + profile_def = helper.create_circle_profile_def(self.settings["geometry"], indices["profile"]) + 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_arbitrary_extrusion_representation(self): + helper = Helper(self.file) + indices = helper.auto_detect_arbitrary_closed_profile_extruded_area_solid(self.settings["geometry"]) + profile_def = helper.create_arbitrary_closed_profile_def(self.settings["geometry"], indices["profile"]) + item = helper.create_extruded_area_solid(self.settings["geometry"], indices["extrusion"], profile_def) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/helper.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/helper.py index b2b28619e8..7cf1bb78ac 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, verts=bm.verts, edges=bm.edges) + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges) bm.faces.ensure_lookup_table() face = None @@ -26,47 +26,89 @@ class Helper: if face.normal.z < -0.1: break profile = [l.vert.index for l in face.loops] - face_verts_set = set(face.verts) - - bm.edges.ensure_lookup_table() - extrusion = None - for edge in bm.edges: - unshared_verts = set(edge.verts) - face_verts_set - if len(unshared_verts) == 1: - if unshared_verts.pop() == edge.verts[1]: - extrusion = [edge.verts[0].index, edge.verts[1].index] - else: - extrusion = [edge.verts[1].index, edge.verts[0].index] - break + extrusion = self.detect_extrusion_edge(bm, face) + bm.to_mesh(mesh) + mesh.update() bm.free() return {"profile": profile, "extrusion": extrusion} # After a limited dissolve, we detect the circle profile as it should be the - # only ngon. The extrusion direction is any edge that only shares a single - # vertex with the profile. We prioritise the profile that has a downwards - # normal. - def auto_detect_circle_profile_extruded_area_solid(self, obj): - # TODO + # only ngon (this assumes the circle has at least a facetation of > 4 edges. + # The extrusion direction is any edge that only shares a single vertex with + # the profile. We prioritise the profile that has a downwards normal. + 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) + + bm.faces.ensure_lookup_table() + potential_faces = [] + for face in bm.faces: + if len(face.verts) > 4: + potential_faces.append(face) + for face in potential_faces: + if face.normal.z < -0.1: + break + + profile = [l.vert.index for l in face.loops] + extrusion = self.detect_extrusion_edge(bm, face) + + bm.to_mesh(mesh) + mesh.update() + bm.free() + + return {"profile": profile, "extrusion": extrusion} # After a limited dissolve, the arbitrary profile is any ngon or tri. # Failing that, it is equivalent to a rectangular profile. The extrusion # direction is any edge that only shares a single vertex with the profile. # We prioritise the profile that has a downwards normal. - def auto_detect_arbitrary_closed_profile_extruded_area_solid(self, obj): - # TODO + 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) - def create_extruded_area_solid(self, obj, extrusion_vertex_indices, profile_def): - extrusion_edge = self.get_edges_in_v_indices(obj, extrusion_vertex_indices)[0] + bm.faces.ensure_lookup_table() + potential_faces = [] + for face in bm.faces: + total_verts = len(face.verts) + if total_verts > 4 or total_verts == 3: + potential_faces.append(face) + + if not potential_faces: + potential_faces = bm.faces + + for face in potential_faces: + if face.normal.z < -0.1: + break + + profile = [l.vert.index for l in face.loops] + extrusion = self.detect_extrusion_edge(bm, face) + + bm.to_mesh(mesh) + mesh.update() + bm.free() + + return {"profile": profile, "extrusion": extrusion} + + 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: + unshared_verts = set(edge.verts) - face_verts_set + if len(unshared_verts) == 1: + 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] + + def create_extruded_area_solid(self, mesh, extrusion_indices, profile_def): position = self.create_ifc_axis_2_placement_3d( profile_def["curve_ucs"]["center"], profile_def["curve_ucs"]["z_axis"], profile_def["curve_ucs"]["x_axis"] ) - direction = self.get_extrusion_direction( - obj, profile_def["outer_curve_loop"], extrusion_edge, profile_def["curve_ucs"] - ) + direction = self.get_extrusion_direction(mesh, extrusion_indices, profile_def["curve_ucs"]) unit_direction = direction.normalized() return self.file.createIfcExtrudedAreaSolid( profile_def["curve"], @@ -75,37 +117,34 @@ class Helper: self.convert_si_to_unit(direction.length), ) - def create_arbitrary_closed_profile_def(self, obj, profile_vertex_indices): - outer_curve_loop = self.get_loop_from_v_indices(obj, profile_vertex_indices) - curve_ucs = self.get_curve_profile_coordinate_system(obj, outer_curve_loop) - outer_curve = self.create_polyline_from_loop(obj, outer_curve_loop, curve_ucs) + def create_arbitrary_closed_profile_def(self, mesh, profile_indices): + curve_ucs = self.get_curve_profile_coordinate_system(mesh, profile_indices) + outer_curve = self.create_polyline_from_loop(mesh, profile_indices, curve_ucs) curve = self.file.createIfcArbitraryClosedProfileDef("AREA", None, outer_curve) - return {"outer_curve_loop": outer_curve_loop, "curve_ucs": curve_ucs, "curve": curve} + return {"curve_ucs": curve_ucs, "curve": curve} - def create_rectangle_profile_def(self, obj, profile_vertex_indices): - outer_curve_loop = self.get_loop_from_v_indices(obj, profile_vertex_indices) - curve_ucs = self.get_curve_profile_coordinate_system(obj, outer_curve_loop) + 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( - (obj.data.vertices[outer_curve_loop[0]].co - obj.data.vertices[outer_curve_loop[1]].co).length + (mesh.vertices[profile_indices[0]].co - mesh.vertices[profile_indices[1]].co).length ) ydim = self.convert_si_to_unit( - (obj.data.vertices[outer_curve_loop[1]].co - obj.data.vertices[outer_curve_loop[2]].co).length + (mesh.vertices[profile_indices[1]].co - mesh.vertices[profile_indices[2]].co).length ) curve = self.file.createIfcRectangleProfileDef("AREA", None, None, xdim, ydim) - return {"outer_curve_loop": outer_curve_loop, "curve_ucs": curve_ucs, "curve": curve} + return {"curve_ucs": curve_ucs, "curve": curve} - def create_circle_profile_def(self, obj, profile_vertex_indices): - indices = profile_vertex_indices - outer_curve_loop = self.get_loop_from_v_indices(obj, indices) - curve_ucs = self.get_curve_profile_coordinate_system(obj, outer_curve_loop) + 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((obj.data.vertices[indices[0]].co - obj.data.vertices[indices[int(len(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))) curve = self.file.createIfcCircleProfileDef("AREA", None, position, radius) - return {"outer_curve_loop": outer_curve_loop, "curve_ucs": curve_ucs, "curve": curve} + return {"curve_ucs": curve_ucs, "curve": curve} + # Not used anywhere, but probably useful in the future def get_loop_from_v_indices(self, obj, indices): edges = self.get_edges_in_v_indices(obj, indices) loop = self.get_loop_from_edges(edges) @@ -150,18 +189,16 @@ class Helper: def get_edges_in_v_indices(self, obj, indices): return [e for e in obj.data.edges if (e.vertices[0] in indices and e.vertices[1] in indices)] - def get_curve_profile_coordinate_system(self, obj, loop): + def get_curve_profile_coordinate_system(self, mesh, loop): profile_face = bpy.data.meshes.new("profile_face") - profile_verts = [ - (obj.data.vertices[p].co.x, obj.data.vertices[p].co.y, obj.data.vertices[p].co.z) for p in loop - ] + profile_verts = [(mesh.vertices[p].co.x, mesh.vertices[p].co.y, mesh.vertices[p].co.z) for p in loop] profile_faces = [tuple(range(0, len(profile_verts)))] profile_face.from_pydata(profile_verts, [], profile_faces) center = profile_face.polygons[0].center - if (obj.data.vertices[loop[1]].co - obj.data.vertices[loop[0]].co).length < 0.01: - x_axis = (obj.data.vertices[loop[0]].co - center).normalized() + if (mesh.vertices[loop[1]].co - mesh.vertices[loop[0]].co).length < 0.01: + x_axis = (mesh.vertices[loop[0]].co - center).normalized() else: - x_axis = (obj.data.vertices[loop[1]].co - obj.data.vertices[loop[0]].co).normalized() + x_axis = (mesh.vertices[loop[1]].co - mesh.vertices[loop[0]].co).normalized() z_axis = profile_face.polygons[0].normal.normalized() y_axis = z_axis.cross(x_axis).normalized() matrix = Matrix((x_axis, y_axis, z_axis)) @@ -177,10 +214,10 @@ class Helper: def convert_si_to_unit(self, co): return co / self.unit_scale - def create_polyline_from_loop(self, obj, loop, curve_ucs): + def create_polyline_from_loop(self, mesh, loop, curve_ucs): points = [] for point in loop: - transformed_point = curve_ucs["matrix"] @ obj.data.vertices[point].co + transformed_point = curve_ucs["matrix"] @ mesh.vertices[point].co points.append(self.create_cartesian_point(transformed_point.x, transformed_point.y)) points.append(points[0]) return self.file.createIfcPolyline(points) @@ -193,15 +230,21 @@ class Helper: z = self.convert_si_to_unit(z) return self.file.createIfcCartesianPoint((x, y, z)) - def get_extrusion_direction(self, obj, outer_curve_loop, extrusion_edge, curve_ucs): - start, end = self.get_start_and_end_of_extrusion(outer_curve_loop, extrusion_edge) - return curve_ucs["matrix"] @ (curve_ucs["center"] + (obj.data.vertices[end].co - obj.data.vertices[start].co)) + def get_extrusion_direction(self, mesh, extrusion_indices, curve_ucs): + return curve_ucs["matrix"] @ ( + curve_ucs["center"] + (mesh.vertices[extrusion_indices[1]].co - mesh.vertices[extrusion_indices[0]].co) + ) def get_start_and_end_of_extrusion(self, profile_points, extrusion_edge): if extrusion_edge.vertices[0] in profile_points: return (extrusion_edge.vertices[0], extrusion_edge.vertices[1]) return (extrusion_edge.vertices[1], extrusion_edge.vertices[0]) + def create_ifc_axis_2_placement_2d(self, point, forward): + return self.file.createIfcAxis2Placement2D( + self.create_cartesian_point(point.x, point.y), self.file.createIfcDirection((forward.x, forward.y)) + ) + def create_ifc_axis_2_placement_3d(self, point, up, forward): return self.file.createIfcAxis2Placement3D( self.create_cartesian_point(point.x, point.y, point.z), diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py index 4cfeffbb4b..60d6f0a86a 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py @@ -306,7 +306,7 @@ class UpdateMeshRepresentation(bpy.types.Operator): bl_idname = "bim.update_mesh_representation" bl_label = "Update Mesh Representation" obj: bpy.props.StringProperty() - ifc_representation_type: bpy.props.StringProperty() + ifc_representation_class: bpy.props.StringProperty() def execute(self, context): if not ContextData.is_loaded: @@ -350,11 +350,28 @@ class UpdateMeshRepresentation(bpy.types.Operator): "total_items": max(1, len(obj.material_slots)), "should_force_faceted_brep": context.scene.BIMGeometryProperties.should_force_faceted_brep, "should_force_triangulation": context.scene.BIMGeometryProperties.should_force_triangulation, - "is_rectangular_extrusion": self.ifc_representation_type == "IfcExtrudedAreaSolid/IfcRectangleProfileDef" + "ifc_representation_class": self.ifc_representation_class } new_representation = add_representation.Usecase(self.file, representation_data).execute() + #if product.is_a("IfcWall"): + # # Generate axis representation + # axis_context_id = get_context_id("Model", "Axis", "MODEL_VIEW") + # old_axis = ifcopenshell.util.element.get_representation(product, "Model", "Axis", "MODEL_VIEW") + # if ( + # axis_context_id + # and old_axis + # and context_of_items.ContextType == "Model" + # and context_of_items.ContextIdentifier + # and context_of_items.ContextIdentifier == "Body" + # ): + # has_axis_generator = False + # if has_axis_generator: + # # TODO, just pseudocode for now + # representation_data["geometry"] = axis_generator_function_call + # pass + box_context_id = get_context_id("Model", "Box", "MODEL_VIEW") old_box = ifcopenshell.util.element.get_representation(product, "Model", "Box", "MODEL_VIEW") if ( diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py index 17467eec09..7f3762c1d4 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py @@ -82,8 +82,16 @@ class BIM_PT_mesh(Panel): row.operator("bim.update_mesh_representation") row = layout.row() - op = row.operator("bim.update_mesh_representation", text="Convert Mesh to Rectangular Extrusion") - op.ifc_representation_type = "IfcExtrudedAreaSolid/IfcRectangleProfileDef" + op = row.operator("bim.update_mesh_representation", text="Update Mesh As Rectangle Extrusion") + op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcRectangleProfileDef" + + row = layout.row() + op = row.operator("bim.update_mesh_representation", text="Update Mesh As Circle Extrusion") + op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcCircleProfileDef" + + row = layout.row() + op = row.operator("bim.update_mesh_representation", text="Update Mesh As Arbitrary Extrusion") + op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef" row = layout.row() row.operator("bim.get_representation_ifc_parameters")