Files
IfcOpenShell/src/bonsai/bonsai/bim/module/geometry/operator.py
T

3423 lines
145 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 re
import bpy
import bmesh
import numpy as np
import numpy.typing as npt
import ifcopenshell
import ifcopenshell.api.group
import ifcopenshell.api.pset
import ifcopenshell.api.geometry
import ifcopenshell.api.layer
import ifcopenshell.api.material
import ifcopenshell.api.root
import ifcopenshell.api.style
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
import ifcopenshell.api
import ifcopenshell.api.boundary
import ifcopenshell.api.grid
import bonsai.core.geometry
import bonsai.core.geometry as core
import bonsai.core.aggregate
import bonsai.core.nest
import bonsai.core.style
import bonsai.core.root
import bonsai.core.drawing
import bonsai.tool as tool
import bonsai.bim.handler
from mathutils import Vector, Matrix, Quaternion
from time import time
from bonsai.bim.ifc import IfcStore
from ifcopenshell.util.shape_builder import ShapeBuilder
from typing import Any, Union, Literal, get_args, TYPE_CHECKING, assert_never, NamedTuple
from bonsai.bim.module.model.decorator import ProfileDecorator
if TYPE_CHECKING:
from bpy.stub_internal import rna_enums
class EditObjectPlacement(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_object_placement"
bl_label = "Edit Object Placement"
bl_description = (
"Write selected objects placements to IFC.\n"
"A star in the operator name indicates that active object placement in IFC is not yet synced with Blender"
)
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def _execute(self, context):
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
for obj in objs:
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_mesh_separate"
bl_label = "IFC Mesh Separate"
blender_op = bpy.ops.mesh.separate.get_rna_type()
bl_description = blender_op.description + ".\nAlso makes sure changes are in sync with IFC."
bl_options = {"REGISTER", "UNDO"}
blender_type_prop = blender_op.properties["type"]
type: bpy.props.EnumProperty(
name=blender_type_prop.name,
default=blender_type_prop.default,
items=[(i.identifier, i.name, i.description) for i in blender_type_prop.enum_items],
options={"SKIP_SAVE"},
)
if TYPE_CHECKING:
type: Any
@classmethod
def poll(cls, context):
if not context.selected_editable_objects:
cls.poll_message_set("No editable objects are selected.")
return False
return True
def invoke(self, context, event):
if "type" not in self.properties:
return bpy.ops.wm.call_menu(name="BIM_MT_hotkey_separate")
return self.execute(context)
def _execute(self, context):
if not tool.Ifc.get():
return bpy.ops.mesh.separate(type=self.type)
elif context.mode == "EDIT_MESH":
if any(
tool.Ifc.get_entity(obj) or tool.Geometry.is_representation_item(obj) for obj in context.objects_in_mode
):
self.report({"WARNING"}, "IFC Separate is not yet supported for EDIT mode with IFC elements.")
return {"CANCELLED"}
return bpy.ops.mesh.separate(type=self.type)
if self.type == "SELECTED":
self.report({"ERROR"}, "Separate by selection requires 'EDIT_MESH' mode.")
return {"CANCELLED"}
non_ifc_objects: list[bpy.types.Object] = []
selected_objects = context.selected_editable_objects
bpy.ops.object.select_all(action="DESELECT")
should_reload_representation = False
for obj in selected_objects:
if tool.Geometry.is_representation_item(obj):
res = self.separate_item(context, obj)
should_reload_representation |= res
elif element := tool.Ifc.get_entity(obj):
self.separate_element(context, element, obj)
else:
non_ifc_objects.append(obj)
bpy.ops.object.select_all(action="DESELECT")
if should_reload_representation:
rep_obj = tool.Geometry.get_geometry_props().representation_obj
assert rep_obj
tool.Geometry.reload_representation(rep_obj)
tool.Root.reload_item_decorator()
if non_ifc_objects:
with context.temp_override(selected_editable_objects=non_ifc_objects):
bpy.ops.mesh.separate(type=self.type)
def separate_item(self, context: bpy.types.Context, obj: bpy.types.Object) -> bool:
"""
:return: True if item was changed and ``representation_obj`` should be reloaded.
"""
tool.Blender.set_active_object(obj)
item = tool.Geometry.get_active_representation(obj)
representation_obj = tool.Geometry.get_geometry_props().representation_obj
assert item and representation_obj
assert (element := tool.Ifc.get_entity(representation_obj))
if tool.Geometry.is_meshlike_item(item):
bpy.ops.mesh.separate(type=self.type)
# Nothing got separated.
if len(context.selected_objects) == 1:
return False
gprops = tool.Geometry.get_geometry_props()
rep_obj = gprops.representation_obj
assert rep_obj
representation = tool.Geometry.get_active_representation(rep_obj)
assert representation
representation = ifcopenshell.util.representation.resolve_representation(representation)
representation_type: str = representation.RepresentationType
items: list[ifcopenshell.entity_instance] = list(representation.Items)
for obj_ in context.selected_objects:
items.append(self.add_meshlike_item(obj_, representation_type))
items.remove(item)
representation.Items = items
gprops.remove_item_object_by_entity(item)
tool.Geometry.remove_representation_item(item, element)
return True
else:
self.report({"INFO"}, f"Separating an {item.is_a()} is not supported")
return False
def add_meshlike_item(self, obj: bpy.types.Object, representation_type: str) -> ifcopenshell.entity_instance:
props = tool.Geometry.get_geometry_props()
obj.show_in_front = True
tool.Geometry.lock_object(obj)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rep_obj = props.representation_obj
assert rep_obj
assert isinstance(obj.data, bpy.types.Mesh)
verts = tool.Blender.get_verts_coordinates(obj.data.vertices)
verts = verts.astype("d")
if (coordinate_offset := tool.Geometry.get_cartesian_point_offset(rep_obj)) is not None:
verts += coordinate_offset
verts /= unit_scale
faces = [p.vertices[:] for p in obj.data.polygons]
if representation_type in ("Brep", "AdvancedBrep"):
item = builder.faceted_brep(verts, faces)
elif representation_type == "Tessellation":
item = builder.mesh(verts, faces)
else:
assert False, f"Unexpected representation type: '{representation_type}'."
tool.Geometry.name_item_object(obj, item)
tool.Ifc.link(item, obj.data)
props.add_item_object(obj, item)
return item
def separate_element(
self, context: bpy.types.Context, element: ifcopenshell.entity_instance, obj: bpy.types.Object
) -> None:
# You cannot separate meshes if the representation is mapped.
relating_type = tool.Root.get_element_type(element)
tool.Blender.set_active_object(obj)
if relating_type and tool.Root.does_type_have_representations(relating_type):
# We toggle edit mode to ensure that once representations are
# unmapped, our Blender mesh only has a single user.
tool.Blender.toggle_edit_mode(context)
bpy.ops.bim.unassign_type(related_object=obj.name)
tool.Blender.toggle_edit_mode(context)
bpy.ops.mesh.separate(type=self.type)
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
new_objs = [obj]
for new_obj in context.selected_objects:
if new_obj == obj:
continue
# This is not very efficient, it needlessly copies the representations first.
bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
new_objs.append(new_obj)
for new_obj in new_objs:
bpy.ops.bim.update_representation(obj=new_obj.name)
class OverrideOriginSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_origin_set"
blender_op = bpy.ops.object.origin_set.get_rna_type()
bl_label = "IFC Origin Set"
bl_description = (
blender_op.description + ".\nAlso makes sure changes are in sync with IFC (operator works only on IFC objects)"
)
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
blender_type_prop = blender_op.properties["type"]
origin_type: bpy.props.EnumProperty(
name=blender_type_prop.name,
default=blender_type_prop.default,
items=[(i.identifier, i.name, i.description) for i in blender_type_prop.enum_items],
)
def _execute(self, context):
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if tool.Ifc.is_moved(obj):
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
representation = tool.Geometry.get_active_representation(obj)
if not representation:
continue
representation = ifcopenshell.util.representation.resolve_representation(representation)
if not tool.Geometry.is_meshlike(representation):
continue
bpy.ops.object.origin_set(type=self.origin_type)
bpy.ops.bim.update_representation(obj=obj.name)
class AddRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_representation"
bl_label = "Add Representation"
bl_options = {"REGISTER", "UNDO"}
representation_conversion_method: bpy.props.EnumProperty(
items=[
("OUTLINE", "Trace Outline", "Traces outline by local XY axes, for Profile - by local XZ axes."),
(
"BOX",
"Bounding Box",
"Creates a bounding box representation.\n"
"For Plan context - 2D bounding box by local XY axes,\n"
"for Profile - 2D bounding box by local XZ axes.\n"
"For other contexts - bounding box is 3d.",
),
(
"OBJECT",
"From Object",
(
"Copies geometry from another object.\n"
"Final version of the geometry will be used (e.g. with all modifiers, shape keys applied)"
),
),
("PROJECT", "Full Representation", "Reuses the current representation"),
(
"CUBE",
"Cube",
(
"Add cube representation.\n"
"Useful for adding a representation to an object without any representations"
),
),
],
name="Representation Conversion Method",
)
def _execute(self, context):
obj = context.active_object
assert obj
props = tool.Geometry.get_geometry_props()
oprops = tool.Geometry.get_object_geometry_props(obj)
ifc_context = int(oprops.contexts or "0") or None
if not ifc_context:
return
ifc_context = tool.Ifc.get().by_id(ifc_context)
original_data = obj.data
conversion_method = self.representation_conversion_method
if not original_data and conversion_method not in ("OBJECT", "CUBE"):
self.report(
{"ERROR"},
(f"No mesh data found for the active object. Mesh data required for '{conversion_method}' method."),
)
return {"FINISH"}
new_rep_data: bpy.types.Mesh | None = None
if conversion_method == "OUTLINE":
if ifc_context.ContextType == "Plan":
new_rep_data = tool.Geometry.generate_outline_mesh(obj, axis="+Z")
elif ifc_context.ContextIdentifier == "Profile":
new_rep_data = tool.Geometry.generate_outline_mesh(obj, axis="-Y")
else:
new_rep_data = tool.Geometry.generate_outline_mesh(obj, axis="+Z")
tool.Geometry.change_object_data(obj, new_rep_data, is_global=True)
elif conversion_method == "BOX":
if ifc_context.ContextType == "Plan":
new_rep_data = tool.Geometry.generate_2d_box_mesh(obj, axis="Z")
elif ifc_context.ContextIdentifier == "Profile":
new_rep_data = tool.Geometry.generate_2d_box_mesh(obj, axis="Y")
else:
new_rep_data = tool.Geometry.generate_3d_box_mesh(obj)
tool.Geometry.change_object_data(obj, new_rep_data, is_global=True)
elif conversion_method in ("OBJECT", "CUBE"):
if conversion_method == "OBJECT":
if not (source_obj := props.representation_from_object):
self.report({"ERROR"}, "No object is selected to copy a representation from.")
return {"FINISHED"}
depsgraph = context.evaluated_depsgraph_get()
eval_obj = source_obj.evaluated_get(depsgraph)
new_rep_data = bpy.data.meshes.new_from_object(eval_obj)
else: # CUBE
new_rep_data = bpy.data.meshes.new("Cube")
bm = tool.Blender.get_bmesh_for_mesh(new_rep_data)
bmesh.ops.create_cube(bm, size=1)
tool.Blender.apply_bmesh(new_rep_data, bm)
if original_data:
tool.Geometry.change_object_data(obj, new_rep_data, is_global=True)
else:
obj = tool.Geometry.recreate_object_with_data(obj, new_rep_data, is_global=True)
try:
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=ifc_context,
ifc_representation_class=None,
profile_set_usage=None,
)
# Object might be recreated, need to set it as active again.
if context.active_object != obj:
tool.Blender.set_active_object(obj)
if new_rep_data:
bpy.data.meshes.remove(new_rep_data)
except core.IncompatibleRepresentationError:
if obj.data != original_data:
tool.Geometry.change_object_data(obj, original_data, is_global=True)
if new_rep_data:
bpy.data.meshes.remove(new_rep_data)
self.report({"ERROR"}, "No compatible representation for the context could be created.")
return {"CANCELLED"}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
props = tool.Geometry.get_geometry_props()
row = self.layout.row()
row.prop(self, "representation_conversion_method", text="")
if self.representation_conversion_method == "OBJECT":
row = self.layout.row()
row.prop(props, "representation_from_object", text="")
class SelectConnection(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_connection"
bl_label = "Select Connection"
bl_options = {"REGISTER", "UNDO"}
connection: bpy.props.IntProperty()
def _execute(self, context):
core.select_connection(tool.Geometry, connection=tool.Ifc.get().by_id(self.connection))
class RemoveConnection(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_connection"
bl_label = "Remove Connection"
bl_options = {"REGISTER", "UNDO"}
connection: bpy.props.IntProperty()
def _execute(self, context):
core.remove_connection(tool.Geometry, connection=tool.Ifc.get().by_id(self.connection))
class SwitchRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.switch_representation"
bl_label = "Switch Representation"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_definition_id: bpy.props.IntProperty()
should_reload: bpy.props.BoolProperty()
disable_opening_subtractions: bpy.props.BoolProperty()
should_switch_all_meshes: bpy.props.BoolProperty()
@classmethod
def poll(cls, context):
if (obj := context.active_object) and obj.mode == "OBJECT":
return True
cls.poll_message_set("Only available in OBJECT mode - Press TAB in the viewport")
return False
def _execute(self, context):
provided_representation = tool.Ifc.get().by_id(self.ifc_definition_id)
ifc_context = provided_representation.ContextOfItems
for obj in tool.Blender.get_selected_objects():
if not (element := tool.Ifc.get_entity(obj)) or obj.mode != "OBJECT":
continue
# Find representation to switch to.
if (active_representation := tool.Geometry.get_active_representation(obj)) is None:
# No active representation => probably has no representations.
continue
elif obj == context.active_object:
# Prioritize provided representation.
representation = provided_representation
elif active_representation.ContextOfItems == ifc_context:
# Prioritize already active representation if context matches.
representation = active_representation
else:
representation = ifcopenshell.util.representation.get_representation(element, ifc_context)
if not representation:
continue
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=self.should_reload,
is_global=self.should_switch_all_meshes,
should_sync_changes_first=True,
)
class RemoveRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_representation"
bl_label = "Remove Representation"
bl_options = {"REGISTER", "UNDO"}
representation_id: bpy.props.IntProperty()
def _execute(self, context):
core.remove_representation(
tool.Ifc,
tool.Geometry,
obj=context.active_object,
representation=tool.Ifc.get().by_id(self.representation_id),
)
class PurgeUnusedRepresentations(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.purge_unused_representations"
bl_label = "Purge Unused Representations"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
purged_representations = core.purge_unused_representations(tool.Ifc, tool.Geometry)
self.report({"INFO"}, f"{purged_representations} representations were purged.")
class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_representation"
bl_label = "Update Representation"
bl_description = (
"Write selected objects representations to IFC.\n"
"Converting to tesellation will also unassign material layer/profile sets.\n"
"A star in the operator name indicates that active object representation in IFC is not yet synced with Blender.\n"
"ALT+CLICK to apply openings to the mesh"
)
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_representation_class: bpy.props.StringProperty()
apply_openings: bpy.props.BoolProperty(
name="Apply Openings",
description=(
"Whether to apply openings to the mesh.\n"
"If False, operator will skip updating representation that has openings"
),
default=False,
options={"SKIP_SAVE"},
)
from_ui = False
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
self.apply_openings = True
self.from_ui = True
return self.execute(context)
def _execute(self, context):
if context.view_layer.objects.active and context.view_layer.objects.active.mode != "OBJECT":
# Ensure mode is object to prevent invalid mesh data causing CTD
bpy.ops.object.mode_set(mode="OBJECT", toggle=False)
obj_name: str = self.obj
objs = [bpy.data.objects[obj_name]] if obj_name else context.selected_objects
self.file = tool.Ifc.get()
for obj in objs:
# TODO: write unit tests to see how this bulk operation handles
# contradictory ifc_representation_class values and when
# ifc_representation_class is IfcTextLiteral
data = obj.data
if not tool.Geometry.is_data_supported_for_adding_representation(data):
continue
self.update_obj_mesh_representation(context, obj)
tool.Ifc.finish_edit(obj)
tool.Geometry.reload_representation(objs)
return {"FINISHED"}
def update_obj_mesh_representation(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
data = obj.data
assert tool.Geometry.is_data_supported_for_adding_representation(data)
mprops = tool.Geometry.get_mesh_props(data)
product = tool.Ifc.get_entity(obj)
assert product
material = ifcopenshell.util.element.get_material(product, should_skip_usage=True)
# NOTE: Currently iterator doesn't detect whether opening is actually affected the representation
# or it's just present on the element. In theory, we can also allow editing representations
# if we know that representation wasn't affected by existing openings.
has_openings = tool.Geometry.has_openings(product) and tool.Geometry.get_mesh_props(data).has_openings_applied
if has_openings and not self.apply_openings:
# Meshlike things with openings can only be updated without openings applied.
if self.from_ui:
self.report({"ERROR"}, f"Object '{obj.name}' has openings - representation cannot be updated.")
return
if not product.is_a("IfcGridAxis"):
tool.Geometry.clear_cache(product)
if product.is_a("IfcGridAxis"):
# Grid geometry does not follow the "representation" paradigm and needs to be treated specially
tool.Model.create_axis_curve(obj, product)
return
elif product.is_a("IfcRelSpaceBoundary"):
# TODO refactor
settings = tool.Boundary.get_assign_connection_geometry_settings(obj)
ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings)
return
if tool.Ifc.is_moved(obj) or tool.Geometry.is_scaled(obj):
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
old_representation = tool.Geometry.get_active_representation(obj)
assert old_representation
if material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]:
if self.ifc_representation_class == "IfcTessellatedFaceSet":
# We are explicitly casting to a tessellation, so remove all parametric materials.
element_type = ifcopenshell.util.element.get_type(product)
if element_type: # Some invalid IFCs use material sets without a type.
ifcopenshell.api.material.unassign_material(self.file, products=[element_type])
tool.Material.ensure_material_unassigned([element_type])
ifcopenshell.api.material.unassign_material(self.file, products=[product])
tool.Material.ensure_material_unassigned([product])
data = obj.data # Required after ensure_material_unassigned
else:
# These objects are parametrically based on an axis and should not be modified as a mesh
if extrusion := tool.Model.get_extrusion(old_representation):
self.report(
{"INFO"},
f"Representation for '{obj.name}' wasn't updated because "
"it has material set and extrusion that probably was defined parametrically.",
)
return
context_of_items = old_representation.ContextOfItems
# TODO: remove this code a bit later
# added this as a fallback for easier transition some annotation types to 3d
# if they were create before as 2d
element = tool.Ifc.get_entity(obj)
assert element
if tool.Drawing.is_annotation_object_type(element, ("FALL", "SECTION_LEVEL", "PLAN_LEVEL")):
context_of_items = tool.Drawing.get_annotation_context("MODEL_VIEW")
representation_data = {
"context": context_of_items,
"blender_object": obj,
"geometry": obj.data,
"coordinate_offset": tool.Geometry.get_cartesian_point_offset(obj),
"total_items": max(1, len(obj.material_slots)),
"should_force_faceted_brep": tool.Geometry.should_force_faceted_brep(),
"should_force_triangulation": tool.Geometry.should_force_triangulation(),
"should_generate_uvs": tool.Geometry.should_generate_uvs(obj),
"ifc_representation_class": self.ifc_representation_class,
}
if not self.ifc_representation_class:
representation_data["ifc_representation_class"] = tool.Geometry.get_ifc_representation_class(
product, old_representation
)
representation_data["profile_set_usage"] = tool.Geometry.get_profile_set_usage(product)
representation_data["text_literal"] = tool.Geometry.get_text_literal(old_representation)
# TODO: replace with core.add_representation?
new_representation = ifcopenshell.api.geometry.add_representation(self.file, **representation_data)
if new_representation is None:
self.report({"ERROR"}, "Error creating representation for Blender object.")
return {"CANCELLED"}
if tool.Geometry.is_body_representation(new_representation):
[
tool.Geometry.run_style_add_style(obj=mat)
for mat in tool.Geometry.get_object_materials_without_styles(obj)
]
props = tool.Geometry.get_geometry_props()
ifcopenshell.api.style.assign_representation_styles(
self.file,
shape_representation=new_representation,
styles=tool.Geometry.get_styles(obj, only_assigned_to_faces=True),
should_use_presentation_style_assignment=props.should_use_presentation_style_assignment,
)
tool.Geometry.record_object_materials(obj)
# TODO: move this into a replace_representation usecase or something
for inverse in self.file.get_inverse(old_representation):
ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation)
# As openings are already 'baked' to the geometry, we mark their representation as 'Reference'
# as they're not part of the object representation anymore.
if has_openings and self.apply_openings:
ifc_context = new_representation.ContextOfItems
for opening_rel in tool.Geometry.get_openings(product):
opening = opening_rel.RelatedOpeningElement
representation = ifcopenshell.util.representation.get_representation(opening, ifc_context)
if not representation:
continue
representation.RepresentationIdentifier = "Reference"
tool.Ifc.link(new_representation, data)
data.name = tool.Loader.get_mesh_name(new_representation)
# TODO: In simple scenarios, a type has a ShapeRepresentation of ID
# 123. This is then mapped through mapped representations by
# occurrences, with no cartesian transformation. In this case, the mesh
# data is 100% shared and therefore all have the same mesh name
# referencing ID 123. (i.e. the local origins are shared). However, in
# complex scenarios, occurrences may have their own cartesian
# transformation (via MappingTarget). This will mean that occurrences
# will not share the same mesh data and will instead reference a
# different ShapeRepresentation ID. In this scenario, we have to
# propagate the obj.data back to the type itself and all sibling
# occurrences and accommodate their individual cartesian
# transformations.
core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation)
if mprops.ifc_parameters:
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
class UpdateParametricRepresentation(bpy.types.Operator):
bl_idname = "bim.update_parametric_representation"
bl_label = "Update Parametric Representation"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
return (obj := context.active_object) and obj.mode == "OBJECT" and tool.Geometry.has_mesh_properties(obj.data)
def execute(self, context):
self.file = tool.Ifc.get()
obj = context.active_object
assert obj and tool.Geometry.has_mesh_properties(obj.data)
props = tool.Geometry.get_mesh_props(obj.data)
parameter = props.ifc_parameters[self.index]
self.file.by_id(parameter.step_id)[parameter.index] = parameter.value
show_representation_parameters = bool(props.ifc_parameters)
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=tool.Ifc.get().by_id(props.ifc_definition_id),
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
if show_representation_parameters:
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
return {"FINISHED"}
class GetRepresentationIfcParameters(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.get_representation_ifc_parameters"
bl_label = "Get Representation IFC Parameters"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
assert obj and tool.Geometry.has_mesh_properties(data := obj.data)
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
parameters = tool.Geometry.get_mesh_props(data).ifc_parameters
self.report({"INFO"}, f"{len(parameters)} parameters found.")
class CopyRepresentation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.copy_representation"
bl_label = "Copy Representation"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def _execute(self, context):
if not context.active_object:
return
bm = bmesh.new()
bm.from_mesh(context.active_object.data)
geometric_context = tool.Root.get_representation_context(
tool.Root.get_object_representation(context.active_object)
)
for obj in context.selected_objects:
if obj == context.active_object:
continue
if obj.data:
element = tool.Ifc.get_entity(obj)
if not element:
continue
bm.to_mesh(obj.data)
old_rep = tool.Geometry.get_representation_by_context(element, geometric_context)
if old_rep:
ifcopenshell.api.geometry.unassign_representation(
tool.Ifc.get(), product=element, representation=old_rep
)
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=old_rep)
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=geometric_context,
ifc_representation_class=None,
profile_set_usage=None,
)
def lock_error_message(name: str) -> str:
return f"'{name}' is locked. Unlock it via the Spatial panel in the Project Overview tab."
def calc_delete_is_batch(ifc_file: ifcopenshell.file, context: bpy.types.Context) -> bool:
total_elements = len(tool.Ifc.get().wrapped_data.entity_names())
total_polygons = sum([len(o.data.polygons) for o in context.selected_objects if o.type == "MESH"])
# These numbers are a bit arbitrary, but basically batching is only
# really necessary on large models and large geometry removals.
is_batch = total_elements > 500000 and total_polygons > 2000
return is_batch
class OverrideDelete(bpy.types.Operator):
bl_idname = "bim.override_object_delete"
bl_label = "IFC Delete"
blender_op = bpy.ops.object.delete.get_rna_type()
bl_description = (
blender_op.description
+ ".\nAlso makes sure changes in sync with IFC."
+ "\n\nIn IFC projects should be used always instead of 'object.delete'"
+ " to avoid producing invalid IFC objects."
)
bl_options = {"REGISTER", "UNDO"}
# IFC Delete is always global as we assume just 1 scene in IFC project.
# The prop is only needed to support default Blender behaviour when IFC project is not loaded.
use_global: bpy.props.BoolProperty(default=False)
confirm: bpy.props.BoolProperty(default=True)
is_batch: bpy.props.BoolProperty(name="Is Batch", default=False)
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
# Deep magick from the dawn of time
if tool.Ifc.get() is None:
bpy.ops.object.delete(use_global=self.use_global, confirm=self.confirm)
# TODO: is this still needed?
# Required otherwise gizmos are still visible
context.view_layer.objects.active = None
return {"FINISHED"}
else:
return IfcStore.execute_ifc_operator(self, context)
def invoke(self, context, event):
assert context.window_manager
ifc_file = tool.Ifc.get()
if ifc_file is None:
return bpy.ops.object.delete("INVOKE_DEFAULT", use_global=self.use_global, confirm=self.confirm)
else:
self.is_batch = calc_delete_is_batch(ifc_file, context)
if self.is_batch:
return context.window_manager.invoke_props_dialog(self)
elif self.confirm:
return context.window_manager.invoke_confirm(self, event)
self.confirm = True
return self.execute(context)
def draw(self, context):
row = self.layout.row()
row.prop(self, "is_batch", text="Enable Faster Deletion")
if self.is_batch:
row = self.layout.row()
row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR")
row = self.layout.row()
row.label(text="See system console for deletion progress.")
def _execute(self, context: bpy.types.Context):
start_time = time()
if self.is_batch:
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
# Very important to create a copy of selected objects before objects might get deleted.
# Acessing it after might produce a crash in Blender <4.4 and `None` values in >=4.4.
# See https://projects.blender.org/blender/blender/issues/138325
objects_to_remove = context.selected_objects
self.process_arrays(context)
clear_active_object = True
for i, obj in enumerate(objects_to_remove, 1):
# Log time.
time_since_start = time() - start_time
is_valid_data_block = tool.Blender.is_valid_data_block(obj)
if time_since_start > 10:
obj_name = f" ({obj.name})" if is_valid_data_block else ""
print(
f"Removing object {i}/{len(objects_to_remove)}{obj_name}. "
f"Time since start: {time_since_start:.2f} seconds."
)
if not is_valid_data_block:
continue
element = tool.Ifc.get_entity(obj)
if element:
if tool.Geometry.is_locked(element):
if obj == context.view_layer.objects.active:
clear_active_object = False
self.report({"ERROR"}, lock_error_message(obj.name))
continue
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
return {"FINISHED"}
tool.Geometry.delete_ifc_object(obj)
elif tool.Geometry.is_representation_item(obj):
tool.Geometry.delete_ifc_item(obj)
else:
bpy.data.objects.remove(obj)
for opening in tool.Model.get_model_props().openings:
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
bpy.data.objects.remove(opening.obj)
tool.Model.purge_scene_openings()
if self.is_batch:
old_file = tool.Ifc.get()
old_file.end_transaction()
new_file = ifcopenshell.util.element.unbatch_remove_deep2(tool.Ifc.get())
new_file.begin_transaction()
tool.Ifc.set(new_file)
self.transaction_data = {"old_file": old_file, "new_file": new_file}
IfcStore.add_transaction_operation(self)
if clear_active_object:
# Required otherwise gizmos are still visible
context.view_layer.objects.active = None
operator_time = time() - start_time
if operator_time > 10:
self.report({"INFO"}, "IFC Delete was finished in {:.2f} seconds".format(operator_time))
return {"FINISHED"}
def rollback(self, data):
tool.Ifc.set(data["old_file"])
data["old_file"].undo()
def commit(self, data):
data["old_file"].redo()
tool.Ifc.set(data["new_file"])
def process_arrays(self, context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get()
selected_objects = set(context.selected_objects)
array_parents: set[ifcopenshell.entity_instance] = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
if not pset:
continue
array_parents.add(ifc_file.by_guid(pset["Parent"]))
for array_parent in array_parents:
array_parent_obj = tool.Ifc.get_object(array_parent)
data = [(i, data) for i, data in enumerate(tool.Blender.Modifier.Array.get_modifiers_data(array_parent))]
# NOTE: there is a way to remove arrays more precisely but it's more complex
for i, modifier_data in reversed(data):
children = set(tool.Blender.Modifier.Array.get_children_objects(modifier_data))
if children.issubset(selected_objects):
with context.temp_override(active_object=array_parent_obj):
bpy.ops.bim.remove_array(item=i)
else:
break # allows to remove only n last layers of an array
class SelectedIdsData(NamedTuple):
objects: set[bpy.types.Object]
collections: set[bpy.types.Collection]
class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_outliner_delete"
bl_label = "IFC Delete"
blender_op = bpy.ops.outliner.delete.get_rna_type()
bl_description = (
blender_op.description
+ ".\nAlso makes sure changes in sync with IFC."
+ "\n\nIn IFC projects should be used always instead of 'outliner.delete'"
+ " to avoid producing invalid IFC objects."
)
bl_options = {"REGISTER", "UNDO"}
hierarchy: bpy.props.BoolProperty(default=False)
is_batch: bpy.props.BoolProperty(name="Is Batch", default=False)
@classmethod
def poll(cls, context) -> bool:
return len(getattr(context, "selected_ids", [])) > 0
def execute(self, context):
# In this override, we don't check self.hierarchy. This effectively
# makes Delete and Delete Hierarchy identical. This is on purpose, since
# non-hierarchical deletion may imply a whole bunch of potentially
# unintended IFC spatial modifications. To make life less confusing for
# the user, Delete means Delete. End of story.
# Deep magick from the dawn of time
if tool.Ifc.get():
return IfcStore.execute_ifc_operator(self, context)
# https://blender.stackexchange.com/questions/203729/python-get-selected-objects-in-outliner
# TODO: replace with outliner.delete?
selected_ids_data = self.get_selected_ids_data(context)
for obj in selected_ids_data.objects:
bpy.data.objects.remove(obj)
for collection in selected_ids_data.collections:
bpy.data.collections.remove(collection)
return {"FINISHED"}
def invoke(self, context, event):
assert context.window_manager
ifc_file = tool.Ifc.get()
if ifc_file:
self.is_batch = calc_delete_is_batch(ifc_file, context)
if self.is_batch:
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
def draw(self, context):
row = self.layout.row()
row.prop(self, "is_batch", text="Enable Faster Deletion")
if self.is_batch:
row = self.layout.row()
row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR")
def _execute(self, context):
selected_ids_data = self.get_selected_ids_data(context)
if selected_ids_data.objects:
try:
with context.temp_override(selected_objects=list(selected_ids_data.objects)):
bpy.ops.bim.override_object_delete(is_batch=self.is_batch)
except RuntimeError as e:
error_reports = tool.Blender.extract_error_reports(e)
if not error_reports:
raise
tool.Blender.report_operator_errors(self, error_reports)
for collection in selected_ids_data.collections:
# Removing an aggregate object would also remove it's collection
# making the collection data-block invalid.
if not tool.Blender.is_valid_data_block(collection):
continue
if collection.all_objects:
continue
bpy.data.collections.remove(collection)
return {"FINISHED"}
@classmethod
def get_selected_ids_data(cls, context: bpy.types.Context) -> SelectedIdsData:
objects_to_delete: set[bpy.types.Object] = set()
collections_to_delete: set[bpy.types.Collection] = set()
for item in context.selected_ids:
if isinstance(item, bpy.types.Collection):
collection_data = cls.get_collection_objects_and_children(item)
objects_to_delete |= collection_data["objects"]
collections_to_delete |= collection_data["children"]
collections_to_delete.add(item)
elif isinstance(item, bpy.types.Object):
objects_to_delete.add(item)
return SelectedIdsData(objects_to_delete, collections_to_delete)
@staticmethod
def get_collection_objects_and_children(collection: bpy.types.Collection) -> dict[str, Any]:
return {"objects": set(collection.all_objects), "children": set(collection.children_recursive)}
class OverrideDuplicateMoveMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_macro"
bl_label = "IFC Duplicate Objects"
bl_options = {"REGISTER", "UNDO"}
class OverrideDuplicateMove(bpy.types.Operator):
bl_idname = "bim.override_object_duplicate_move"
bl_label = "IFC Duplicate Objects"
bl_options = {"REGISTER", "UNDO"}
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context)
@staticmethod
def execute_duplicate_operator(
operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False
) -> set["rna_enums.OperatorReturnItems"]:
# Deep magick from the dawn of time
if tool.Ifc.get():
IfcStore.execute_ifc_operator(operator, context)
return {"FINISHED"}
if linked:
bpy.ops.object.duplicate_move_linked()
else:
bpy.ops.object.duplicate_move()
return {"FINISHED"}
@staticmethod
def execute_ifc_duplicate_operator(operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False):
objects_to_remove = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
objects_to_remove.add(obj)
operator.report({"ERROR"}, f"Drawing '{obj.name}' not duplicated.")
continue
if tool.Geometry.is_locked(element):
objects_to_remove.add(obj)
operator.report({"ERROR"}, lock_error_message(obj.name))
for obj in objects_to_remove:
tool.Blender.deselect_object(obj)
old_to_new, new_active_obj = tool.Geometry.duplicate_ifc_objects(
set(context.selected_objects) - objects_to_remove,
linked=linked,
active_object=context.active_object,
)
if new_active_obj:
context.view_layer.objects.active = new_active_obj
return old_to_new
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
bl_idname = "bim.override_object_duplicate_move_linked_macro"
bl_label = "IFC Duplicate Linked"
bl_options = {"REGISTER", "UNDO"}
class OverrideDuplicateMoveLinked(bpy.types.Operator):
bl_idname = "bim.override_object_duplicate_move_linked"
bl_label = "IFC Duplicate Linked"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=True)
def _execute(self, context):
return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_description = "Create and move a new linked aggregate"
bl_idname = "bim.object_duplicate_move_linked_aggregate_macro"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.object_duplicate_move_linked_aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
@staticmethod
def execute_ifc_duplicate_linked_aggregate_operator(self, context, location_from_3d_cursor=False):
self.new_active_obj = None
self.group_name = "BBIM_Linked_Aggregate"
self.pset_name = "BBIM_Linked_Aggregate"
old_to_new = {}
def select_objects_and_add_data(element):
add_linked_aggregate_group(element)
obj = tool.Ifc.get_object(element)
obj.select_set(True)
parts = ifcopenshell.util.element.get_parts(element)
if parts:
index = get_max_index(parts)
add_linked_aggregate_pset(element, index)
index += 1
for part in parts:
if part.is_a("IfcElementAssembly"):
select_objects_and_add_data(part)
else:
index = add_linked_aggregate_pset(part, index)
# index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
def add_linked_aggregate_pset(part, index):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
if not pset:
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=part, name=self.pset_name)
ifcopenshell.api.pset.edit_pset(
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
index += 1
else:
pass
return index
def add_linked_aggregate_group(element):
linked_aggregate_group = None
product_groups_name = [
r.RelatingGroup.Name
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
]
if self.group_name in product_groups_name:
return
linked_aggregate_group = ifcopenshell.api.group.add_group(tool.Ifc.get(), name=self.group_name)
ifcopenshell.api.group.assign_group(tool.Ifc.get(), products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new):
for new in old_to_new.values():
if new[0].is_a("IfcElementAssembly"):
group_elements = [
r.RelatedObjects
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
][0]
number = len(group_elements) - 1
number = f"{number:02d}"
new_obj = tool.Ifc.get_object(new[0])
pattern1 = r"_\d"
if re.findall(pattern1, new_obj.name):
split_name = new_obj.name.split("_")
new_obj.name = split_name[0] + "_" + number
continue
pattern2 = r"\.\d{3}"
if re.findall(pattern2, new_obj.name):
split_name = new_obj.name.split(".")
new_obj.name = split_name[0] + "_" + number
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
def copy_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=new[0], name=self.pset_name)
ifcopenshell.api.pset.edit_pset(
tool.Ifc.get(),
pset=new_pset,
properties={"Index": pset["Index"]},
)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(old, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.assign_group", group=linked_aggregate_group[0], products=new)
def get_location_from_3d_cursor(old_to_new, aggregate):
base_obj = tool.Ifc.get_object(aggregate)
base_obj_location = base_obj.location.copy()
for new in old_to_new.values():
new_obj = tool.Ifc.get_object(new[0])
location_diff = new_obj.location - base_obj_location
new_obj.location = context.scene.cursor.location + location_diff
if len(context.selected_objects) != 1:
return {"FINISHED"}
selected_obj = context.selected_objects[0]
selected_element = tool.Ifc.get_entity(selected_obj)
if selected_element.is_a("IfcElementAssembly"):
pass
elif selected_element.Decomposes:
if selected_element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
selected_element = selected_element.Decomposes[0].RelatingObject
selected_obj = tool.Ifc.get_object(selected_element)
else:
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
return {"FINISHED"}
select_objects_and_add_data(selected_element)
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
tool.Root.recreate_aggregate(old_to_new)
copy_linked_aggregate_data(old_to_new)
custom_incremental_naming_for_element_assembly(old_to_new)
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, selected_element)
bonsai.bim.handler.refresh_ui_data()
return old_to_new
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
bl_idname = "bim.duplicate_linked_aggregate_to_3d_cursor"
bl_label = "IFC Duplicate Linked Aggregate to 3d Cursor"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(
self, context, location_from_3d_cursor=True
)
class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.refresh_linked_aggregate"
bl_label = "IFC Refresh Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def _execute(self, context):
self.new_active_obj = None
self.group_name = "BBIM_Linked_Aggregate"
self.pset_name = "BBIM_Linked_Aggregate"
refresh_start_time = time()
old_to_new = {}
original_names: dict[int, dict[int, str]] = {}
def delete_objects(element: ifcopenshell.entity_instance) -> None:
"""Remove IfcElementAssembly and it's parts."""
parts = ifcopenshell.util.element.get_parts(element)
if parts:
for part in parts:
if part.is_a("IfcElementAssembly"):
delete_objects(part)
continue
tool.Geometry.delete_ifc_object(tool.Ifc.get_object(part))
tool.Geometry.delete_ifc_object(tool.Ifc.get_object(element))
def get_original_names(element: ifcopenshell.entity_instance) -> dict[int, dict[int, str]]:
group = [
r.RelatingGroup
for r in getattr(element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
][0].id()
original_names[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"]
original_names[group][index] = tool.Ifc.get_object(element).name
parts = ifcopenshell.util.element.get_parts(element)
if parts:
for part in parts:
if part.is_a("IfcElementAssembly"):
# TODO: missing assignment.
original_names | get_original_names(part)
else:
try:
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
except:
continue
if pset:
index = pset["Index"]
original_names[group][index] = tool.Ifc.get_object(part).name
return original_names
def set_original_name(obj: bpy.types.Object, original_names: dict[int, dict[int, str]]) -> None:
element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if ifcopenshell.util.element.get_parts(
element
): # if element has parts it means it is the base of and aggregate or sub-aggregate
aggregate = element
group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
]
if not group:
return
group = group[0].id()
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset["Index"]
try:
obj.name = original_names[group][index]
except:
return
def get_element_assembly(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
if element.is_a("IfcElementAssembly"):
return element
elif element.Decomposes:
if element.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"):
element = element.Decomposes[0].RelatingObject
return element
else:
return None
def handle_selection(
selected_objs: list[bpy.types.Object],
) -> tuple[list[int], list[ifcopenshell.entity_instance]]:
selected_elements = [tool.Ifc.get_entity(selected_obj) for selected_obj in selected_objs]
if None in selected_elements:
self.report({"INFO"}, "Object has no Ifc Metadata.")
return None, None
selected_parents = []
for selected_element in selected_elements:
selected_element = get_element_assembly(selected_element)
if not selected_element:
self.report({"INFO"}, "Object is not part of a IfcElementAssembly.")
return None, None
selected_parents.append(selected_element)
selected_parents = list(set(selected_parents))
linked_aggregate_groups = []
for selected_element in selected_parents:
product_linked_agg_group = [
r.RelatingGroup
for r in getattr(selected_element, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if self.group_name in r.RelatingGroup.Name
]
try:
linked_aggregate_groups.append(product_linked_agg_group[0].id())
except:
self.report({"INFO"}, "Object is not part of a Linked Aggregate.")
return None, None
return list(set(linked_aggregate_groups)), selected_parents
def get_original_matrix(
element: ifcopenshell.entity_instance, base_instance: ifcopenshell.entity_instance
) -> tuple[Matrix, tuple[Vector, Quaternion, Vector]]:
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
return selected_matrix, duplicate_matrix
def set_new_matrix(
selected_matrix: Matrix, duplicate_matrix: tuple[Vector, Quaternion, Vector], old_to_new: dict
) -> None:
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
active_element = tool.Ifc.get_entity(context.active_object)
if not active_element:
self.report({"INFO"}, "Object has no Ifc metadata.")
return {"FINISHED"}
active_element = get_element_assembly(active_element)
selected_objs = context.selected_objects
linked_aggregate_groups, selected_parents = handle_selection(selected_objs)
if not linked_aggregate_groups or not selected_parents:
return {"FINISHED"}
if len(linked_aggregate_groups) > 1:
if len(selected_parents) != len(linked_aggregate_groups):
self.report(
{"INFO"},
"Select only one object from each Linked Aggregate or multiple objects from the same Linked Aggregate.",
)
return {"FINISHED"}
for group in linked_aggregate_groups:
elements = tool.Drawing.get_group_elements(tool.Ifc.get().by_id(group))
if len(linked_aggregate_groups) > 1:
base_instance = [e for e in elements if e in selected_parents][0]
instances_to_refresh = elements
elif (len(linked_aggregate_groups) == 1) and (len(selected_parents) > 1):
base_instance = active_element
instances_to_refresh = [element for element in elements if element in selected_parents]
else:
base_instance = active_element
instances_to_refresh = elements
for element in instances_to_refresh:
if element.GlobalId == base_instance.GlobalId:
continue
element_aggregate = ifcopenshell.util.element.get_aggregate(element)
selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance)
original_names = get_original_names(element)
delete_objects(element)
for obj in context.selected_objects:
obj.select_set(False)
tool.Ifc.get_object(base_instance).select_set(True)
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
set_new_matrix(selected_matrix, duplicate_matrix, old_to_new)
for old, new in old_to_new.items():
if element_aggregate and new[0].is_a("IfcElementAssembly"):
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if not new_aggregate:
bonsai.core.aggregate.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(element_aggregate),
related_obj=tool.Ifc.get_object(new[0]),
)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
set_original_name(new_obj, original_names)
bonsai.bim.handler.refresh_ui_data()
operator_time = time() - refresh_start_time
if operator_time > 10:
self.report({"INFO"}, "Refresh Aggregate was finished in {:.2f} seconds".format(operator_time))
return {"FINISHED"}
class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_object_join"
bl_label = "IFC Join"
blender_op = bpy.ops.mesh.separate.get_rna_type()
bl_description = (
blender_op.description
+ ".\nAlso makes sure changes are in sync with IFC."
+ "\n\nJoining IFC object into another IFC object/Blender object will "
+ "only merge their current representations and will remove joined object from IFC. "
+ "Representation items are ignored for this case."
+ "\n\nJoining representation items only supported for joining with other representation items "
+ "of the same representation (only mesh-like items supported currently) "
+ "or with non-IFC Blender objects."
)
bl_options = {"REGISTER", "UNDO"}
target: bpy.types.Object
target_element: ifcopenshell.entity_instance
@classmethod
def poll(cls, context):
if not bpy.ops.object.join.poll():
cls.poll_message_set("Active object is not EDITable.")
return False
if not context.selected_editable_objects:
cls.poll_message_set("No objects are selected.")
return False
return True
def invoke(self, context, event):
if not tool.Ifc.get():
return bpy.ops.object.join()
return self.execute(context)
def _execute(self, context):
if not tool.Ifc.get():
return bpy.ops.object.join()
assert context.active_object
self.target = context.active_object
self.target_type = self.target.type
if not any(obj.type == self.target_type and obj != self.target for obj in context.selected_editable_objects):
self.report({"ERROR"}, f"No additional objects of type '{self.target_type}' selected to join.")
return {"CANCELLED"}
if tool.Geometry.is_representation_item(self.target):
return self.join_item(context)
elif target_element := tool.Ifc.get_entity(self.target):
self.target_element = target_element
return self.join_ifc_obj(context)
return self.join_blender_obj(context)
def join_item(self, context: bpy.types.Context) -> None:
assert context.view_layer
props = tool.Geometry.get_geometry_props()
ifc_file = tool.Ifc.get()
item = tool.Geometry.get_active_representation(self.target)
assert item
if tool.Geometry.is_meshlike_item(item):
tool.Geometry.dissolve_triangulated_edges(self.target)
item_objs = [i.obj for i in props.item_objs if i.obj]
joined_ifc_item_objs: list[bpy.types.Object] = []
for selected_obj in context.selected_editable_objects:
if selected_obj in item_objs:
if selected_obj != self.target:
tool.Geometry.dissolve_triangulated_edges(selected_obj)
joined_ifc_item_objs.append(selected_obj)
elif not tool.Ifc.get_entity(selected_obj) and selected_obj.type == self.target_type:
# Allow joining with non-ifc Blender objects.
pass
else:
selected_obj.select_set(False)
bpy.ops.object.join()
new_item = tool.Geometry.edit_meshlike_item(self.target)
for joined_obj in joined_ifc_item_objs:
tool.Geometry.delete_ifc_item(joined_obj)
# Refresh `item_objs`.
items_data = {i.obj: i.ifc_definition_id for i in props.item_objs if i.obj}
props.item_objs.clear()
if new_item:
items_data[self.target] = new_item.id()
for obj, ifc_id in items_data.items():
props.add_item_object(obj, ifc_file.by_id(ifc_id))
if new_item is None:
tool.Root.reload_item_decorator()
return
assert (rep_obj := props.representation_obj)
tool.Geometry.reload_representation(rep_obj)
context.view_layer.update()
tool.Root.reload_item_decorator()
else:
self.report(
{"WARNING"},
f"Unsupported type of item to join: {item}. Currently only mesh-like items are supported.",
)
def join_ifc_obj(self, context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get()
builder = ShapeBuilder(ifc_file)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
representation = tool.Geometry.get_active_representation(self.target)
assert representation
representation_type = representation.RepresentationType
if representation_type in ("Tessellation", "Brep"):
for obj in context.selected_editable_objects:
if obj == self.target:
continue
if obj.type != self.target_type:
# Should be safe to pass to object.join, but need to skip bim.update_representation.
obj.select_set(False)
continue
element = tool.Ifc.get_entity(obj)
if element:
ifcopenshell.api.root.remove_product(ifc_file, product=element)
elif tool.Geometry.is_representation_item(obj):
obj.select_set(False)
bpy.ops.object.join()
bpy.ops.bim.update_representation(obj=self.target.name, ifc_representation_class="")
else:
# Could support it but need to make sure all objects are on the same plane.
if representation_type == "Curve2D":
self.report({"ERROR"}, "Joining representation of type Curve2D is not supported.")
return
M_TRANSLATION = (slice(0, 3), 3)
target_placement = np.array(self.target.matrix_world)
target_placement[M_TRANSLATION] /= si_conversion
def apply_placement(
local_pos: npt.NDArray[np.float64], obj_placement: ifcopenshell.util.placement.MatrixType
) -> npt.NDArray[np.float64]:
position = obj_placement @ local_pos
position = np.linalg.inv(target_placement) @ position
return position
items = list(representation.Items)
for obj in context.selected_editable_objects:
if obj == self.target:
continue
if obj.type != self.target_type:
obj.select_set(False)
continue
element = tool.Ifc.get_entity(obj)
# Non IFC elements cannot be joined since we cannot guarantee SweptSolid compliance
# This check also is also needed to skip representation items.
if not element:
obj.select_set(False)
continue
# Only objects of the same representation type can be joined
obj_rep = tool.Geometry.get_active_representation(obj)
assert obj_rep
if obj_rep.RepresentationType != representation_type:
obj.select_set(False)
self.report(
{"ERROR"},
f"IFC join failed - object '{obj.name}' has a different representation type "
f"({obj_rep.RepresentationType})\nthan target '{self.target.name}' ({representation_type}).",
)
return
placement = np.array(obj.matrix_world)
placement[M_TRANSLATION] /= si_conversion
rep_items = list(obj_rep.Items)
curve_set_items = {}
curve_set_mapping = {}
supported_item_types = ("IfcSweptAreaSolid", "IfcIndexedPolyCurve", "IfcGeometricCurveSet")
def validate_item(item: ifcopenshell.entity_instance) -> bool:
return any(item.is_a(ifc_class) for ifc_class in supported_item_types)
error_msg = "Unsupported representation item type for joining: {}."
for item in rep_items:
item_class = item.is_a()
if not validate_item(item):
self.report({"ERROR"}, error_msg.format(item_class))
return
if item_class == "IfcGeometricCurveSet":
sub_items = item.Elements
for sub_item in item.Elements:
if not validate_item(sub_item):
self.report({"ERROR"}, error_msg.format(sub_item.is_a()))
return
rep_items.extend(sub_items)
for sub_item in sub_items:
curve_set_items[sub_item] = item
processed_point_lists = {}
for item in rep_items:
item_class = item.is_a()
if item_class == "IfcGeometricCurveSet":
copied_item = ifc_file.create_entity("IfcGeometricCurveSet", Elements=())
elif item_class == "IfcIndexedPolyCurve":
# Process points lists separately as they tend to be reused.
copied_item = ifcopenshell.util.element.copy_deep(
ifc_file, item, exclude=("IfcCartesianPointList",)
)
new_points = processed_point_lists.get(points := item.Points)
if new_points is None:
new_points = ifcopenshell.util.element.copy_deep(ifc_file, points)
dim = item.Dim
append_coord = (1.0,) if dim == 3 else (0.0, 1.0)
coords = points.CoordList
points.CoordList = [
apply_placement(np.append(c, append_coord), placement).tolist()[:3] for c in coords
]
processed_point_lists[points] = new_points
item.Points = new_points
else:
copied_item = ifcopenshell.util.element.copy_deep(ifc_file, item)
for style in item.StyledByItem:
copied_style = ifcopenshell.util.element.copy(ifc_file, style)
copied_style.Item = copied_item
if item.is_a("IfcSweptAreaSolid"):
if copied_item.Position:
position = ifcopenshell.util.placement.get_axis2placement(copied_item.Position)
else:
position = np.eye(4, dtype=float)
position = apply_placement(position, placement)
copied_item.Position = builder.create_axis2_placement_3d_from_matrix(position)
elif item.is_a("IfcIndexedPolyCurve"):
curve_set = curve_set_items.get(item)
if curve_set:
new_curve_set = curve_set_mapping[curve_set]
new_curve_set.Elements = new_curve_set.Elements + (copied_item,)
continue # Item is added to curve set items instead of representation items.
elif item_class == "IfcGeometricCurveSet":
curve_set_mapping[item] = copied_item
else:
assert False, f"Unexpected item type: {item.is_a()}. This is a bug."
items.append(copied_item)
ifcopenshell.api.root.remove_product(ifc_file, product=element)
representation.Items = items
bpy.ops.object.join()
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=self.target,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
def join_blender_obj(self, context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get()
for obj in context.selected_editable_objects:
if obj.type != self.target_type:
continue
# TODO Properly handle element types, grid axes, and representation items
element = tool.Ifc.get_entity(obj)
if element:
ifcopenshell.api.root.remove_product(ifc_file, product=element)
elif tool.Geometry.is_representation_item(obj):
obj.select_set(False)
bpy.ops.object.join()
class OverridePasteBuffer(bpy.types.Operator):
bl_idname = "bim.override_paste_buffer"
bl_label = "IFC Paste BIM Objects"
bl_description = (
"Paste objects from the internal clipboard.\n\n"
"Note that pasted objects will be unliked from IFC, so some IFC data will be lost (psets, materials, etc).\n"
"To paste object and keep IFC data use 'IFC Duplicate Objects'.\n\n"
"If working in Bonsai, to prevent issues this operator must be always used instead of the default Blender 'Paste Objects'."
)
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
previously_selected_objects = set(context.selected_objects)
bpy.ops.view3d.pastebuffer()
# If object selection did not change, then there were no objects to paste.
# Then exist early to prevent unlinking previously selected objects.
pasted_objects = context.selected_objects
if previously_selected_objects == set(pasted_objects):
self.report({"INFO"}, "No objects to paste.")
return {"FINISHED"}
for obj in pasted_objects:
# Pasted objects may come from another Blender session, or even
# from the same session where the original object has since
# been deleted. As the source element may not exist, paste will
# always unlink the element. If you want to duplicate an
# element, use the duplicate commands.
tool.Root.unlink_object(obj)
self.report({"INFO"}, f"{len(pasted_objects)} object(s) pasted and unlinked from IFC.")
return {"FINISHED"}
class OverrideEscape(bpy.types.Operator):
bl_idname = "bim.override_escape"
bl_label = "Override Escape"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Geometry.get_geometry_props()
if props.mode == "ITEM":
tool.Geometry.disable_item_mode()
elif props.mode == "EDIT":
bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT", should_save=False)
tool.Geometry.disable_item_mode()
elif tool.Model.get_model_props().openings:
bpy.ops.bim.hide_all_openings()
elif tool.Aggregate.get_aggregate_props().in_aggregate_mode:
bpy.ops.bim.disable_aggregate_mode()
elif active_object := context.active_object:
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
pass
return {"FINISHED"}
class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Switch from Object to Item to Edit mode"
bl_idname = "bim.override_mode_set_edit"
bl_label = "IFC Mode Set Edit"
bl_options = {"REGISTER", "UNDO"}
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
def _invoke(self, context, event):
if not tool.Ifc.get():
return tool.Blender.toggle_edit_mode(context)
return self.execute(context)
def _execute(self, context):
selected_objs = context.selected_objects # Purposely exclude active object
aprops = tool.Aggregate.get_aggregate_props()
nprops = tool.Nest.get_nest_props()
if self.has_aggregates(selected_objs):
bonsai.core.aggregate.enter_aggregate_mode(tool.Aggregate, context.active_object)
return {"FINISHED"}
if self.has_nests(selected_objs):
if not nprops.in_nest_mode:
bonsai.core.nest.enable_nest_mode(tool.Nest, context.active_object)
return {"FINISHED"}
if len(selected_objs) == 1 and context.active_object == selected_objs[0]:
self.handle_single_object(context, context.active_object)
elif len(selected_objs) == 0:
if aprops.in_aggregate_mode:
gprops = tool.Geometry.get_geometry_props()
if gprops.representation_obj:
tool.Geometry.disable_item_mode()
else:
bonsai.core.aggregate.exit_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
if nprops.in_nest_mode:
bonsai.core.nest.disable_nest_mode(tool.Nest)
return {"FINISHED"}
tool.Geometry.disable_item_mode()
elif len(selected_objs) > 1:
self.handle_multiple_selected_objects(context)
def handle_single_object(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
props = tool.Geometry.get_geometry_props()
pprops = tool.Project.get_project_props()
aprops = tool.Aggregate.get_aggregate_props()
if obj == props.representation_obj:
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
elif obj in [o.obj for o in aprops.not_editing_objects]:
obj.select_set(False)
self.report(
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
)
elif obj in pprops.clipping_planes_objs:
self.report({"ERROR"}, "Clipping planes cannot be edited")
elif element:
if not obj.data:
self.report({"INFO"}, "No geometry to edit")
elif tool.Geometry.is_locked(element):
self.report({"ERROR"}, lock_error_message(obj.name))
elif obj.data and tool.Geometry.is_profile_based(obj.data):
bpy.ops.bim.hotkey(hotkey="S_E")
elif element.is_a("IfcRelSpaceBoundary"):
bpy.ops.bim.enable_editing_boundary_geometry()
elif element.is_a("IfcGridAxis"):
self.enable_edit_mode(context)
elif tool.Blender.Modifier.try_applying_edit_mode(obj, element):
pass
else:
bpy.ops.bim.import_representation_items()
elif tool.Geometry.is_representation_item(obj):
self.enable_editing_representation_item(context, obj)
else: # A regular Blender object
self.enable_edit_mode(context)
def handle_multiple_selected_objects(self, context: bpy.types.Context) -> Union[None, set[str]]:
obj = context.active_object
if obj and (tool.Ifc.get_entity(obj) or tool.Geometry.is_representation_item(obj)):
tool.Blender.select_and_activate_single_object(context, obj)
self.handle_single_object(context, obj)
else:
blender_objs = []
ifc_objs = []
for selected_obj in tool.Blender.get_selected_objects():
if tool.Ifc.get_entity(selected_obj) or tool.Geometry.is_representation_item(selected_obj):
ifc_objs.append(selected_obj)
else:
blender_objs.append(selected_obj)
if blender_objs:
for obj in ifc_objs:
obj.select_set(False)
context.view_layer.objects.active = blender_objs[0]
blender_objs[0].select_set(True)
return tool.Blender.toggle_edit_mode(context)
elif ifc_objs:
obj = ifc_objs[0]
tool.Blender.select_and_activate_single_object(context, obj)
self.handle_single_object(context, obj)
def enable_editing_representation_item(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
item = tool.Geometry.get_active_representation(obj)
assert item
element = tool.Ifc.get_entity(tool.Geometry.get_geometry_props().representation_obj)
if tool.Geometry.is_meshlike_item(item):
tool.Geometry.dissolve_triangulated_edges(obj)
tool.Blender.select_and_activate_single_object(context, obj)
assert isinstance(mesh := obj.data, bpy.types.Mesh)
props = tool.Geometry.get_mesh_props(mesh)
props.mesh_checksum = tool.Geometry.get_mesh_checksum(mesh)
self.enable_edit_mode(context)
elif (
item.is_a("IfcSweptAreaSolid")
and (usage := tool.Model.get_usage_type(element))
and usage in ("LAYER1", "LAYER2")
):
self.report({"INFO"}, f"Parametric {usage} elements cannot be edited directly")
elif item.is_a("IfcSweptAreaSolid"):
tool.Geometry.sync_item_positions()
res = tool.Model.import_profile((profile := item.SweptArea), obj=obj)
if res is None:
self.report(
{"INFO"},
f"Couldn't import profile, editing it directly is not yet supported. Failing profile: {profile}.",
)
return
tool.Ifc.link(item, obj.data)
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif item.is_a("IfcAnnotationFillArea"):
tool.Model.import_annotation_fill_area(item, obj=obj)
tool.Ifc.link(item, obj.data)
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif tool.Geometry.is_curvelike_item(item):
tool.Model.import_curve(item, obj=obj)
tool.Ifc.link(item, obj.data)
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
else:
self.report({"INFO"}, f"Editing {item.is_a()} geometry is not supported")
def enable_edit_mode(self, context: bpy.types.Context) -> Union[None, set[str]]:
if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}:
return {"CANCELLED"}
props = tool.Geometry.get_geometry_props()
props.is_changing_mode = True
if props.mode != "EDIT":
props.mode = "EDIT"
props.is_changing_mode = False
def has_aggregates(self, objs):
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
parts = ifcopenshell.util.element.get_parts(element)
props = tool.Aggregate.get_aggregate_props()
if (aggregate or parts) and not props.in_aggregate_mode:
return True
elif element != tool.Ifc.get_entity(props.editing_aggregate) and aggregate != tool.Ifc.get_entity(
props.editing_aggregate
):
return True
elif parts and aggregate == tool.Ifc.get_entity(props.editing_aggregate):
return True
else:
return False
def has_nests(self, objs):
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
nest = ifcopenshell.util.element.get_nest(element)
components = ifcopenshell.util.element.get_components(element)
props = tool.Nest.get_nest_props()
if (nest or components) and not props.in_nest_mode:
return True
else:
return False
class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Switch from Edit to Item or Object mode"
bl_idname = "bim.override_mode_set_object"
bl_label = "IFC Mode Set Object"
bl_options = {"REGISTER", "UNDO"}
should_save: bpy.props.BoolProperty(name="Should Save", default=True)
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
def _invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
if not tool.Ifc.get():
return tool.Blender.toggle_edit_mode(context)
if not context.active_object:
return {"FINISHED"}
self.is_valid = True
tool.Blender.toggle_edit_mode(context)
props = tool.Geometry.get_geometry_props()
props.is_changing_mode = True
if props.representation_obj:
if props.mode != "ITEM":
props.mode = "ITEM"
else:
if props.mode != "OBJECT":
props.mode = "OBJECT"
props.is_changing_mode = False
if context.active_object and self.should_save:
element = tool.Ifc.get_entity(context.active_object)
if element and element.is_a("IfcRelSpaceBoundary"):
return bpy.ops.bim.edit_boundary_geometry()
elif tool.Geometry.is_representation_item(context.active_object):
self.edit_representation_item(context.active_object)
tool.Root.reload_item_decorator()
# So you can keep hitting tab to cycle out of edit mode
context.active_object.select_set(False)
bpy.context.view_layer.objects.active = None
return {"FINISHED"}
objs = context.selected_objects or [context.active_object]
self.edited_objs = []
self.unchanged_objs_with_openings = []
for obj in objs:
if not obj:
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
if tool.Profile.is_editing_profile():
props = tool.Profile.get_profile_props()
profile_id = props.active_profile_id
if profile_id:
profile = tool.Ifc.get().by_id(profile_id)
if tool.Ifc.get_object(profile): # We are editing an arbitrary profile
bpy.ops.bim.edit_arbitrary_profile()
elif tool.Blender.Modifier.is_railing(element):
bpy.ops.bim.finish_editing_railing_path()
elif tool.Blender.Modifier.is_roof(element):
bpy.ops.bim.finish_editing_roof_path()
elif tool.Model.get_usage_type(element) == "PROFILE":
bpy.ops.bim.edit_extrusion_axis()
# if in the process of editing arbitrary profile
elif props.active_arbitrary_profile_id:
bpy.ops.bim.edit_arbitrary_profile()
else:
bpy.ops.bim.edit_extrusion_profile()
return self.execute(context)
elif representation := tool.Geometry.get_active_representation(obj):
if not tool.Geometry.is_geometric_data(obj.data):
self.is_valid = False
self.should_save = False
assert tool.Geometry.has_mesh_properties(obj.data)
mesh_props = tool.Geometry.get_mesh_props(obj.data)
if tool.Geometry.is_meshlike(
representation
) and mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
elif getattr(element, "HasOpenings", None):
self.unchanged_objs_with_openings.append(obj)
else:
tool.Ifc.finish_edit(obj)
elif element.is_a("IfcGridAxis"):
if not tool.Geometry.is_geometric_data(obj.data):
self.is_valid = False
self.should_save = False
assert tool.Geometry.has_mesh_properties(obj.data)
mesh_props = tool.Geometry.get_mesh_props(obj.data)
if mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
else:
tool.Ifc.finish_edit(obj)
return self.execute(context)
def _execute(self, context):
if not context.active_object:
return {"FINISHED"}
for obj in self.edited_objs:
if self.should_save:
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
if getattr(tool.Ifc.get_entity(obj), "HasOpenings", False):
tool.Geometry.reload_representation(obj)
else:
tool.Geometry.reload_representation(obj)
tool.Ifc.finish_edit(obj)
for obj in self.unchanged_objs_with_openings:
tool.Geometry.reload_representation(obj)
tool.Ifc.finish_edit(obj)
return {"FINISHED"}
def edit_representation_item(self, obj: bpy.types.Object) -> None:
props = tool.Geometry.get_geometry_props()
item = tool.Geometry.get_active_representation(obj)
assert item
if tool.Geometry.is_meshlike_item(item):
if tool.Geometry.is_geometric_data(obj.data) and (
item.is_a("IfcVertex") or item.is_a("IfcEdge") or obj.data.polygons
):
tool.Geometry.edit_meshlike_item(obj)
else:
tool.Geometry.import_item(obj)
elif item.is_a("IfcSweptAreaSolid"):
ProfileDecorator.uninstall()
if not (profile := tool.Model.export_profile(obj)):
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
ProfileDecorator.install(bpy.context)
self.enable_edit_mode(bpy.context)
return
old_profile = item.SweptArea
profile.ProfileName = old_profile.ProfileName
for inverse in tool.Ifc.get().get_inverse(old_profile):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id())
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
element = tool.Ifc.get_entity(props.representation_obj)
# Only certain classes should have a footprint
if element.is_a() in ("IfcSlab", "IfcRamp"):
footprint_context = ifcopenshell.util.representation.get_context(
tool.Ifc.get(), "Plan", "FootPrint", "SKETCH_VIEW"
)
if footprint_context:
if profile.is_a("IfcCompositeProfileDef"):
profiles = profile.Profiles
else:
profiles = [profile]
curves = []
for profile in profiles:
curves.append(profile.OuterCurve)
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
curves.extend(profile.InnerCurves)
new_footprint = ifcopenshell.api.geometry.add_footprint_representation(
tool.Ifc.get(),
context=footprint_context,
curves=curves,
)
old_footprint = ifcopenshell.util.representation.get_representation(
element, "Plan", "FootPrint", "SKETCH_VIEW"
)
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
bonsai.core.geometry.remove_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint
)
else:
ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(),
product=element,
representation=new_footprint,
)
elif item.is_a("IfcAnnotationFillArea"):
ProfileDecorator.uninstall()
if not (profile := tool.Model.export_annotation_fill_area(obj)):
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
ProfileDecorator.install(bpy.context)
self.enable_edit_mode(bpy.context)
return
for inverse in tool.Ifc.get().get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, profile)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
tool.Ifc.link(profile, obj.data)
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
elif tool.Geometry.is_curvelike_item(item):
ProfileDecorator.uninstall()
new = tool.Model.export_curves(obj)
if not new:
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
ProfileDecorator.install(bpy.context)
self.enable_edit_mode(bpy.context)
return
additional_curves = []
if len(new) > 1:
additional_curves = new[1:]
new = new[0]
for inverse in tool.Ifc.get().get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, new)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
tool.Ifc.link(new, obj.data)
tool.Geometry.import_item(obj)
for item in additional_curves:
representation = tool.Geometry.get_active_representation(props.representation_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
representation.Items = list(representation.Items) + [item]
mesh = bpy.data.meshes.new("temp")
new_obj = bpy.data.objects.new("temp", mesh)
tool.Geometry.name_item_object(new_obj, item)
tool.Ifc.link(item, mesh)
bpy.context.collection.objects.link(new_obj)
props.add_item_object(new_obj, item)
new_obj.matrix_world = obj.matrix_world
tool.Geometry.import_item(new_obj)
if additional_curves:
tool.Root.reload_item_decorator()
tool.Geometry.reload_representation(props.representation_obj)
def enable_edit_mode(self, context):
if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}:
return {"CANCELLED"}
props = tool.Geometry.get_geometry_props()
props.is_changing_mode = True
if props.mode != "EDIT":
props.mode = "EDIT"
props.is_changing_mode = False
class FlipObject(bpy.types.Operator):
bl_idname = "bim.flip_object"
bl_label = "Flip Object"
bl_description = "Flip Element about its local axes, keep the position"
bl_options = {"REGISTER", "UNDO"}
flip_local_axes: bpy.props.EnumProperty(
name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY"
)
def execute(self, context):
for obj in context.selected_objects:
tool.Geometry.flip_object(obj, self.flip_local_axes)
return {"FINISHED"}
class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_representation_items"
bl_label = "Enable Editing Representation Items"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing = True
props.items.clear()
def add_tag(item, tag: str) -> None:
if item.tags:
item.tags += ","
item.tags += tag
if tool.Geometry.has_mesh_properties(data := obj.data):
representation = tool.Geometry.get_data_representation(data)
assert representation
# Shape aspects must be considered from the PartOfProductDefinitionShape level
element = tool.Ifc.get_entity(obj)
assert element
product_reps = []
if element.is_a("IfcProduct"):
product_reps = [element.Representation]
if element_type := ifcopenshell.util.element.get_type(element):
product_reps.extend(element_type.RepresentationMaps or [])
elif element.is_a("IfcTypeProduct"):
product_reps = element.RepresentationMaps
item_aspect = {}
for product_rep in product_reps:
for aspect in product_rep.HasShapeAspects:
for aspect_rep in aspect.ShapeRepresentations:
if aspect_rep.ContextOfItems != representation.ContextOfItems:
continue
for item in aspect_rep.Items:
item_aspect[item] = aspect
# IfcShapeRepresentation or IfcTopologyRepresentation.
if not representation.is_a("IfcShapeModel"):
return
queue = list(representation.Items)
while queue:
item = queue.pop()
if item.is_a("IfcMappedItem"):
queue.extend(item.MappingSource.MappedRepresentation.Items)
else:
new = props.items.add()
new.name = item.is_a()
new.ifc_definition_id = item.id()
styles = []
for inverse in tool.Ifc.get().get_inverse(item):
if inverse.is_a("IfcStyledItem"):
styles = inverse.Styles
if styles and styles[0].is_a("IfcPresentationStyleAssignment"):
styles = styles[0].Styles
for style in styles:
if style.is_a("IfcSurfaceStyle"):
new.surface_style = style.Name or "Unnamed"
new.surface_style_id = style.id()
elif inverse.is_a("IfcPresentationLayerAssignment"):
new.layer = inverse.Name or "Unnamed"
new.layer_id = inverse.id()
elif inverse.is_a("IfcIndexedTextureMap"):
add_tag(new, "UV")
elif inverse.is_a("IfcIndexedColourMap"):
add_tag(new, "Colour")
if aspect := item_aspect.get(item, None):
new.shape_aspect = aspect.Name
new.shape_aspect_id = aspect.id()
# sort created items
sorted_items = sorted(props.items[:], key=lambda i: (not i.shape_aspect, i.shape_aspect))
for i, item in enumerate(sorted_items[:-1]): # last item is sorted automatically
props.items.move(props.items[:].index(item), i)
class DisableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_representation_items"
bl_label = "Disable Editing Representation Items"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
tool.Geometry.get_object_geometry_props(obj).is_editing = False
class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_representation_item"
bl_label = "Remove Representation Item"
bl_options = {"REGISTER", "UNDO"}
representation_item_id: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
if tool.Geometry.get_geometry_props().representation_obj:
return False # Artificial restriction for now to prevent removing when in item mode
if (
not (obj := tool.Geometry.get_active_or_representation_obj())
or len(tool.Geometry.get_object_geometry_props(obj).items) <= 1
):
cls.poll_message_set(
"Active object need to have more than 1 representation items to keep representation valid"
)
return False
return True
def _execute(self, context):
assert (obj := tool.Geometry.get_active_or_representation_obj())
assert (element := tool.Ifc.get_entity(obj))
ifc_file = tool.Ifc.get()
representation_item = ifc_file.by_id(self.representation_item_id)
tool.Geometry.remove_representation_item(representation_item, element)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class SelectRepresentationItem(bpy.types.Operator):
bl_idname = "bim.select_representation_item"
bl_label = "Select Representation Item"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = tool.Geometry.get_geometry_props()
if not props.representation_obj:
cls.poll_message_set("No object opened in item mode.")
return False
return True
def execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
obj_props = tool.Geometry.get_object_geometry_props(obj)
assert obj_props.active_item
item = tool.Ifc.get().by_id(obj_props.active_item.ifc_definition_id)
item_ids = self.get_nested_item_ids(item)
props = tool.Geometry.get_geometry_props()
for item_obj in props.item_objs:
obj_ = item_obj.obj
props = tool.Geometry.get_mesh_props(obj_.data)
if props.ifc_definition_id in item_ids:
tool.Blender.select_object(obj_)
return {"FINISHED"}
def get_nested_item_ids(self, item):
results = set()
if item.is_a("IfcBooleanResult"):
results.update(self.get_nested_item_ids(item.FirstOperand))
results.update(self.get_nested_item_ids(item.SecondOperand))
elif item.is_a("IfcCsgSolid"):
results.update(self.get_nested_item_ids(item.TreeRootExpression))
else:
results.add(item.id())
return results
def poll_editing_representation_item_style(cls, context):
if not (obj := tool.Geometry.get_active_or_representation_obj()):
return False
props = tool.Geometry.get_object_geometry_props(obj)
if not props.is_editing:
return False
if not (item := props.active_item):
return False
shape_aspect = item.shape_aspect
if shape_aspect == "":
return True
from bonsai.bim.module.material.data import ObjectMaterialData
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
constituents = ObjectMaterialData.data["active_material_constituents"]
if any(c for c in constituents if c == shape_aspect):
cls.poll_message_set(
"Style comes from item's shape aspect related to the material constituent "
"with the same name and cannot be edited on representation item directly - "
"you can change it from the material constituent"
)
return False
return True
class EnableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_representation_item_style"
bl_label = "Enable Editing Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return poll_editing_representation_item_style(cls, context)
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_style = True
ifc_file = tool.Ifc.get()
# set dropdown to currently active style
assert (active_item := props.active_item)
representation_item_id = active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
style = tool.Style.get_representation_item_style(representation_item)
if style:
props.representation_item_style = str(style.id())
class EditRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_representation_item_style"
bl_label = "Edit Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_style = False
ifc_file = tool.Ifc.get()
surface_style_id = tool.Blender.get_enum_safe(props, "representation_item_style")
if surface_style_id in (None, "-"):
surface_style = None
else:
surface_style = ifc_file.by_id(int(props.representation_item_style))
assert (active_item := props.active_item)
representation_item_id = active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
tool.Style.assign_style_to_representation_item(representation_item, surface_style)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class DisableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_representation_item_style"
bl_label = "Disable Editing Representation Item Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_style = False
class UnassignRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_representation_item_style"
bl_label = "Unassign Representation Item Style"
bl_description = "Will remove the style from all selected objects"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return poll_editing_representation_item_style(cls, context)
def _execute(self, context):
active_obj = tool.Geometry.get_active_or_representation_obj()
assert active_obj
active_props = tool.Geometry.get_object_geometry_props(active_obj)
active_props.is_editing_item_style = False
# Get active representation item
active_representation_item_id = active_props.active_item.ifc_definition_id
active_representation_item = tool.Ifc.get_entity_by_id(active_representation_item_id)
if not active_representation_item:
self.report({"ERROR"}, f"Couldn't find representation item by id {active_representation_item_id}.")
return {"CANCELLED"}
# Retrieve styles applied to the active representation item
active_styles = set()
if hasattr(active_representation_item, "StyledByItem"):
for styled_by_item in active_representation_item.StyledByItem:
if hasattr(styled_by_item, "Styles"):
active_styles.update(styled_by_item.Styles)
if not active_styles:
self.report({"ERROR"}, "Couldn't find any styles associated with the active representation item.")
return {"CANCELLED"}
# Unassign matching styles from the active object itself
for style in active_styles:
tool.Style.assign_style_to_representation_item(active_representation_item, None)
tool.Geometry.reload_representation(active_obj)
break # No need to check further if one matching style is found
# Iterate over selected objects and unassign matching styles
for obj in context.selected_objects:
if obj == active_obj:
continue # Skip the active object itself
# Get the IFC entity directly from the object
element = tool.Ifc.get_entity(obj)
if not element:
continue # Skip if no IFC entity is found
representation_item_id = element.Representation.Representations[0].Items[0].id()
representation_item = tool.Ifc.get_entity_by_id(representation_item_id)
if representation_item:
# Retrieve styles for the current representation item
styles = set()
if hasattr(representation_item, "StyledByItem"):
for styled_by_item in representation_item.StyledByItem:
if hasattr(styled_by_item, "Styles"):
styles.update(styled_by_item.Styles)
# Unassign matching styles
for style in styles:
if style in active_styles:
tool.Style.assign_style_to_representation_item(representation_item, None)
tool.Geometry.reload_representation(obj)
break # No need to check further if one matching style is found
# Reload UI items
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class EnableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_representation_item_shape_aspect"
bl_label = "Enable Editing Representation Item Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_shape_aspect = True
# set dropdown to currently active shape aspect
shape_aspect_id = props.active_item.shape_aspect_id
if shape_aspect_id != 0:
props.representation_item_shape_aspect = str(shape_aspect_id)
class EditRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_representation_item_shape_aspect"
bl_label = "Edit Representation Item Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
element = tool.Ifc.get_entity(obj)
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_shape_aspect = False
ifc_file = tool.Ifc.get()
assert props.active_item
representation_item_id = props.active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
if props.representation_item_shape_aspect == "NEW":
active_representation = tool.Geometry.get_active_representation(obj)
# find IfcProductRepresentationSelect based on current representation
if hasattr(element, "Representation"): # IfcProduct
product_shape = element.Representation
else: # IfcTypeProduct
for representation_map in element.RepresentationMaps:
if representation_map.MappedRepresentation == active_representation:
product_shape = representation_map
previous_shape_aspect_id = props.active_item.shape_aspect_id
# will be None if item didn't had a shape aspect
previous_shape_aspect = tool.Ifc.get_entity_by_id(previous_shape_aspect_id)
shape_aspect = tool.Geometry.create_shape_aspect(
product_shape, active_representation, [representation_item], previous_shape_aspect
)
else:
shape_aspect = ifc_file.by_id(int(props.representation_item_shape_aspect))
tool.Geometry.add_representation_item_to_shape_aspect([representation_item], shape_aspect)
# set attributes from UI
shape_aspect_attrs = props.shape_aspect_attrs
shape_aspect.Name = shape_aspect_attrs.name
shape_aspect.Description = shape_aspect_attrs.description
shape_aspect_representation = tool.Geometry.get_shape_aspect_representation_for_item(
shape_aspect, representation_item
)
styles = tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
# TODO this looks wrong to me. In theory styles can be > 1 (e.g. curve
# styles) and then the usecase will assign the wrong style.
tool.Ifc.run(
"style.assign_representation_styles",
shape_representation=shape_aspect_representation,
styles=styles,
)
tool.Geometry.reload_representation(obj)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
class DisableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_representation_item_shape_aspect"
bl_label = "Disable Editing Representation Item Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_shape_aspect = False
class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_representation_item_from_shape_aspect"
bl_label = "Remove Representation Item From Shape Aspect"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
element = tool.Ifc.get_entity(obj)
props = tool.Geometry.get_object_geometry_props(obj)
ifc_file = tool.Ifc.get()
assert props.active_item
representation_item_id = props.active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
shape_aspect = ifc_file.by_id(props.active_item.shape_aspect_id)
# unassign items before removing items as removing items
# might remove shape aspect
if representation_item.StyledByItem:
styles = tool.Geometry.get_shape_aspect_styles(element, shape_aspect, representation_item)
self.remove_styles_from_item(representation_item, styles)
tool.Geometry.reload_representation(obj)
tool.Geometry.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
# reload items ui
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
def remove_styles_from_item(self, representation_item, styles):
ifc_file = tool.Ifc.get()
styled_item = representation_item.StyledByItem[0]
new_styles = [s for s in styled_item.Styles if s not in styles]
styled_item.Styles = new_styles
for style_ in new_styles:
if style_.is_a("IfcPresentationStyleAssignment"):
new_assignment_styles = [s for s in styled_item.Styles if s not in styles]
if not new_assignment_styles:
ifc_file.remove(style_)
else:
style_.Styles = new_assignment_styles
if not styled_item.Styles:
ifc_file.remove(styled_item)
class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.import_representation_items"
bl_label = "Import Representation Items"
bl_description = "Import representation items for the active object."
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
assert obj
props = tool.Geometry.get_geometry_props()
if previous_obj := props.representation_obj:
previous_obj.hide_set(False)
props.representation_obj = obj
obj.hide_set(True)
tool.Geometry.lock_object(obj)
props.is_changing_mode = True
if props.mode != "ITEM":
props.mode = "ITEM"
props.is_changing_mode = False
tool.Loader.load_settings()
data = obj.data
assert data
if "ios_item_ids" in data: # Has faces.
item_ids = data["ios_item_ids"]
elif "ios_edges_item_ids" in data: # Has edges.
item_ids = data["ios_edges_item_ids"]
elif "ios_verts_item_ids" in data: # Has only verts.
item_ids = data["ios_verts_item_ids"]
else:
assert False, "Unexpected mesh type."
if not item_ids:
# It is possible that the user has created a shape that
# IfcOpenShell cannot render (i.e. boolean clipped everything), but
# we still want to edit items. I'm not sure the best way to handle
# this, but for now perhaps we can detect when there are no
# item_ids at all.
representation = tool.Ifc.get_entity(data)
item_ids = [i["item"].id() for i in ifcopenshell.util.representation.resolve_items(representation)]
queue = list(set(item_ids))
processed_ids = set()
boolean_ids = set()
while queue:
item_id = queue.pop()
if item_id in processed_ids:
continue # In theory, an item can be used multiple times (e.g. in a boolean stack).
item = tool.Ifc.get().by_id(item_id)
if item.is_a("IfcBooleanResult"):
queue.append(item.FirstOperand.id())
queue.append(item.SecondOperand.id())
boolean_ids.add(item.SecondOperand.id())
continue
item_mesh = bpy.data.meshes.new("tmp")
tool.Ifc.link(item, item_mesh)
item_obj = bpy.data.objects.new("tmp", item_mesh)
tool.Geometry.name_item_object(item_obj, item)
item_obj.matrix_world = obj.matrix_world
bpy.context.collection.objects.link(item_obj)
props.add_item_object(item_obj, item)
tool.Geometry.import_item(item_obj)
tool.Geometry.import_item_attributes(item_obj)
if not tool.Geometry.is_movable(item):
tool.Geometry.lock_object(item_obj)
item_obj.select_set(True) # so you can quickly hit tab again, for edit mode
context.view_layer.objects.active = item_obj
processed_ids.add(item_id)
tool.Root.reload_item_decorator()
class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_item_attributes"
bl_label = "Update Item Attributes"
bl_description = "Update item attributes in IFC and reload mesh for representation item"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
tool.Geometry.sync_item_positions()
tool.Geometry.update_item_attributes(obj)
tool.Geometry.reload_representation(tool.Geometry.get_geometry_props().representation_obj)
tool.Geometry.import_item(obj)
tool.Root.reload_item_decorator()
class NameProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.name_profile"
bl_label = "Name Profile"
bl_description = "Add name to existing unnamed profile"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
extrusion_item_obj: bpy.props.StringProperty(name="Extrusion Item Object Name")
profile_name: bpy.props.StringProperty(
name="Profile Name",
options={"SKIP_SAVE"},
)
if TYPE_CHECKING:
extrusion_item_obj: str
profile_name: str
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "profile_name")
def _execute(self, context):
if not self.profile_name:
self.report({"INFO"}, "Profile name is not provided")
return {"CANCELLED"}
ifc_file = tool.Ifc.get()
extrusion_item_obj = bpy.data.objects[self.extrusion_item_obj]
mesh_props = tool.Geometry.get_mesh_props(extrusion_item_obj.data)
extrusion = ifc_file.by_id(mesh_props.ifc_definition_id)
assert extrusion.is_a("IfcSweptAreaSolid")
profile = extrusion.SweptArea
profile.ProfileName = self.profile_name
bonsai.bim.handler.refresh_ui_data() # Ensure enum is up to date.
mesh_props.item_profile = str(profile.id())
class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_meshlike_item"
bl_label = "Add Meshlike Item"
bl_options = {"REGISTER", "UNDO"}
shape: bpy.props.StringProperty(name="Shape")
def _execute(self, context):
props = tool.Geometry.get_geometry_props()
mesh = bpy.data.meshes.new("Tmp")
obj = bpy.data.objects.new("Tmp", mesh)
scene = bpy.context.scene
scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bm = bmesh.new()
matrix = props.representation_obj.matrix_world.copy()
matrix.translation = context.scene.cursor.location
matrix = props.representation_obj.matrix_world.inverted() @ matrix
if self.shape == "CUBE":
bmesh.ops.create_cube(bm, matrix=matrix, size=0.5)
elif self.shape == "PLANE":
bmesh.ops.create_grid(bm, matrix=matrix, size=0.5)
elif self.shape == "CIRCLE":
bmesh.ops.create_circle(bm, matrix=matrix, radius=0.25, segments=16, cap_ends=True)
elif self.shape == "UVSPHERE":
bmesh.ops.create_uvsphere(bm, matrix=matrix, radius=0.25, u_segments=16, v_segments=16)
elif self.shape == "ICOSPHERE":
bmesh.ops.create_icosphere(bm, matrix=matrix, radius=0.25, subdivisions=2)
elif self.shape == "CYLINDER":
# Cone is legitimate.
bmesh.ops.create_cone(bm, matrix=matrix, radius1=0.25, radius2=0.25, depth=0.5, segments=16, cap_ends=True)
bm.to_mesh(mesh)
bm.free()
mesh.update()
obj.matrix_world = props.representation_obj.matrix_world
obj.show_in_front = True
tool.Geometry.lock_object(obj)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
rep_obj = tool.Geometry.get_geometry_props().representation_obj
assert rep_obj
assert isinstance(obj.data, bpy.types.Mesh)
verts = tool.Blender.get_verts_coordinates(obj.data.vertices)
verts = verts.astype("d")
if (coordinate_offset := tool.Geometry.get_cartesian_point_offset(rep_obj)) is not None:
verts += coordinate_offset
verts /= unit_scale
faces = [p.vertices[:] for p in obj.data.polygons]
representation = tool.Geometry.get_active_representation(props.representation_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
if tool.Ifc.get().schema == "IFC2X3" or representation.RepresentationType in ("Brep", "AdvancedBrep"):
item = builder.faceted_brep(verts, faces)
else:
item = builder.mesh(verts, faces)
props.add_item_object(obj, item)
representation.Items = list(representation.Items) + [item]
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.name_item_object(obj, item)
tool.Ifc.link(item, obj.data)
tool.Root.reload_item_decorator()
class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_swept_area_solid_item"
bl_label = "Add Swept Area Solid Item"
bl_options = {"REGISTER", "UNDO"}
shape: bpy.props.StringProperty(name="Shape")
def _execute(self, context):
props = tool.Geometry.get_geometry_props()
mesh = bpy.data.meshes.new("Tmp")
obj = bpy.data.objects.new("Tmp", mesh)
scene = bpy.context.scene
scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
matrix = props.representation_obj.matrix_world.copy()
matrix.translation = context.scene.cursor.location
local_matrix = props.representation_obj.matrix_world.inverted() @ matrix
obj.show_in_front = True
obj.matrix_world = matrix
tool.Geometry.record_object_position(obj)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.shape == "CUBE":
curve = builder.rectangle(size=Vector((0.5, 0.5)) / unit_scale)
elif self.shape == "CYLINDER":
curve = builder.circle(radius=0.25 / unit_scale)
item = builder.extrude(
curve,
magnitude=0.5 / unit_scale,
position=local_matrix.translation,
position_x_axis=local_matrix.col[0].to_3d(),
position_z_axis=local_matrix.col[2].to_3d(),
)
representation = tool.Geometry.get_active_representation(props.representation_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
props.add_item_object(obj, item)
representation.Items = list(representation.Items) + [item]
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.name_item_object(obj, item)
assert isinstance(obj.data, bpy.types.Mesh)
tool.Ifc.link(item, obj.data)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
tool.Root.reload_item_decorator()
class AddCurvelikeItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_curvelike_item"
bl_label = "Add Curvelike Item"
bl_options = {"REGISTER", "UNDO"}
CurveShape = Literal["LINE", "CIRCLE", "ELLIPSE"]
shape: bpy.props.EnumProperty(name="Shape", items=[(i, i, i) for i in get_args(CurveShape)])
if TYPE_CHECKING:
shape: CurveShape
def _execute(self, context):
props = tool.Geometry.get_geometry_props()
representation = tool.Geometry.get_active_representation(props.representation_obj)
is_2d = representation.ContextOfItems.ContextType == "Plan"
representation = ifcopenshell.util.representation.resolve_representation(representation)
mesh = bpy.data.meshes.new("Tmp")
obj = bpy.data.objects.new("Tmp", mesh)
scene = bpy.context.scene
scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
tool.Geometry.lock_object(obj)
matrix = props.representation_obj.matrix_world.copy()
matrix.translation = context.scene.cursor.location
local_matrix = props.representation_obj.matrix_world.inverted() @ matrix
obj.show_in_front = True
obj.matrix_world = matrix
tool.Geometry.record_object_position(obj)
ifc_file = tool.Ifc.get()
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
offset = local_matrix.translation.to_2d() / unit_scale
if not is_2d:
offset = offset.to_3d()
if self.shape == "LINE":
if is_2d:
points = [Vector((0, 0)), Vector((0.5 / unit_scale, 0))]
else:
points = [Vector((0, 0, 0)), Vector((0.5 / unit_scale, 0, 0))]
item = builder.polyline(points=points, position_offset=offset)
elif self.shape == "CIRCLE":
item = builder.circle(radius=0.25 / unit_scale, center=offset)
elif self.shape == "ELLIPSE":
item = ifc_file.create_entity(
"IfcEllipse",
Position=builder.create_axis2_placement_2d(),
SemiAxis1=0.25 / unit_scale,
SemiAxis2=0.125 / unit_scale,
)
else:
assert_never(self.shape)
props.add_item_object(obj, item)
representation.Items = list(representation.Items) + [item]
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.name_item_object(obj, item)
assert isinstance(obj.data, bpy.types.Mesh)
tool.Ifc.link(item, obj.data)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
class AddHalfSpaceSolidItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_half_space_solid_item"
bl_label = "Add Half Space Solid Item"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
if (
not tool.Blender.get_selected_objects()
or not (active_obj := tool.Blender.get_active_object())
or not tool.Geometry.is_representation_item(active_obj)
):
# In theory we can have half space solid as a top level item but let's not go there today.
self.report({"ERROR"}, "Select an item to apply the half space solid to.")
return {"CANCELLED"}
props = tool.Geometry.get_geometry_props()
mesh = bpy.data.meshes.new("Tmp")
obj = bpy.data.objects.new("Tmp", mesh)
scene = bpy.context.scene
scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
matrix = props.representation_obj.matrix_world.copy()
matrix.translation = context.scene.cursor.location
local_matrix = props.representation_obj.matrix_world.inverted() @ matrix
obj.show_in_front = True
obj.matrix_world = matrix
tool.Geometry.record_object_position(obj)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
location = np.array(local_matrix.translation) / unit_scale
item = builder.half_space_solid(builder.plane(location=location))
props.add_item_object(obj, item)
representation = tool.Geometry.get_active_representation(props.representation_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
representation.Items = list(representation.Items) + [item]
tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.name_item_object(obj, item)
assert isinstance(obj.data, bpy.types.Mesh)
tool.Ifc.link(item, obj.data)
tool.Geometry.import_item(obj)
# TODO refactor to core and not rely on selection
tool.Blender.select_and_activate_single_object(context, active_obj)
tool.Blender.select_object(obj)
bpy.ops.bim.add_boolean()
class OverrideMoveMacro(bpy.types.Macro):
bl_idname = "bim.override_move_macro"
bl_label = "IFC Move Aggregate"
bl_description = "Move selected items.\n\nAutomatically select all parts of an aggregate/nesting to move."
bl_options = {"REGISTER", "UNDO"}
class OverrideMoveSelect(bpy.types.Operator):
bl_idname = "bim.override_move_select"
bl_label = "IFC Move Select"
bl_description = "Select items for IFC Move Aggregate."
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
# Deep magick from the dawn of time
if tool.Ifc.get():
IfcStore.execute_ifc_operator(self, context)
if self.new_active_obj:
context.view_layer.objects.active = self.new_active_obj
return {"FINISHED"}
return {"FINISHED"}
def _execute(self, context):
# Get filling objects
selection: list[bpy.types.Object] = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
selection.append(obj)
for rel in getattr(element, "HasOpenings", []) or []:
opening = rel.RelatedOpeningElement
for rel2 in getattr(opening, "HasFillings", []) or []:
filling = rel2.RelatedBuildingElement
selection.append(tool.Ifc.get_object(filling))
for obj in selection:
obj.select_set(True)
self.new_active_obj = obj
# Get aggregates
props = tool.Aggregate.get_aggregate_props()
not_editing_objs = [obj for o in props.not_editing_objects if (obj := o.obj)]
aggregates_to_move: list[bpy.types.Object] = []
for obj in context.selected_objects:
self.new_active_obj = None
if obj in not_editing_objs:
obj.select_set(False)
continue
if obj == props.editing_aggregate:
continue
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcElement"):
continue
if parts := ifcopenshell.util.element.get_parts(element):
aggregates_to_move.append(tool.Ifc.get_object(element))
aggregates_to_move.extend(tool.Ifc.get_object(e) for e in tool.Aggregate.get_parts_recursively(element))
continue
# Controls the aggregate level it should consider to move
aggregate = None
if aggregates := tool.Aggregate.get_aggregates_recursively(element):
aggregate = aggregates[-1]
if props.in_aggregate_mode:
current_aggregate_index = aggregates.index(tool.Ifc.get_entity(props.editing_aggregate))
aggregate = aggregates[current_aggregate_index - 1]
if tool.Ifc.get_entity(props.editing_aggregate) == aggregates[0]:
aggregate = aggregates[0]
if not parts and props.in_aggregate_mode and aggregate == tool.Ifc.get_entity(props.editing_aggregate):
continue
if aggregate:
aggregates_to_move.append(tool.Ifc.get_object(aggregate))
obj.select_set(False)
if aggregates_to_move:
for obj in set(aggregates_to_move):
obj.select_set(True)
for part in tool.Aggregate.get_parts_recursively(tool.Ifc.get_entity(obj)):
part_obj = tool.Ifc.get_object(part)
part_obj.select_set(True)
self.new_active_obj = obj
return {"FINISHED"}
# Get nests
props = tool.Nest.get_nest_props()
not_editing_objs = [o.obj for o in props.not_editing_objects]
nests_to_move = []
for obj in context.selected_objects:
self.new_active_obj = None
if obj in not_editing_objs:
obj.select_set(False)
continue
if obj == props.editing_nest:
continue
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcElement"):
continue
components = ifcopenshell.util.element.get_components(element)
if components:
nests_to_move.append(tool.Ifc.get_object(element))
continue
if not components and props.in_nest_mode:
continue
nest = ifcopenshell.util.element.get_nest(element)
if nest:
nests_to_move.append(tool.Ifc.get_object(nest))
obj.select_set(False)
nests_to_move = set(nests_to_move)
if nests_to_move:
for obj in nests_to_move:
obj.select_set(True)
for component in tool.Nest.get_components_recursively(tool.Ifc.get_entity(obj)):
component_obj = tool.Ifc.get_object(component)
component_obj.select_set(True)
self.new_active_obj = obj
return {"FINISHED"}
return {"FINISHED"}
class EditRepresentationItemLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_representation_item_layer"
bl_label = "Edit Representation Item Layer"
bl_description = "Edit presentation layer for the active representation item."
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
ifc_file = tool.Ifc.get()
obj = context.active_object
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
new_layer = ifc_file.by_id(int(props.representation_item_layer))
assert props.active_item
item = ifc_file.by_id(int(props.active_item.ifc_definition_id))
item_layer = next(iter(item.LayerAssignment), None)
# We assume just 1 layer can be assigned to representation item.
if item_layer:
# Same layer is already assigned.
if item_layer == new_layer:
props.is_editing_item_layer = False
return {"FINISHED"}
ifcopenshell.api.layer.unassign_layer(ifc_file, [item], item_layer)
ifcopenshell.api.layer.assign_layer(ifc_file, [item], new_layer)
props.is_editing_item_layer = False
bpy.ops.bim.enable_editing_representation_items()
return {"FINISHED"}
class UnassignRepresentationItemLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_representation_item_layer"
bl_label = "Unassign Representation Item Layer"
bl_description = "Unassign presentation layer from the active representation item."
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
ifc_file = tool.Ifc.get()
obj = context.active_object
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
item = ifc_file.by_id(props.active_item.ifc_definition_id)
layer = item.LayerAssignment[0] # If there is no layer, then button is not visible in UI.
ifcopenshell.api.layer.unassign_layer(ifc_file, [item], layer)
bpy.ops.bim.enable_editing_representation_items()
return {"FINISHED"}
class AssignRepresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_representation_layer"
bl_label = "Assign Representation Layer"
bl_description = "Assign presentation layer to the active representation."
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
ifc_file = tool.Ifc.get()
obj = context.active_object
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
new_layer = ifc_file.by_id(int(props.representation_layer))
representation = tool.Geometry.get_active_representation(obj)
assert representation
item_layers = representation.LayerAssignments
# On practice enum doesn't display already assigned layers
# but let's double check.
if new_layer in item_layers:
props.is_adding_representation_layer = False
return {"FINISHED"}
ifcopenshell.api.layer.assign_layer(ifc_file, [representation], new_layer)
props.is_adding_representation_layer = False
return {"FINISHED"}
class UnassignRepresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_representation_layer"
bl_label = "Unassign Representation Layer"
bl_description = "Unassign presentation layer from the active representation."
bl_options = {"REGISTER", "UNDO"}
layer_id: bpy.props.IntProperty()
def _execute(self, context):
ifc_file = tool.Ifc.get()
obj = context.active_object
assert obj
representation = tool.Geometry.get_active_representation(obj)
assert representation
layer = ifc_file.by_id(self.layer_id)
ifcopenshell.api.layer.unassign_layer(ifc_file, [representation], layer)
return {"FINISHED"}