From 5921bcb63ddc2324a1b8224b5516567a78bc4de4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 27 Feb 2021 21:15:08 +1100 Subject: [PATCH] You can now automagically convert meshes into rectangular solid extrusions --- .../bim/module/geometry/add_representation.py | 16 ++ .../blenderbim/bim/module/geometry/helper.py | 210 ++++++++++++++++++ .../bim/module/geometry/operator.py | 155 ++++++------- .../blenderbim/bim/module/geometry/ui.py | 5 + 4 files changed, 309 insertions(+), 77 deletions(-) create mode 100644 src/ifcblenderexport/blenderbim/bim/module/geometry/helper.py diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py index 31c9113e61..0d8498e160 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/add_representation.py @@ -2,6 +2,7 @@ import bpy import bmesh import ifcopenshell.util.unit from mathutils import Vector +from blenderbim.bim.module.geometry.helper import Helper class Usecase: @@ -20,6 +21,7 @@ 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, } self.ifc_vertices = [] for key, value in settings.items(): @@ -124,6 +126,8 @@ 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() return self.create_mesh_representation() def create_curve3d_representation(self): @@ -215,6 +219,18 @@ class Usecase: results.append(self.file.createIfcPolyline(points)) return results + def create_rectangular_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) + 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 new file mode 100644 index 0000000000..b2b28619e8 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/helper.py @@ -0,0 +1,210 @@ +import bpy +import bmesh +import ifcopenshell +import ifcopenshell.util.unit +from math import pi +from mathutils import Vector, Matrix + + +class Helper: + def __init__(self, file): + self.file = file + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + + # We can detect a rectangular extrusion by picking any face, then find an + # edge that shares a single vertex only with that face to find the extrusion + # edge. A face with the normal facing down is prioritised. A limited + # dissolve ensure that faces are quads and not tris. + 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) + + bm.faces.ensure_lookup_table() + face = None + for face in bm.faces: + 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 + + 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 + bm = bmesh.new() + + # 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 + bm = bmesh.new() + + 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] + 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"] + ) + unit_direction = direction.normalized() + return self.file.createIfcExtrudedAreaSolid( + profile_def["curve"], + position, + self.file.createIfcDirection((unit_direction.x, unit_direction.y, unit_direction.z)), + 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) + curve = self.file.createIfcArbitraryClosedProfileDef("AREA", None, outer_curve) + return {"outer_curve_loop": outer_curve_loop, "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) + xdim = self.convert_si_to_unit( + (obj.data.vertices[outer_curve_loop[0]].co - obj.data.vertices[outer_curve_loop[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 + ) + curve = self.file.createIfcRectangleProfileDef("AREA", None, None, xdim, ydim) + return {"outer_curve_loop": outer_curve_loop, "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) + radius = self.convert_si_to_unit( + abs((obj.data.vertices[indices[0]].co - obj.data.vertices[indices[int(len(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} + + 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) + loop.pop(-1) + return loop + + def get_loop_from_edges(self, edges): + while edges: + currentEdge = edges.pop() + startVert = currentEdge.vertices[0] + endVert = currentEdge.vertices[1] + polyLine = [startVert, endVert] + ok = 1 + while ok: + ok = 0 + i = len(edges) + while i: + i -= 1 + ed = edges[i] + if ed.vertices[0] == endVert: + polyLine.append(ed.vertices[1]) + endVert = polyLine[-1] + ok = 1 + del edges[i] + elif ed.vertices[1] == endVert: + polyLine.append(ed.vertices[0]) + endVert = polyLine[-1] + ok = 1 + del edges[i] + elif ed.vertices[0] == startVert: + polyLine.insert(0, ed.vertices[1]) + startVert = polyLine[0] + ok = 1 + del edges[i] + elif ed.vertices[1] == startVert: + polyLine.insert(0, ed.vertices[0]) + startVert = polyLine[0] + ok = 1 + del edges[i] + return polyLine + + 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): + 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_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() + else: + x_axis = (obj.data.vertices[loop[1]].co - obj.data.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)) + matrix.normalize() + return { + "center": center, + "x_axis": x_axis, + "y_axis": y_axis, + "z_axis": z_axis, + "matrix": matrix.to_4x4() @ Matrix.Translation(-center), + } + + def convert_si_to_unit(self, co): + return co / self.unit_scale + + def create_polyline_from_loop(self, obj, loop, curve_ucs): + points = [] + for point in loop: + transformed_point = curve_ucs["matrix"] @ obj.data.vertices[point].co + points.append(self.create_cartesian_point(transformed_point.x, transformed_point.y)) + points.append(points[0]) + return self.file.createIfcPolyline(points) + + def create_cartesian_point(self, x, y, z=None): + x = self.convert_si_to_unit(x) + y = self.convert_si_to_unit(y) + if z is None: + return self.file.createIfcCartesianPoint((x, y)) + 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_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_3d(self, point, up, forward): + return self.file.createIfcAxis2Placement3D( + self.create_cartesian_point(point.x, point.y, point.z), + self.file.createIfcDirection((up.x, up.y, up.z)), + self.file.createIfcDirection((forward.x, forward.y, forward.z)), + ) diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py index cb76cceee8..4cfeffbb4b 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/operator.py @@ -19,11 +19,11 @@ from blenderbim.bim.module.void.data import Data as VoidData from mathutils import Vector -def get_box_context_id(): +def get_context_id(context_type, context_identifier, target_view): for context in ContextData.contexts.values(): - if context["ContextType"] == "Model": + if context["ContextType"] == context_type: for i, subcontext in context["HasSubContexts"].items(): - if subcontext["ContextIdentifier"] == "Box" and subcontext["TargetView"] == "MODEL_VIEW": + if subcontext["ContextIdentifier"] == context_identifier and subcontext["TargetView"] == target_view: return i @@ -120,7 +120,7 @@ class AddRepresentation(bpy.types.Operator): print("Failed to write shape representation") return {"FINISHED"} - box_context_id = get_box_context_id() + box_context_id = get_context_id("Model", "Box", "MODEL_VIEW") if ( box_context_id and context_of_items.ContextType == "Model" @@ -306,6 +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() def execute(self, context): if not ContextData.is_loaded: @@ -315,81 +316,81 @@ class UpdateMeshRepresentation(bpy.types.Operator): self.file = IfcStore.get_file() for obj in objs: - bpy.ops.bim.edit_object_placement(obj=obj.name) - - product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - - if product.is_a("IfcGridAxis"): - create_axis_curve.Usecase(self.file, {"AxisCurve": obj, "grid_axis": product}).execute() - continue - - old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id) - context_of_items = old_representation.ContextOfItems - - gprop = context.scene.BIMGeoreferenceProperties - coordinate_offset = None - if gprop.has_blender_offset and gprop.blender_offset_type == "CARTESIAN_POINT": - coordinate_offset = Vector( - ( - float(gprop.blender_eastings), - float(gprop.blender_northings), - float(gprop.blender_orthogonal_height), - ) - ) - - representation_data = { - "context": context_of_items, - "blender_object": obj, - "geometry": obj.data, - "coordinate_offset": coordinate_offset, - "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, - } - - new_representation = add_representation.Usecase(self.file, representation_data).execute() - - if not new_representation: - print("Failed to write shape representation") - return {"FINISHED"} - - box_context_id = get_box_context_id() - old_box = ifcopenshell.util.element.get_representation(product, "Model", "Box", "MODEL_VIEW") - if ( - box_context_id - and old_box - and context_of_items.ContextType == "Model" - and context_of_items.ContextIdentifier - and context_of_items.ContextIdentifier == "Body" - ): - representation_data["context"] = self.file.by_id(box_context_id) - new_box = add_representation.Usecase(self.file, representation_data).execute() - for inverse in self.file.get_inverse(old_box): - ifcopenshell.util.element.replace_attribute(inverse, old_box, new_box) - - assign_styles.Usecase( - self.file, - { - "shape_representation": new_representation, - "styles": [ - self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id) - for s in obj.material_slots - if s.material - ], - "should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, - }, - ).execute() - - # TODO: move this into a replace_representation usecase or something - for inverse in self.file.get_inverse(old_representation): - ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation) - - obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id()) - obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}" - bpy.ops.bim.remove_representation(representation_id=old_representation.id()) - Data.load(obj.BIMObjectProperties.ifc_definition_id) + self.update_obj_mesh_representation(context, obj) return {"FINISHED"} + def update_obj_mesh_representation(self, context, obj): + product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + + if product.is_a("IfcGridAxis"): + create_axis_curve.Usecase(self.file, {"AxisCurve": obj, "grid_axis": product}).execute() + return + + bpy.ops.bim.edit_object_placement(obj=obj.name) + + old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id) + context_of_items = old_representation.ContextOfItems + + gprop = context.scene.BIMGeoreferenceProperties + coordinate_offset = None + if gprop.has_blender_offset and gprop.blender_offset_type == "CARTESIAN_POINT": + coordinate_offset = Vector( + ( + float(gprop.blender_eastings), + float(gprop.blender_northings), + float(gprop.blender_orthogonal_height), + ) + ) + + representation_data = { + "context": context_of_items, + "blender_object": obj, + "geometry": obj.data, + "coordinate_offset": coordinate_offset, + "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" + } + + new_representation = add_representation.Usecase(self.file, representation_data).execute() + + box_context_id = get_context_id("Model", "Box", "MODEL_VIEW") + old_box = ifcopenshell.util.element.get_representation(product, "Model", "Box", "MODEL_VIEW") + if ( + box_context_id + and old_box + and context_of_items.ContextType == "Model" + and context_of_items.ContextIdentifier + and context_of_items.ContextIdentifier == "Body" + ): + representation_data["context"] = self.file.by_id(box_context_id) + new_box = add_representation.Usecase(self.file, representation_data).execute() + for inverse in self.file.get_inverse(old_box): + ifcopenshell.util.element.replace_attribute(inverse, old_box, new_box) + + assign_styles.Usecase( + self.file, + { + "shape_representation": new_representation, + "styles": [ + self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id) + for s in obj.material_slots + if s.material + ], + "should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, + }, + ).execute() + + # TODO: move this into a replace_representation usecase or something + for inverse in self.file.get_inverse(old_representation): + ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation) + + obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id()) + obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}" + bpy.ops.bim.remove_representation(representation_id=old_representation.id()) + Data.load(obj.BIMObjectProperties.ifc_definition_id) + class UpdateParametricRepresentation(bpy.types.Operator): bl_idname = "bim.update_parametric_representation" diff --git a/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py b/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py index da3804fcc6..17467eec09 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/module/geometry/ui.py @@ -80,6 +80,11 @@ class BIM_PT_mesh(Panel): row = layout.row() 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" + row = layout.row() row.operator("bim.get_representation_ifc_parameters") for index, ifc_parameter in enumerate(props.ifc_parameters):