diff --git a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py index 22944a5bfa..5af93cd2f4 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py @@ -28,6 +28,7 @@ classes = ( operator.EnableEditingBoundary, operator.DisableEditingBoundary, operator.EditBoundaryAttributes, + operator.UpdateBoundaryGeometry, ui.BIM_PT_Boundary, ui.BIM_PT_SpaceBoundaries, ui.BIM_PT_SceneBoundaries, diff --git a/src/blenderbim/blenderbim/bim/module/boundary/operator.py b/src/blenderbim/blenderbim/bim/module/boundary/operator.py index b9320937cb..f40bb5b130 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/operator.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/operator.py @@ -18,12 +18,14 @@ import logging import bpy +import mathutils import ifcopenshell.util.attribute import ifcopenshell.api import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.boundary.data import Data import blenderbim.bim.import_ifc as import_ifc +from blenderbim.bim.module.geometry.helper import Helper def get_boundaries_collection(blender_space): @@ -263,3 +265,55 @@ class EditBoundaryAttributes(bpy.types.Operator): setattr(boundary, ifc_attribute, entity) bpy.ops.bim.disable_editing_boundary() return {"FINISHED"} + + +def polyline_from_indexes(mesh, indexes): + return tuple(mesh.vertices[i].co for i in indexes) + + +def polyline_to_2d(polyline, placement_matrix): + matrix_inv = placement_matrix.inverted() + return tuple((matrix_inv @ v).to_2d() for v in polyline) + + +class UpdateBoundaryGeometry(bpy.types.Operator): + bl_idname = "bim.update_boundary_geometry" + bl_label = "Update boundary geometry" + bl_description = """ + Update boundary connection geometry from mesh. + Mesh must lie on a single plane. It should look like a face or a face with holes. + """ + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + ifc_file = tool.Ifc.get() + helper = Helper(ifc_file) + mesh = context.active_object.data + curves = helper.auto_detect_curve_bounded_plane(mesh) + outer_boundary = polyline_from_indexes(mesh, curves["outer_curve"]) + inner_boundaries = tuple(polyline_from_indexes(mesh, boundary) for boundary in curves["inner_curves"]) + + # Create placement matrix + location = outer_boundary[0] + i = (outer_boundary[1] - outer_boundary[0]).normalized() + k = mesh.polygons[0].normal + j = k.cross(i) + matrix = mathutils.Matrix() + matrix[0].xyz = i + matrix[1].xyz = j + matrix[2].xyz = k + matrix.translation = location + + settings = { + "rel_space_boundary": tool.Ifc.get_entity(context.active_object), + "outer_boundary": polyline_to_2d(outer_boundary, matrix), + "inner_boundaries": tuple(polyline_to_2d(boundary, matrix) for boundary in inner_boundaries), + "location": location, + "axis": k, + "ref_direction": i, + } + ifcopenshell.api.run("boundary.assign_connection_geometry", ifc_file, **settings) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/boundary/ui.py b/src/blenderbim/blenderbim/bim/module/boundary/ui.py index 56d4444ac2..6325f0f1d5 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/ui.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/ui.py @@ -89,6 +89,8 @@ class BIM_PT_Boundary(Panel): row.label(text="InnerBoundaries") row.label(text=f"[{i}]") row.label(text=f"{inner_boundary.is_a()}/{inner_boundary.Name}") + row = self.layout.row() + row.operator("bim.update_boundary_geometry") def draw_relation_data(self, boundary, ifc_attribute: str): row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/helper.py b/src/blenderbim/blenderbim/bim/module/geometry/helper.py index 1fd3ab338c..d6c7ddd8ad 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/helper.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/helper.py @@ -22,7 +22,7 @@ import mathutils import ifcopenshell import ifcopenshell.util.unit from math import pi -from mathutils import Vector, Matrix +from mathutils import Vector, Matrix, geometry class Helper: @@ -215,6 +215,95 @@ class Helper: return {"profile": outer_loop, "inner_curves": inner_loops, "extrusion": extrusion} + # An arbitrary face with voids is similar to a profile with voids for extrusion. + # Before we begin we check that all faces are coplanar instead of finding suitable face. + # Then we process the same way. + # Only 2 parameters are returned as there is no extrusion. + def auto_detect_curve_bounded_plane(self, mesh, tolerance=0.001): + 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() + faces = bm.faces + normal = faces[0].normal + point = faces[0].verts[0].co + for face in faces: + pt_f = face.verts[0].co + if ( + normal.dot(face.normal) < 1 - tolerance + or abs(geometry.distance_point_to_plane(pt_f, point, normal)) > tolerance + ): + raise ValueError("All faces must be coplanar and have same orientation") + + loop_edges = set() + for face in faces: + potential_edges = set(face.edges) + for face2 in 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) + + bm.to_mesh(mesh) + mesh.update() + bm.free() + + return {"outer_curve": outer_loop, "inner_curves": inner_loops} + # An extrusion edge is an edge that shares a single vertex with a profile # face and is not on the plane of the face. def detect_extrusion_edge(self, bm, profile_face): diff --git a/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py new file mode 100644 index 0000000000..fc51b18480 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py @@ -0,0 +1,47 @@ +import ifcopenshell.util.unit + + +class Usecase: + def __init__(self, file, **kwargs): + """location, axis and ref_direction defines the plane""" + self.file = file + self.rel_space_boundary = None + self.outer_boundary = None + self.inner_boundaries = () + self.location = None + self.axis = None + self.ref_direction = None + self.unit_scale = None + self.ifc_vertices = [] + for key, value in kwargs.items(): + setattr(self, key, value) + + def execute(self): + if self.unit_scale is None: + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + outer_boundary = self.create_polyline(self.outer_boundary) + inner_boundaries = tuple(self.create_polyline(boundary) for boundary in self.inner_boundaries) + plane = self.create_plane(self.location, self.axis, self.ref_direction) + curve_bounded_plane = self.file.createIfcCurveBoundedPlane(plane, outer_boundary, inner_boundaries) + connection_geometry = self.file.createIfcConnectionSurfaceGeometry(curve_bounded_plane) + self.rel_space_boundary.ConnectionGeometry = connection_geometry + + def create_point(self, point): + return self.file.createIfcCartesianPoint(point / self.unit_scale) + + def close_polyline(self, points): + return points + (points[0],) + + def create_polyline(self, points): + if points[0] == points[-1]: + points = points[0 : len(points) - 1] + return self.file.createIfcPolyline(self.close_polyline(tuple(self.create_point(point) for point in points))) + + def create_plane(self, location, axis, ref_direction): + return self.file.createIfcPlane( + self.file.createIfcAxis2Placement3D( + self.create_point(location), + self.file.createIfcDirection(axis), + self.file.createIfcDirection(ref_direction), + ) + )