Files
IfcOpenShell/src/bonsai/bonsai/bim/module/model/opening.py
T
2025-05-13 17:23:46 +05:00

1003 lines
43 KiB
Python

# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import gpu
import json
import bmesh
import shapely
import logging
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.geometry
import ifcopenshell.api.feature
import ifcopenshell.geom
import ifcopenshell.util.shape
import ifcopenshell.util.element
import ifcopenshell.util.shape_builder
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.core.geometry
import bonsai.bim.import_ifc as import_ifc
from collections import defaultdict
from math import pi, radians
from mathutils import Vector, Matrix
from bpy.types import Operator
from bpy.types import SpaceView3D
from bpy.props import FloatProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from gpu_extras.batch import batch_for_shader
from typing import Union, Optional, Any, cast, Sequence
class FilledOpeningGenerator:
def generate(
self,
filling_obj: bpy.types.Object,
voided_obj: bpy.types.Object,
target: Optional[Vector] = None,
) -> Union[None, str]:
"""
:param target: Target opening position. If ommited, cursor position is used.
:return: None if there was no errors, otherwise returns a string with error message.
"""
props = tool.Model.get_model_props()
opening_thickness_si = 0.0
filling = tool.Ifc.get_entity(filling_obj)
element = tool.Ifc.get_entity(voided_obj)
assert filling and element
if filling.FillsVoids:
ifcopenshell.api.run(
"feature.remove_feature", tool.Ifc.get(), feature=filling.FillsVoids[0].RelatingOpeningElement
)
if target is None:
should_set_z_level = True
target = bpy.context.scene.cursor.location.copy()
else:
should_set_z_level = False
# Sometimes, the voided_obj may be an aggregate, which won't have any representation.
if voided_obj.data:
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.01)
if not raycast[0]:
target = filling_obj.matrix_world.translation.copy()
raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.5)
if not raycast[0]:
return "TARGET is too far away from the voided object's mesh."
# In this prototype, we assume openings are only added to axis-based elements
layers = tool.Model.get_material_layer_parameters(element)
if layers["layer_set_direction"] == "AXIS2":
opening_thickness_si = layers["thickness"] * 2
axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"]
new_matrix = voided_obj.matrix_world.copy()
point_on_axis = tool.Cad.point_on_edge(target, axis)
new_matrix.translation.x = point_on_axis.x
new_matrix.translation.y = point_on_axis.y
if should_set_z_level:
if filling.is_a("IfcDoor"):
new_matrix.translation.z = voided_obj.matrix_world.translation.z + props.rl1
else:
new_matrix.translation.z = voided_obj.matrix_world.translation.z + props.rl2
else:
new_matrix.translation.z = filling_obj.matrix_world.copy().translation.z
elif layers["layer_set_direction"] == "AXIS3":
new_matrix = voided_obj.matrix_world.copy()
local_position_on_voided_obj = raycast[1]
# Equivalent to "side Z" for a wall axis, so that stuff like skylights appear on the top.
local_position_on_voided_obj.z = layers["offset"] + layers["thickness"]
new_matrix.translation.xyz = voided_obj.matrix_world @ local_position_on_voided_obj
rotation_matrix = Matrix.Rotation(radians(-90), 4, "X")
new_matrix @= rotation_matrix
else:
assert False, f"Unexpected layer set direction: {layers['layer_set_direction']}"
filling_obj.matrix_world = new_matrix
bpy.context.view_layer.update()
if tool.Ifc.is_moved(voided_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=voided_obj)
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
opening = ifcopenshell.api.run(
"root.create_entity",
tool.Ifc.get(),
ifc_class="IfcOpeningElement",
predefined_type="OPENING",
name="Opening",
)
ifcopenshell.api.run(
"geometry.edit_object_placement",
tool.Ifc.get(),
product=opening,
matrix=np.array(filling_obj.matrix_world),
is_si=True,
)
if existing_opening_occurrence:
representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
assert representation
representation = ifcopenshell.util.representation.resolve_representation(representation)
else:
representation = self.generate_opening_from_filling(
filling, filling_obj, opening_thickness_si=opening_thickness_si
)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
ifcopenshell.api.run("feature.add_feature", tool.Ifc.get(), feature=opening, element=element)
ifcopenshell.api.run("feature.add_filling", tool.Ifc.get(), opening=opening, element=filling)
voided_objs = [voided_obj]
# Openings affect all subelements of an aggregate
for subelement in tool.Aggregate.get_parts_recursively(element):
subobj = tool.Ifc.get_object(subelement)
if subobj:
voided_objs.append(subobj)
for voided_obj in voided_objs:
if voided_obj.data:
voided_element = tool.Ifc.get_entity(voided_obj)
assert voided_element
context = tool.Geometry.get_active_representation_context(voided_obj)
representation = tool.Geometry.get_representation_by_context(voided_element, context)
assert representation
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
def regenerate_from_type(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
relating_type = settings["relating_type"]
for related_object in settings["related_objects"]:
self._regenerate_from_type(related_object)
def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None:
filling = related_object
if not getattr(filling, "FillsVoids", None):
return
opening = filling.FillsVoids[0].RelatingOpeningElement
voided_element = opening.VoidsElements[0].RelatingBuildingElement
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
ifcopenshell.api.run(
"geometry.unassign_representation", tool.Ifc.get(), product=opening, representation=opening_rep
)
ifcopenshell.api.run("geometry.remove_representation", tool.Ifc.get(), representation=opening_rep)
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
if existing_opening_occurrence:
representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
representation = ifcopenshell.util.representation.resolve_representation(representation)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
else:
opening_obj = tool.Ifc.get_object(opening)
if opening_obj:
tool.Ifc.unlink(element=opening)
tool.Blender.remove_data_blocks([opening_obj], remove_unused_data=True)
filling_obj = tool.Ifc.get_object(filling)
representation = self.generate_opening_from_filling(filling, filling_obj)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
# update voided object representation or all it's parts if it's an aggregate
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
for voided_element in voided_elements:
voided_obj = tool.Ifc.get_object(voided_element)
representation = tool.Geometry.get_active_representation(voided_obj)
if not representation:
continue
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=voided_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
def generate_opening_from_filling(
self,
filling: ifcopenshell.entity_instance,
filling_obj: bpy.types.Object,
opening_thickness_si: float = 0.0,
) -> ifcopenshell.entity_instance:
# Since openings are reused later, we give a default thickness of 1.2m
# which should cover the majority of curved, or super thick walls.
thickness = max(1.2, opening_thickness_si)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
profile = None
filling_type = ifcopenshell.util.element.get_type(filling)
if filling_type:
profile = ifcopenshell.util.representation.get_representation(
filling_type, "Model", "Profile", "ELEVATION_VIEW"
)
filling_obj = tool.Ifc.get_object(filling_type)
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
assert context
if profile:
profile = ifcopenshell.util.representation.resolve_representation(profile)
def get_curve_2d_from_3d(profile: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
if len(profile.Items) == 1:
curve_3d = profile.Items[0]
if tool.Ifc.get_schema() == "IFC2X3":
coords = [Vector(p).xz for p in shape_builder.get_polyline_coords(curve_3d)]
return shape_builder.polyline(coords, closed=True)
# using different algorithm to keep arc segments possible in the future
ifc_segments = [shape_builder.deep_copy(s) for s in curve_3d.Segments]
ifc_points = tool.Ifc.get().createIfcCartesianPointList2D(
[Vector(p).xz for p in curve_3d.Points.CoordList]
)
return tool.Ifc.get().createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
geometry = ifcopenshell.geom.create_shape(settings, profile)
verts = ifcopenshell.util.shape.get_vertices(geometry)
# [0, 2] represents X and Z ordinates
verts = [(np.around(v[[0, 2]], decimals=3) / unit_scale).tolist() for v in verts]
edges = ifcopenshell.util.shape.get_edges(geometry)
boundary_lines = [shapely.LineString([verts[v] for v in e]) for e in edges]
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(boundary_lines)
polygon = max(closed_polygons.geoms, key=lambda polygon: polygon.area)
return shape_builder.polyline(list(polygon.exterior.coords))
extrusion = shape_builder.extrude(
get_curve_2d_from_3d(profile),
magnitude=thickness / unit_scale,
position=Vector([0.0, -thickness * 0.5 / unit_scale, 0.0]),
**shape_builder.extrude_kwargs("Y"),
)
return shape_builder.get_representation(context, [extrusion])
if (
filling_rep := tool.Geometry.get_active_representation(filling_obj)
) and filling_rep.ContextOfItems == context:
x, y, z = filling_obj.dimensions
else:
# The filling_obj's mesh data is not the body geometry.
settings = ifcopenshell.geom.settings()
filling_element = tool.Ifc.get_entity(filling_obj)
filling_body = ifcopenshell.util.representation.get_representation(filling_element, context)
filling_geometry = ifcopenshell.geom.create_shape(settings, filling_body)
x = ifcopenshell.util.shape.get_x(filling_geometry)
y = ifcopenshell.util.shape.get_y(filling_geometry)
z = ifcopenshell.util.shape.get_z(filling_geometry)
opening_position = Vector([0.0, -thickness * 0.5 / unit_scale, 0.0])
opening_size = Vector([x, z]) / unit_scale
# Windows and doors can have a casing that overlaps the wall
# but shouldn't affect the size of the opening.
# So we shouldn't use object dimensions in that case. More: #2784
# Just keeping it for windows and doors for now to be safe
has_width_attribute, has_height_attribute = False, False
if filling.is_a() in ["IfcWindow", "IfcDoor"]:
if filling.OverallWidth:
opening_size.x = filling.OverallWidth
has_width_attribute = True
if filling.OverallHeight:
opening_size.y = filling.OverallHeight
has_height_attribute = True
# making sure if min_x or min_z != 0 to shift the opening accordingly
# to prevent something like #2784
if not has_width_attribute:
opening_position.x = min(v[0] for v in filling_obj.bound_box)
if not has_height_attribute:
opening_position.z = min(v[2] for v in filling_obj.bound_box)
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=opening_size),
magnitude=thickness / unit_scale,
position=opening_position,
**shape_builder.extrude_kwargs("Y"),
)
return shape_builder.get_representation(context, [extrusion])
def has_visible_openings(self, element):
for opening in [r.RelatedOpeningElement for r in tool.Geometry.get_openings(element)]:
if tool.Ifc.get_object(opening):
return True
return False
def get_existing_opening_occurrence_if_any(
self, filling: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
filling_type = ifcopenshell.util.element.get_type(filling)
if filling_type:
filling_occurrences = ifcopenshell.util.element.get_types(filling_type)
for filling_occurrence in filling_occurrences:
if filling_occurrence != filling and filling_occurrence.FillsVoids:
return filling_occurrence.FillsVoids[0].RelatingOpeningElement
class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.recalculate_fill"
bl_label = "Recalculate Fill"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.FillsVoids:
continue
openings = [r.RelatingOpeningElement for r in element.FillsVoids or []]
building_elements = []
for opening in openings:
building_elements.extend([r.RelatingBuildingElement for r in opening.VoidsElements or []])
for building_element in building_elements:
building_obj = tool.Ifc.get_object(building_element)
if tool.Ifc.is_moved(building_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=building_obj)
for opening in openings:
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
ifcopenshell.api.run(
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=obj.matrix_world
)
decomposed_building_elements = set()
for building_element in building_elements:
decomposed_building_elements.add(building_element)
decomposed_building_elements.update(ifcopenshell.util.element.get_decomposition(building_element))
for building_element in decomposed_building_elements:
building_obj = tool.Ifc.get_object(building_element)
if building_obj and building_obj.data:
representation = tool.Geometry.get_active_representation(building_obj)
if representation:
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=building_obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
class FlipFill(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.flip_fill"
bl_label = "Flip Fill"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.FillsVoids:
continue
tool.Geometry.flip_object(obj, "XY")
return {"FINISHED"}
class AddBoolean(Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boolean"
bl_label = "Add Boolean"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Applies a boolean to the selected IFC object using the other selected blender object as a void"
@classmethod
def poll(cls, context):
if not len(context.selected_objects) >= 2:
cls.poll_message_set("At least 2 objects need to be selected.")
return False
return True
def _execute(self, context):
first_obj = tool.Blender.get_active_object()
if not first_obj or not tool.Geometry.is_boolean_operand(first_obj):
self.report({"INFO"}, "At least two valid objects must be selected to add a boolean.")
return {"CANCELLED"}
second_objs = [
o for o in tool.Blender.get_selected_objects() if o != first_obj and tool.Geometry.is_boolean_operand(o)
]
if not second_objs:
self.report({"INFO"}, "At least two representation items must be selected to add a boolean.")
return {"CANCELLED"}
props = tool.Feature.get_boolean_props()
first_item = tool.Geometry.get_active_representation(first_obj)
assert first_item
second_items = [
representation for o in second_objs if (representation := tool.Geometry.get_active_representation(o))
]
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
rep_obj = tool.Geometry.get_geometry_props().representation_obj
rep_element = tool.Ifc.get_entity(rep_obj)
tool.Model.mark_manual_booleans(rep_element, booleans)
tool.Geometry.reload_representation(rep_obj)
if props.is_editing:
bpy.ops.bim.enable_editing_booleans()
tool.Root.reload_item_decorator()
class ShowOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.show_openings"
bl_label = "Show Openings"
bl_description = "Show openings for selected objects"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
for obj in tool.Blender.get_selected_objects():
element = tool.Ifc.get_entity(obj)
if element is None:
continue
self.show_object_openings(obj, element)
DecorationsHandler.install(bpy.context)
bpy.ops.bim.update_openings_focus()
return {"FINISHED"}
def show_object_openings(self, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
openings_elements = [rel.RelatedOpeningElement for rel in tool.Geometry.get_openings(element)]
if not openings_elements:
return
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
openings_elements_to_load = [o for o in openings_elements if not tool.Ifc.get_object(o)]
openings_objects = tool.Model.load_openings(openings_elements_to_load)
for obj in openings_objects:
tool.Root.add_tracked_opening(obj, "OPENING")
class UpdateOpeningsFocus(Operator):
bl_idname = "bim.update_openings_focus"
bl_label = "Update Openings Focus"
bl_description = "Show objects that are not part of the object or its openings as transparent"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
preferences = tool.Blender.get_addon_preferences()
if preferences.opening_focus_opacity == 100:
return {"FINISHED"}
openings: set[bpy.types.Object] = set()
building_objects: set[bpy.types.Object] = set()
props = tool.Model.get_model_props()
for opening in props.openings:
if opening.obj:
openings.add(opening.obj)
opening_element = tool.Ifc.get_entity(opening.obj)
assert opening_element
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
building_obj = tool.Ifc.get_object(building_element)
assert isinstance(building_obj, bpy.types.Object)
building_objects.add(building_obj)
for obj in context.scene.objects:
obj.color = [
obj.color[0],
obj.color[1],
obj.color[2],
(
1
if not props.openings
or not building_objects
or obj in openings
or obj in building_objects
or obj in context.selected_objects
else preferences.opening_focus_opacity / 100
),
]
return {"FINISHED"}
def hide_openings(context: bpy.types.Context, objects: Sequence[bpy.types.Object]) -> None:
objects_to_remove = set()
props = tool.Model.get_model_props()
for opening_prop in props.openings:
opening_obj = opening_prop.obj
if not opening_obj:
continue
opening_element = tool.Ifc.get_entity(opening_obj)
if opening_element:
if not opening_element.is_a("IfcOpeningElement"):
# This opening has been assigned to another ifc class. Remove it from the openings pool. See #3854
opening_prop.obj = None
continue
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
if building_element:
building_obj = tool.Ifc.get_object(building_element)
if building_obj in objects:
tool.Ifc.unlink(element=opening_element)
objects_to_remove.add(opening_obj)
if opening_obj in objects:
objects_to_remove.add(opening_obj)
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
tool.Model.purge_scene_openings()
bpy.ops.bim.update_openings_focus()
class HideAllOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.hide_all_openings"
bl_label = "Hide All Openings"
bl_description = "Hide every single opening"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
hide_openings(context, context.scene.objects[:])
return {"FINISHED"}
class HideOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.hide_openings"
bl_label = "Hide"
bl_description = "Hide Openings"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
hide_openings(context, context.selected_objects[:])
return {"FINISHED"}
class EditOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_openings"
bl_label = "Edit"
bl_description = "Edit Openings"
bl_options = {"REGISTER", "UNDO"}
apply_all: bpy.props.BoolProperty(default=False)
def _execute(self, context):
building_objs, opening_elements = self.get_buildings_and_openings(context)
self.edit_openings(building_objs, opening_elements)
tool.Model.purge_scene_openings()
tool.Model.reload_body_representation(building_objs)
bpy.ops.bim.update_openings_focus()
return {"FINISHED"}
def get_buildings_and_openings(
self, context: bpy.types.Context
) -> tuple[set[bpy.types.Object], set[ifcopenshell.entity_instance]]:
props = tool.Model.get_model_props()
building_objs: set[bpy.types.Object] = set()
opening_elements: set[ifcopenshell.entity_instance] = set()
objects_to_remove = set()
if self.apply_all:
for opening_prop in props.openings:
opening_obj = opening_prop.obj
if opening_obj is None:
continue
opening_element = tool.Ifc.get_entity(opening_obj)
if opening_element is None:
objects_to_remove.add(opening_obj)
continue
opening_elements.add(opening_element)
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
building_objs.add(tool.Ifc.get_object(building_element))
else:
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element.is_a("IfcOpeningElement"):
opening_element = element
opening_elements.add(opening_element)
if opening_element.VoidsElements:
building_element = opening_element.VoidsElements[0].RelatingBuildingElement
building_obj = tool.Ifc.get_object(building_element)
if building_obj:
building_objs.add(building_obj)
else:
for relation in tool.Geometry.get_openings(element):
opening_element = relation.RelatedOpeningElement
if tool.Ifc.get_object(opening_element):
opening_elements.add(opening_element)
building_objs.add(obj)
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
return building_objs, opening_elements
def edit_openings(
self, building_objs: set[bpy.types.Object], opening_elements: set[ifcopenshell.entity_instance]
) -> None:
props = tool.Geometry.get_geometry_props()
objects_to_remove: set[bpy.types.Object] = set()
for opening_element in opening_elements:
opening_obj = tool.Ifc.get_object(opening_element)
similar_openings = bonsai.core.geometry.get_similar_openings(tool.Ifc, opening_element)
similar_openings_building_objs = bonsai.core.geometry.get_similar_openings_building_objs(
tool.Ifc, similar_openings
)
building_objs.update(similar_openings_building_objs)
if opening_obj:
if tool.Ifc.is_edited(opening_obj):
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
bonsai.core.geometry.edit_similar_opening_placement(
tool.Geometry, opening_element, similar_openings
)
elif tool.Ifc.is_moved(opening_obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj)
bonsai.core.geometry.edit_similar_opening_placement(
tool.Geometry, opening_element, similar_openings
)
building_objs.update(
self.get_all_building_objects_of_similar_openings(opening_element)
) # NB this has nothing to do with clone similar_opening
tool.Ifc.unlink(element=opening_element)
if props.representation_obj == opening_obj:
props.representation_obj = None
objects_to_remove.add(opening_obj)
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
def get_all_building_objects_of_similar_openings(self, opening):
if not opening.is_a("IfcOpeningElement") or not opening.HasFillings:
return []
results = set()
for rel in opening.HasFillings:
filling_type = ifcopenshell.util.element.get_type(rel.RelatedBuildingElement)
if not filling_type:
continue
for occurrence in ifcopenshell.util.element.get_types(filling_type):
for rel2 in occurrence.FillsVoids:
for rel3 in rel2.RelatingOpeningElement.VoidsElements:
obj = tool.Ifc.get_object(rel3.RelatingBuildingElement)
if obj:
results.add(obj)
return results
class CloneOpening(Operator, tool.Ifc.Operator):
bl_idname = "bim.clone_opening"
bl_label = "Clone Opening"
bl_description = "Clone the active Opening object and assign to the selected Element"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if len(context.selected_objects) != 2:
cls.poll_message_set("Exactly 2 objects must be selected.")
return False
return True
def _execute(self, context):
# NOTE: Operator displayed in UI only with IfcOpeningElement being active.
ifc_file = tool.Ifc.get()
objects = bpy.context.selected_objects
opening_obj = context.active_object
assert opening_obj
opening = tool.Ifc.get_entity(opening_obj)
assert opening and opening.is_a("IfcOpeningElement")
voided_obj = next(o for o in objects if o != opening_obj)
voided_element = tool.Ifc.get_entity(voided_obj)
assert voided_element
opening_placement = opening.ObjectPlacement
opening_representation = opening.Representation
new_opening = ifcopenshell.api.run("root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement")
new_opening.Representation = opening_representation
ifcopenshell.api.feature.add_feature(ifc_file, feature=new_opening, element=voided_element)
new_opening.ObjectPlacement = opening_placement
# Update affected representations.
elements_to_update = tool.Aggregate.get_parts_recursively(voided_element)
for element in elements_to_update:
obj = tool.Ifc.get_object(element)
if not isinstance(obj, bpy.types.Object) or not isinstance(obj.data, bpy.types.Mesh):
continue
representation = tool.Geometry.get_active_representation(obj)
assert representation
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
class PurgeUnusedOpenings(Operator, tool.Ifc.Operator):
bl_idname = "bim.purge_unused_openings"
bl_label = "Purge Unused Openings"
bl_description = "Purge Openings that do not intersect with their related building element"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
poll = any(
tool.Geometry.has_openings(element)
for element in [tool.Ifc.get_entity(obj) for obj in context.selected_objects]
if element
)
if not poll:
cls.poll_message_set("No objects with openings selected.")
return False
return True
def _execute(self, context):
bpy.ops.bim.show_openings()
objects = context.selected_objects[:]
[o.select_set(False) for o in objects]
active_object = context.active_object
purged = 0
for obj in objects:
element = tool.Ifc.get_entity(obj)
if not element or not tool.Geometry.has_openings(element):
continue
obj_bvh_tree = tool.Geometry.get_bvh_tree(obj)
for opening_rel in tool.Geometry.get_openings(element):
opening_elt = opening_rel.RelatedOpeningElement
opening_obj = tool.Ifc.get_object(opening_elt)
opening_bvh_tree = tool.Geometry.get_bvh_tree(opening_obj)
if not opening_bvh_tree.overlap(obj_bvh_tree):
opening_obj.select_set(True)
purged += 1
if context.selected_objects:
bpy.ops.bim.override_object_delete(is_batch=False)
bpy.ops.bim.edit_openings(apply_all=True)
[o.select_set(True) for o in objects]
context.view_layer.objects.active = active_object
self.report({"INFO"}, f"{purged} unused openings were purged.")
return {"FINISHED"}
class RemoveBoolean(Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_boolean"
bl_label = "Remove Boolean"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Removes the actively selected boolean"
@classmethod
def poll(cls, context):
props = tool.Feature.get_boolean_props()
return props.active_boolean
def _execute(self, context):
props = tool.Feature.get_boolean_props()
ifcopenshell.api.geometry.remove_boolean(
tool.Ifc.get(), tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)
)
bpy.ops.bim.enable_editing_booleans()
rep_obj = tool.Geometry.get_geometry_props().representation_obj
assert rep_obj
tool.Geometry.reload_representation(rep_obj)
tool.Root.reload_item_decorator()
class SelectBoolean(Operator):
bl_idname = "bim.select_boolean"
bl_label = "Select Boolean"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Selects operands of the active boolean\nSHIFT-CLICK to select all operands recursively"
is_recursive: bpy.props.BoolProperty(name="Is Recursive", default=False, options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
props = tool.Feature.get_boolean_props()
return props.active_boolean
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.shift:
self.is_recursive = True
return self.execute(context)
def execute(self, context):
props = tool.Feature.get_boolean_props()
queue = [tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)]
items = {i.ifc_definition_id: i.obj for i in tool.Geometry.get_geometry_props().item_objs}
while queue:
item = queue.pop()
if item.is_a("IfcBooleanResult"):
if self.is_recursive:
queue.append(item.FirstOperand)
queue.append(item.SecondOperand)
else:
if obj := items.get(item.FirstOperand.id()):
tool.Blender.select_object(obj)
if obj := items.get(item.SecondOperand.id()):
tool.Blender.select_object(obj)
elif obj := items.get(item.id()):
tool.Blender.select_object(obj)
return {"FINISHED"}
# TODO: merge with ProfileDecorator?
class DecorationsHandler:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def __call__(self, context):
props = tool.Model.get_model_props()
if not props.openings:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
gprops = tool.Geometry.get_geometry_props()
for opening in props.openings:
obj = opening.obj
if gprops.representation_obj == obj:
# We are editing the representation of the opening :
for item in gprops.item_objs:
if item.obj.mode == "EDIT":
obj = item.obj
break
if not obj:
continue
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
verts = []
selected_edges = []
unselected_edges = []
selected_vertices = []
unselected_vertices = []
if obj.mode == "EDIT":
bm = bmesh.from_edit_mesh(obj.data)
for vertex in bm.verts:
co = tuple(obj.matrix_world @ vertex.co)
verts.append(co)
if vertex.hide:
continue
if vertex.select:
selected_vertices.append(co)
else:
unselected_vertices.append(co)
for edge in bm.edges:
edge_indices = [v.index for v in edge.verts]
if edge.hide:
continue
if edge.select:
selected_edges.append(edge_indices)
else:
unselected_edges.append(edge_indices)
self.draw_batch("LINES", verts, transparent_color(unselected_elements_color, 0.5), unselected_edges)
self.draw_batch("LINES", verts, selected_elements_color, selected_edges)
self.draw_batch("POINTS", unselected_vertices, unselected_elements_color)
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
verts = [tuple(obj.matrix_world @ v.co) for v in bm.verts]
if ios_edges_attribute := obj.data.attributes.get("ios_edges"):
edges = [e for i, e in enumerate(bm.edges) if ios_edges_attribute.data[i].value]
else:
edges = bm.edges
edges_indices = [tuple([v.index for v in e.verts]) for e in edges]
color = selected_elements_color if obj in context.selected_objects else special_elements_color
self.draw_batch("LINES", verts, color, edges_indices)
obj.data.calc_loop_triangles()
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
self.draw_batch("TRIS", verts, transparent_color(special_elements_color), tris)
if "HalfSpaceSolid" in obj.name:
# Arrow shape
verts = [
tuple(obj.matrix_world @ Vector((0, 0, 0))),
tuple(obj.matrix_world @ Vector((0, 0, 0.5))),
tuple(obj.matrix_world @ Vector((0.05, 0, 0.45))),
tuple(obj.matrix_world @ Vector((-0.05, 0, 0.45))),
tuple(obj.matrix_world @ Vector((0, 0.05, 0.45))),
tuple(obj.matrix_world @ Vector((0, -0.05, 0.45))),
]
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
color = selected_elements_color if obj in context.selected_objects else special_elements_color
self.draw_batch("LINES", verts, color, edges)
if obj.mode != "EDIT":
bm.free()