mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
bim.add_boundary - add description/error reports
Example description - https://i.imgur.com/jn3y2KU.png fyi @steverugi
This commit is contained in:
@@ -26,6 +26,7 @@ import numpy as np
|
||||
import multiprocessing
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.boundary
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.shape
|
||||
@@ -591,47 +592,70 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_boundary"
|
||||
bl_label = "Add Boundary"
|
||||
bl_description = (
|
||||
"There are two options for this operator:\n"
|
||||
"- just 1 IfcSpace is selected - generate a boundary based on "
|
||||
"potential nearby (0.1m) boundary elements.\n"
|
||||
"- 1 IfcSpace and 1 IfcElement are selected - generate a boundary "
|
||||
"for IfcSpace and use selected IfcElement as boundary's building element."
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
relating_space = None
|
||||
related_building_element = None
|
||||
relating_space_obj = None
|
||||
related_building_element_obj = None
|
||||
parent_boundaries = []
|
||||
parent_boundaries: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
objs = context.selected_objects
|
||||
objs = tool.Blender.get_selected_objects()
|
||||
ifc_objects = {element: obj for obj in objs if (element := tool.Ifc.get_entity(obj))}
|
||||
if len(ifc_objects) not in (1, 2):
|
||||
self.report({"ERROR"}, "Please select one or two IFC elements.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not any((element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace") for obj in objs):
|
||||
self.report({"INFO"}, "No IfcSpace elements selected.")
|
||||
return {"FINISHED"}
|
||||
if not any(element.is_a("IfcSpace") for element in ifc_objects):
|
||||
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if len(objs) == 2:
|
||||
# The user may select two objects, a space and its related building element
|
||||
for obj in objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
if element.is_a("IfcSpace"):
|
||||
relating_space = element
|
||||
relating_space_obj = obj
|
||||
else:
|
||||
related_building_element = element
|
||||
related_building_element_obj = obj
|
||||
# Validate input.
|
||||
space = next((e for e in ifc_objects.keys() if e.is_a("IfcSpace")), None)
|
||||
if not space:
|
||||
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
|
||||
return {"CANCELLED"}
|
||||
element = next((e for e in ifc_objects.keys() if e.is_a("IfcElement")), None)
|
||||
if not element:
|
||||
self.report({"ERROR"}, "One of the selected objects must be an IfcElement.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
parent_boundary = self.create_element_boundary(
|
||||
context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
|
||||
context, space, ifc_objects[space], element, ifc_objects[element]
|
||||
)
|
||||
if parent_boundary:
|
||||
parent_boundaries.append(parent_boundary)
|
||||
|
||||
if not parent_boundary:
|
||||
self.report({"ERROR"}, "Failed to create a boundary.")
|
||||
return {"FINISHED"}
|
||||
parent_boundaries.append(parent_boundary)
|
||||
|
||||
elif len(objs) == 1:
|
||||
space = next(iter(ifc_objects))
|
||||
if not space.is_a("IfcSpace"):
|
||||
self.report({"ERROR"}, "1 element selected but it's not an IfcSpace - please, select IfcSpace..")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# New prototype, old code not yet removed. Still testing.
|
||||
space = tool.Ifc.get_entity(objs[0])
|
||||
if space.is_a("IfcSpace"):
|
||||
self.auto_generate_boundaries(space, objs[0])
|
||||
elif len(objs) == 1:
|
||||
res = self.auto_generate_boundaries(space, ifc_objects[space])
|
||||
if isinstance(res, str):
|
||||
self.report({"ERROR"}, res)
|
||||
return {"FINISHED"}
|
||||
parent_boundaries.extend(res)
|
||||
|
||||
elif len(objs) == 1 and False:
|
||||
# Optionally the user may select just the space, and the building element shall be auto-detected
|
||||
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
|
||||
|
||||
relating_space = None
|
||||
related_building_element = None
|
||||
relating_space_obj = None
|
||||
related_building_element_obj = None
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="Please set an active container to detect space boundaries from.")
|
||||
|
||||
@@ -668,8 +692,17 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
obj = tool.Ifc.get_object(parent_boundary)
|
||||
obj.select_set(True)
|
||||
|
||||
def auto_generate_boundaries(self, space, space_obj):
|
||||
def auto_generate_boundaries(
|
||||
self, space: ifcopenshell.entity_instance, space_obj: bpy.types.Object
|
||||
) -> Union[str, list[ifcopenshell.entity_instance]]:
|
||||
"""
|
||||
:return: list of created boundaries or a string with error description.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Model.get_model_props()
|
||||
boundaries: list[ifcopenshell.entity_instance] = []
|
||||
assert isinstance(space_obj.data, bpy.types.Mesh)
|
||||
|
||||
# Identify all potential building elements
|
||||
# TODO: don't select everything, use AABB culling in Blender
|
||||
building_elements = (
|
||||
@@ -689,7 +722,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
shapes = {}
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("disable-opening-subtractions", True)
|
||||
iterator = ifcopenshell.geom.iterator(settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=include)
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=include)
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
@@ -702,12 +735,12 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
|
||||
|
||||
if not building_elements:
|
||||
return
|
||||
return "No building elements found to create boundaries."
|
||||
|
||||
# Create a dissolved bmesh for the space
|
||||
space_bm = bmesh.new()
|
||||
space_bm.from_mesh(space_obj.data)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts, edges=space_bm.edges)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
|
||||
|
||||
# Create dissolved bmeshes for all boundary elements
|
||||
building_element_bms = {}
|
||||
@@ -785,7 +818,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# we cheat by using the exterior boundary to mean "gross".
|
||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
||||
|
||||
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
if building_element.is_a("IfcVirtualElement"):
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||
else:
|
||||
@@ -817,6 +850,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
exterior_boundary_polygon, space_face_matrix
|
||||
)
|
||||
self.set_boundary_name(parent_boundary)
|
||||
boundaries.append(parent_boundary)
|
||||
|
||||
for rel in getattr(building_element, "HasOpenings", []):
|
||||
opening = rel.RelatedOpeningElement
|
||||
@@ -862,7 +896,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
|
||||
continue
|
||||
|
||||
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||
boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
boundary.RelatingSpace = space
|
||||
boundary.RelatedBuildingElement = filling or opening
|
||||
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
|
||||
@@ -876,29 +910,39 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if boundary.is_a() != "IfcRelSpaceBoundary":
|
||||
boundary.ParentBoundary = parent_boundary
|
||||
self.set_boundary_name(boundary)
|
||||
boundaries.append(boundary)
|
||||
|
||||
return boundaries
|
||||
|
||||
def create_element_boundary(
|
||||
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
|
||||
):
|
||||
if not relating_space or not related_building_element:
|
||||
return
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
space: ifcopenshell.entity_instance,
|
||||
space_obj: bpy.types.Object,
|
||||
building_element: ifcopenshell.entity_instance,
|
||||
building_element_obj: bpy.types.Object,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Create element boundary for IfcSpace and IfcElement as building element.
|
||||
|
||||
:return: IfcRelSpaceBoundary or ``None`` if couldn't find a face on ``space_obj``
|
||||
that would be close enough to ``related_building_element_obj``.
|
||||
"""
|
||||
props = tool.Model.get_model_props()
|
||||
# Find which face on space should be bounded to related building element
|
||||
# TODO: Handle round wall where multiple faces need to be bound to the same related building element
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(relating_space_obj.data)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
|
||||
space_bm = bmesh.new()
|
||||
assert isinstance(space_obj.data, bpy.types.Mesh)
|
||||
space_bm.from_mesh(space_obj.data)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
|
||||
|
||||
target_distance = inf
|
||||
target_face = None
|
||||
for face in bm.faces:
|
||||
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
|
||||
raycast = related_building_element_obj.closest_point_on_mesh(
|
||||
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
|
||||
)
|
||||
to_building_obj_space = building_element_obj.matrix_world.inverted()
|
||||
for face in space_bm.faces:
|
||||
centroid = space_obj.matrix_world @ face.calc_center_median()
|
||||
raycast = building_element_obj.closest_point_on_mesh(to_building_obj_space @ centroid, distance=1)
|
||||
if raycast[0]:
|
||||
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
|
||||
distance = (building_element_obj.matrix_world @ raycast[1] - centroid).length
|
||||
if distance < target_distance:
|
||||
target_face = face
|
||||
target_distance = distance
|
||||
@@ -913,9 +957,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
target_face_polygon = shapely.Polygon([tuple((target_face_matrix_i @ v).xy) for v in target_face_verts])
|
||||
|
||||
related_building_element_polygon = self.get_flattened_polygon(
|
||||
related_building_element, relating_space_obj, target_face_matrix_i
|
||||
)
|
||||
related_building_element_polygon = self.get_flattened_polygon(building_element, space_obj, target_face_matrix_i)
|
||||
|
||||
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
|
||||
|
||||
@@ -931,7 +973,8 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
):
|
||||
return
|
||||
|
||||
parent_boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||
ifc_file = tool.Ifc.get()
|
||||
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
|
||||
# internal space
|
||||
@@ -944,14 +987,14 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# space.
|
||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
||||
|
||||
for rel in getattr(related_building_element, "HasOpenings", []):
|
||||
for rel in getattr(building_element, "HasOpenings", []):
|
||||
opening = rel.RelatedOpeningElement
|
||||
if not opening.HasFillings:
|
||||
continue
|
||||
# TODO: HasFilling is a zero to many relationship. How to handle many ?
|
||||
filling = opening.HasFillings[0].RelatedBuildingElement
|
||||
|
||||
opening_polygon = self.get_flattened_polygon(opening, relating_space_obj, target_face_matrix_i)
|
||||
opening_polygon = self.get_flattened_polygon(opening, space_obj, target_face_matrix_i)
|
||||
|
||||
# An inner boundary is supposed to overlap its parent boundary according to IFC4 documentation
|
||||
# so we extend our exterior boundary with all opening which has a filling
|
||||
@@ -967,10 +1010,10 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(opening_polygon, target_face_matrix)
|
||||
boundary = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||
boundary.RelatingSpace = relating_space
|
||||
boundary.RelatingSpace = space
|
||||
boundary.RelatedBuildingElement = filling
|
||||
boundary.ConnectionGeometry = connection_geometry
|
||||
if related_building_element.is_a("IfcVirtualElement"):
|
||||
if building_element.is_a("IfcVirtualElement"):
|
||||
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||
else:
|
||||
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
@@ -982,8 +1025,8 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
connection_geometry = self.create_connection_geometry_from_polygon(
|
||||
exterior_boundary_polygon, target_face_matrix
|
||||
)
|
||||
parent_boundary.RelatingSpace = relating_space
|
||||
parent_boundary.RelatedBuildingElement = related_building_element
|
||||
parent_boundary.RelatingSpace = space
|
||||
parent_boundary.RelatedBuildingElement = building_element
|
||||
parent_boundary.ConnectionGeometry = connection_geometry
|
||||
self.set_boundary_name(parent_boundary)
|
||||
return parent_boundary
|
||||
|
||||
Reference in New Issue
Block a user