From 521fc63c48ef2bb40f6ba4513a2c88153b6c4361 Mon Sep 17 00:00:00 2001 From: Robin Quint Date: Thu, 7 Aug 2025 16:03:10 +0200 Subject: [PATCH] Added bim.mirror_geometry() operator for truly mirroring products / types --- .../bonsai/bim/module/model/__init__.py | 2 +- src/bonsai/bonsai/bim/module/model/door.py | 66 ++++++++++++++++++- src/bonsai/bonsai/bim/module/model/product.py | 48 ++++++++++---- .../bonsai/bim/module/model/workspace.py | 2 +- 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index bb76ca298a..dbcc2a9d74 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -82,8 +82,8 @@ classes = ( product.DrawOccurrence, product.LoadTypeThumbnails, product.MirrorElements, + product.TrueMirrorElements, product.SetActiveType, - product.ChangeSwingDirection, workspace.Hotkey, workspace.BIM_MT_add_representation_item, wall.AddPerpendicularWall, diff --git a/src/bonsai/bonsai/bim/module/model/door.py b/src/bonsai/bonsai/bim/module/model/door.py index ba27524007..e888adb09c 100644 --- a/src/bonsai/bonsai/bim/module/model/door.py +++ b/src/bonsai/bonsai/bim/module/model/door.py @@ -608,8 +608,70 @@ class FinishEditingDoor(_DoorEditMixin, bpy.types.Operator, tool.Ifc.Operator): bl_description = "Apply changes and finish editing door parameters" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context: bpy.types.Context) -> set[str]: - return self._finish_targets(context) + def finish_editing_door_on_object(self, obj: bpy.types.Object) -> None: + element = tool.Ifc.get_entity(obj) + assert element + if not tool.Blender.Modifier.is_door(element): + return + props = tool.Model.get_door_props(obj) + + door_data = props.get_general_kwargs(convert_to_project_units=True) + lining_props = props.get_lining_kwargs(convert_to_project_units=True) + panel_props = props.get_panel_kwargs(convert_to_project_units=True) + + door_data["lining_properties"] = lining_props + door_data["panel_properties"] = panel_props + + props.is_editing = False + + update_door_modifier_representation(obj) + element_type = ifcopenshell.util.element.get_type(element) + if element_type: + tool.Model.mark_thumbnail_for_update(element_type) + + pset = tool.Pset.get_element_pset(element, "BBIM_Door") + door_data_str = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list)) + ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": door_data_str}) + + if inverted_pset := ifcopenshell.util.element.get_pset(element, "BBIM_InvertedSwingType", "Data"): + inverted_data = json.loads(inverted_pset) + if "inverted_swing_type" in inverted_data and (inverted_type := tool.Ifc.get_entity_by_id(int(inverted_data["inverted_swing_type"]))): + # object has mirrored repr, update it as well + self.copy_door_params(door_data, inverted_type) + + def copy_door_params(self, from_data, to_elem): + if "RIGHT" in from_data["door_type"]: + from_data["door_type"] = from_data["door_type"].replace("RIGHT", "LEFT") + else: + from_data["door_type"] = from_data["door_type"].replace("LEFT", "RIGHT") + + to_pset = tool.Pset.get_element_pset(to_elem, "BBIM_Door") + ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=to_pset, properties={"Data": json.dumps(from_data)}) + + # reload door representation + from_data.update(from_data.pop("lining_properties")) + from_data.update(from_data.pop("panel_properties")) + from_data.update(tool.Model.get_constituents_props_data(to_elem)) + + to_obj = tool.Ifc.get_object(to_elem) + props = tool.Model.get_door_props(to_obj) + + # we need this workaround because set_props_kwargs_from_ifc_data will + # "update" the mesh of the active object, which will switch its representation + prev_active = bpy.context.view_layer.objects.active + bpy.context.view_layer.objects.active = to_obj + + props.set_props_kwargs_from_ifc_data(from_data) + + bpy.context.view_layer.objects.active = prev_active + + update_door_modifier_representation(to_obj) + tool.Model.mark_thumbnail_for_update(to_elem) + + def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002 + for obj in tool.Blender.get_selected_objects(): + self.finish_editing_door_on_object(obj) + return {"FINISHED"} class EnableEditingDoor(_DoorEditMixin, bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 545acf85fa..7fd0fd2704 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -679,11 +679,11 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator): obj.matrix_world = newmat -class ChangeSwingDirection(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.change_swing_direction" - bl_label = "Change door / window swing direction" +class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.mirror_geometry" + bl_label = "Mirror Element Geometry" bl_options = {"REGISTER", "UNDO"} - bl_description = "Changes the swing direction of the selection door / window" + bl_description = "Mirrors the selected objects by mirroring their representation (or their types representation)" @classmethod def poll(cls, context): @@ -703,14 +703,11 @@ class ChangeSwingDirection(bpy.types.Operator, tool.Ifc.Operator): active_context = tool.Geometry.get_active_representation_context(obj) if type_element: - # obj has a type, try to change swing direction of type - if (type_pset := ifcopenshell.util.element.get_pset(type_element, "BBIM_Door", "Data")): - # get or create inverted type and assign it - self.assign_inverted_door_type(element) + # obj has a type, use / create inverted type and assign it + self.assign_inverted_type(element) else: - if (obj_pset := ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data", should_inherit=False)): - # obj has an own geometric door representation, simply flip the door_type - self.invert_door_swing(element) + # invert representation of entity directly + self.invert_representation(element) # bonsai does not automatically switch to the representation that should be active in the given context # when switching to a type that was previously viewed in another context (e.g. plan view), @@ -725,6 +722,23 @@ class ChangeSwingDirection(bpy.types.Operator, tool.Ifc.Operator): should_sync_changes_first=False, ) + def invert_general_object(self, element): + if element.is_a("IfcProduct"): + if not element.Representation: + return + + for representation in element.Representation.Representations: + for item in representation.Items: + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + builder.mirror(item, (1, 0), create_copy=False) + elif element.is_a("IfcTypeProduct"): + for representation_map in element.RepresentationMaps: + for item in representation_map.MappedRepresentation.Items: + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + builder.mirror(item, (1, 0), create_copy=False) + + tool.Geometry.reload_representation(tool.Ifc.get_object(element)) + def invert_door_swing(self, element): obj = tool.Ifc.get_object(element) @@ -758,6 +772,12 @@ class ChangeSwingDirection(bpy.types.Operator, tool.Ifc.Operator): tool.Model.mark_thumbnail_for_update(element) + def invert_representation(self, element): + if ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"): + self.invert_door_swing(element) + else: + self.invert_general_object(element) + def get_inverted_type(self, type_element): inverted_pset = ifcopenshell.util.element.get_pset(type_element, "BBIM_InvertedSwingType", "Data") if not inverted_pset: @@ -775,17 +795,17 @@ class ChangeSwingDirection(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), inverted_pset, "BBIM_InvertedSwingType", { "Data": json.dumps({ "inverted_swing_type": inverted_type_element.id() }) }) - def assign_inverted_door_type(self, element): + def assign_inverted_type(self, element): type_element = ifcopenshell.util.element.get_type(element) inverted_type = self.get_inverted_type(type_element) if not inverted_type: old_to_new, _ = tool.Geometry.duplicate_ifc_objects([ tool.Ifc.get_object(type_element) ]) inverted_type = old_to_new[type_element][0] - self.invert_door_swing(inverted_type) + self.invert_representation(inverted_type) self.set_inverted_type(inverted_type, type_element) self.set_inverted_type(type_element, inverted_type) - + bonsai.core.type.assign_type(tool.Ifc, tool.Type, element, inverted_type) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 869eecbdfd..86d6bc77ea 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1296,7 +1296,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): def hotkey_S_C_F(self): if not bpy.context.selected_objects: return - bpy.ops.bim.change_swing_direction() + bpy.ops.bim.mirror_geometry() def hotkey_S_G(self): obj = bpy.context.active_object