mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user