Allow to manually update boundary connection geometry (#1899)

* Allow to manually update boundary geometry

Following have been created in this purpose:
* usecase in ifcopenshell.api
* blenderbim operator
* button in blenderbim manually trigger it

* Assign kwargs to object instead of settings dict

* Add forgotten helper file
This commit is contained in:
Cyril Waechter
2021-11-30 03:29:22 +01:00
committed by GitHub
parent 47a25ca8b9
commit f991b053d3
5 changed files with 194 additions and 1 deletions
@@ -28,6 +28,7 @@ classes = (
operator.EnableEditingBoundary, operator.EnableEditingBoundary,
operator.DisableEditingBoundary, operator.DisableEditingBoundary,
operator.EditBoundaryAttributes, operator.EditBoundaryAttributes,
operator.UpdateBoundaryGeometry,
ui.BIM_PT_Boundary, ui.BIM_PT_Boundary,
ui.BIM_PT_SpaceBoundaries, ui.BIM_PT_SpaceBoundaries,
ui.BIM_PT_SceneBoundaries, ui.BIM_PT_SceneBoundaries,
@@ -18,12 +18,14 @@
import logging import logging
import bpy import bpy
import mathutils
import ifcopenshell.util.attribute import ifcopenshell.util.attribute
import ifcopenshell.api import ifcopenshell.api
import blenderbim.tool as tool import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.boundary.data import Data from ifcopenshell.api.boundary.data import Data
import blenderbim.bim.import_ifc as import_ifc import blenderbim.bim.import_ifc as import_ifc
from blenderbim.bim.module.geometry.helper import Helper
def get_boundaries_collection(blender_space): def get_boundaries_collection(blender_space):
@@ -263,3 +265,55 @@ class EditBoundaryAttributes(bpy.types.Operator):
setattr(boundary, ifc_attribute, entity) setattr(boundary, ifc_attribute, entity)
bpy.ops.bim.disable_editing_boundary() bpy.ops.bim.disable_editing_boundary()
return {"FINISHED"} 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"}
@@ -89,6 +89,8 @@ class BIM_PT_Boundary(Panel):
row.label(text="InnerBoundaries") row.label(text="InnerBoundaries")
row.label(text=f"[{i}]") row.label(text=f"[{i}]")
row.label(text=f"{inner_boundary.is_a()}/{inner_boundary.Name}") 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): def draw_relation_data(self, boundary, ifc_attribute: str):
row = self.layout.row(align=True) row = self.layout.row(align=True)
@@ -22,7 +22,7 @@ import mathutils
import ifcopenshell import ifcopenshell
import ifcopenshell.util.unit import ifcopenshell.util.unit
from math import pi from math import pi
from mathutils import Vector, Matrix from mathutils import Vector, Matrix, geometry
class Helper: class Helper:
@@ -215,6 +215,95 @@ class Helper:
return {"profile": outer_loop, "inner_curves": inner_loops, "extrusion": extrusion} 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 # An extrusion edge is an edge that shares a single vertex with a profile
# face and is not on the plane of the face. # face and is not on the plane of the face.
def detect_extrusion_edge(self, bm, profile_face): def detect_extrusion_edge(self, bm, profile_face):
@@ -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),
)
)