mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 06:58:56 +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 multiprocessing
|
||||||
import ifcopenshell.api
|
import ifcopenshell.api
|
||||||
import ifcopenshell.api.boundary
|
import ifcopenshell.api.boundary
|
||||||
|
import ifcopenshell.api.root
|
||||||
import ifcopenshell.geom
|
import ifcopenshell.geom
|
||||||
import ifcopenshell.util.unit
|
import ifcopenshell.util.unit
|
||||||
import ifcopenshell.util.shape
|
import ifcopenshell.util.shape
|
||||||
@@ -591,47 +592,70 @@ class DecorateBoundaries(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.add_boundary"
|
bl_idname = "bim.add_boundary"
|
||||||
bl_label = "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"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
relating_space = None
|
parent_boundaries: list[ifcopenshell.entity_instance] = []
|
||||||
related_building_element = None
|
|
||||||
relating_space_obj = None
|
|
||||||
related_building_element_obj = None
|
|
||||||
parent_boundaries = []
|
|
||||||
|
|
||||||
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):
|
if not any(element.is_a("IfcSpace") for element in ifc_objects):
|
||||||
self.report({"INFO"}, "No IfcSpace elements selected.")
|
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
|
||||||
return {"FINISHED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
if len(objs) == 2:
|
if len(objs) == 2:
|
||||||
# The user may select two objects, a space and its related building element
|
# Validate input.
|
||||||
for obj in objs:
|
space = next((e for e in ifc_objects.keys() if e.is_a("IfcSpace")), None)
|
||||||
element = tool.Ifc.get_entity(obj)
|
if not space:
|
||||||
if not element:
|
self.report({"ERROR"}, "One of the selected objects must be an IfcSpace.")
|
||||||
continue
|
return {"CANCELLED"}
|
||||||
if element.is_a("IfcSpace"):
|
element = next((e for e in ifc_objects.keys() if e.is_a("IfcElement")), None)
|
||||||
relating_space = element
|
if not element:
|
||||||
relating_space_obj = obj
|
self.report({"ERROR"}, "One of the selected objects must be an IfcElement.")
|
||||||
else:
|
return {"CANCELLED"}
|
||||||
related_building_element = element
|
|
||||||
related_building_element_obj = obj
|
|
||||||
parent_boundary = self.create_element_boundary(
|
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:
|
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.
|
# New prototype, old code not yet removed. Still testing.
|
||||||
space = tool.Ifc.get_entity(objs[0])
|
res = self.auto_generate_boundaries(space, ifc_objects[space])
|
||||||
if space.is_a("IfcSpace"):
|
if isinstance(res, str):
|
||||||
self.auto_generate_boundaries(space, objs[0])
|
self.report({"ERROR"}, res)
|
||||||
elif len(objs) == 1:
|
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
|
# 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
|
# 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):
|
def msg(self, context):
|
||||||
self.layout.label(text="Please set an active container to detect space boundaries from.")
|
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 = tool.Ifc.get_object(parent_boundary)
|
||||||
obj.select_set(True)
|
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()
|
props = tool.Model.get_model_props()
|
||||||
|
boundaries: list[ifcopenshell.entity_instance] = []
|
||||||
|
assert isinstance(space_obj.data, bpy.types.Mesh)
|
||||||
|
|
||||||
# Identify all potential building elements
|
# Identify all potential building elements
|
||||||
# TODO: don't select everything, use AABB culling in Blender
|
# TODO: don't select everything, use AABB culling in Blender
|
||||||
building_elements = (
|
building_elements = (
|
||||||
@@ -689,7 +722,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
shapes = {}
|
shapes = {}
|
||||||
settings = ifcopenshell.geom.settings()
|
settings = ifcopenshell.geom.settings()
|
||||||
settings.set("disable-opening-subtractions", True)
|
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():
|
if iterator.initialize():
|
||||||
while True:
|
while True:
|
||||||
tree.add_element(iterator.get_native())
|
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]
|
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
|
||||||
|
|
||||||
if not building_elements:
|
if not building_elements:
|
||||||
return
|
return "No building elements found to create boundaries."
|
||||||
|
|
||||||
# Create a dissolved bmesh for the space
|
# Create a dissolved bmesh for the space
|
||||||
space_bm = bmesh.new()
|
space_bm = bmesh.new()
|
||||||
space_bm.from_mesh(space_obj.data)
|
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
|
# Create dissolved bmeshes for all boundary elements
|
||||||
building_element_bms = {}
|
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".
|
# we cheat by using the exterior boundary to mean "gross".
|
||||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
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"):
|
if building_element.is_a("IfcVirtualElement"):
|
||||||
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||||
else:
|
else:
|
||||||
@@ -817,6 +850,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
exterior_boundary_polygon, space_face_matrix
|
exterior_boundary_polygon, space_face_matrix
|
||||||
)
|
)
|
||||||
self.set_boundary_name(parent_boundary)
|
self.set_boundary_name(parent_boundary)
|
||||||
|
boundaries.append(parent_boundary)
|
||||||
|
|
||||||
for rel in getattr(building_element, "HasOpenings", []):
|
for rel in getattr(building_element, "HasOpenings", []):
|
||||||
opening = rel.RelatedOpeningElement
|
opening = rel.RelatedOpeningElement
|
||||||
@@ -862,7 +896,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
|
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
|
||||||
continue
|
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.RelatingSpace = space
|
||||||
boundary.RelatedBuildingElement = filling or opening
|
boundary.RelatedBuildingElement = filling or opening
|
||||||
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
|
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":
|
if boundary.is_a() != "IfcRelSpaceBoundary":
|
||||||
boundary.ParentBoundary = parent_boundary
|
boundary.ParentBoundary = parent_boundary
|
||||||
self.set_boundary_name(boundary)
|
self.set_boundary_name(boundary)
|
||||||
|
boundaries.append(boundary)
|
||||||
|
|
||||||
|
return boundaries
|
||||||
|
|
||||||
def create_element_boundary(
|
def create_element_boundary(
|
||||||
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
|
self,
|
||||||
):
|
context: bpy.types.Context,
|
||||||
if not relating_space or not related_building_element:
|
space: ifcopenshell.entity_instance,
|
||||||
return
|
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()
|
props = tool.Model.get_model_props()
|
||||||
# Find which face on space should be bounded to related building element
|
# 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
|
# TODO: Handle round wall where multiple faces need to be bound to the same related building element
|
||||||
bm = bmesh.new()
|
space_bm = bmesh.new()
|
||||||
bm.from_mesh(relating_space_obj.data)
|
assert isinstance(space_obj.data, bpy.types.Mesh)
|
||||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
|
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_distance = inf
|
||||||
target_face = None
|
target_face = None
|
||||||
for face in bm.faces:
|
to_building_obj_space = building_element_obj.matrix_world.inverted()
|
||||||
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
|
for face in space_bm.faces:
|
||||||
raycast = related_building_element_obj.closest_point_on_mesh(
|
centroid = space_obj.matrix_world @ face.calc_center_median()
|
||||||
related_building_element_obj.matrix_world.inverted() @ centroid, distance=1
|
raycast = building_element_obj.closest_point_on_mesh(to_building_obj_space @ centroid, distance=1)
|
||||||
)
|
|
||||||
if raycast[0]:
|
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:
|
if distance < target_distance:
|
||||||
target_face = face
|
target_face = face
|
||||||
target_distance = distance
|
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])
|
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_polygon = self.get_flattened_polygon(building_element, space_obj, target_face_matrix_i)
|
||||||
related_building_element, relating_space_obj, target_face_matrix_i
|
|
||||||
)
|
|
||||||
|
|
||||||
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
|
gross_boundary_polygon = target_face_polygon.intersection(related_building_element_polygon)
|
||||||
|
|
||||||
@@ -931,7 +973,8 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
):
|
):
|
||||||
return
|
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"
|
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||||
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
|
# Set to EXTERNAL by default and turn later to internal if there is a corresponding boundary relating to an
|
||||||
# internal space
|
# internal space
|
||||||
@@ -944,14 +987,14 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
# space.
|
# space.
|
||||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
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
|
opening = rel.RelatedOpeningElement
|
||||||
if not opening.HasFillings:
|
if not opening.HasFillings:
|
||||||
continue
|
continue
|
||||||
# TODO: HasFilling is a zero to many relationship. How to handle many ?
|
# TODO: HasFilling is a zero to many relationship. How to handle many ?
|
||||||
filling = opening.HasFillings[0].RelatedBuildingElement
|
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
|
# 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
|
# 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)
|
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 = tool.Ifc.run("root.create_entity", ifc_class=props.boundary_class)
|
||||||
boundary.RelatingSpace = relating_space
|
boundary.RelatingSpace = space
|
||||||
boundary.RelatedBuildingElement = filling
|
boundary.RelatedBuildingElement = filling
|
||||||
boundary.ConnectionGeometry = connection_geometry
|
boundary.ConnectionGeometry = connection_geometry
|
||||||
if related_building_element.is_a("IfcVirtualElement"):
|
if building_element.is_a("IfcVirtualElement"):
|
||||||
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||||
else:
|
else:
|
||||||
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||||
@@ -982,8 +1025,8 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
connection_geometry = self.create_connection_geometry_from_polygon(
|
connection_geometry = self.create_connection_geometry_from_polygon(
|
||||||
exterior_boundary_polygon, target_face_matrix
|
exterior_boundary_polygon, target_face_matrix
|
||||||
)
|
)
|
||||||
parent_boundary.RelatingSpace = relating_space
|
parent_boundary.RelatingSpace = space
|
||||||
parent_boundary.RelatedBuildingElement = related_building_element
|
parent_boundary.RelatedBuildingElement = building_element
|
||||||
parent_boundary.ConnectionGeometry = connection_geometry
|
parent_boundary.ConnectionGeometry = connection_geometry
|
||||||
self.set_boundary_name(parent_boundary)
|
self.set_boundary_name(parent_boundary)
|
||||||
return parent_boundary
|
return parent_boundary
|
||||||
|
|||||||
Reference in New Issue
Block a user