diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index dbcc2a9d74..a4dcd28ded 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -367,6 +367,8 @@ def register(): ) bpy.types.VIEW3D_MT_add.prepend(ui.add_menu) + bpy.types.VIEW3D_MT_object_context_menu.append(ui.object_menu) + bpy.types.VIEW3D_MT_object.append(ui.object_menu) bpy.app.handlers.load_post.append(handler.load_post) workspace.load_custom_icons() @@ -399,5 +401,7 @@ def unregister(): bpy.app.handlers.load_post.remove(handler.load_post) bpy.types.VIEW3D_MT_add.remove(ui.add_menu) + bpy.types.VIEW3D_MT_object_context_menu.remove(ui.object_menu) + bpy.types.VIEW3D_MT_object.remove(ui.object_menu) workspace.unload_custom_icons() diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index dcf815a935..496dcf063b 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -757,17 +757,30 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator): usage_type = tool.Model.get_usage_type(element) type_has_reps = bool(type_element and (type_element.RepresentationMaps or [])) - is_assign_type_path = bool(type_element and element.id() != type_element.id() and usage_type != "LAYER3" and type_has_reps) + # LAYER2 (walls) and LAYER3 (slabs) generate instance-specific bodies via DumbWallGenerator / + # DumbSlabGenerator rather than mapping the type's RepresentationMaps. assign_inverted_type + # only flips the *type* geometry and would leave the instance body unchanged, so both layer + # usage types must go through invert_representation instead. + is_assign_type_path = bool(type_element and element.id() != type_element.id() and usage_type not in ("LAYER2", "LAYER3") and type_has_reps) if is_assign_type_path: self.assign_inverted_type(element, mirror_axes) else: # invert representation of entity directly; - # LAYER3 (slabs) and elements whose type carries no geometry use this path + # LAYER2/LAYER3 (walls/slabs) and elements whose type carries no geometry use this path # Update opening placements BEFORE invert_representation so its internal reload # sees the correct positions. No element rotation change on this path → frame_change = I. if opening_placements_before and M_slab_before is not None: self._apply_opening_mirror(element, mirror_axes, M_slab_before, opening_placements_before, np.eye(3)) self.invert_representation(element, mirror_axes) + # For LAYER2 walls, layers stack along local Y (LayerSetDirection=AXIS2). A Y-axis flip + # reverses that direction, so DirectionSense and OffsetFromReferenceLine must both invert. + # (X/Z flips don't touch local Y, and the assign_inverted_type path handles Y-mirror via + # a 180°Z element rotation which implicitly flips local Y without changing the IFC attribute.) + if usage_type == "LAYER2" and mirror_axes[1] > 0.5: + mat_usage = ifcopenshell.util.element.get_material(element, should_inherit=False) + if mat_usage and mat_usage.is_a("IfcMaterialLayerSetUsage"): + mat_usage.DirectionSense = "NEGATIVE" if mat_usage.DirectionSense == "POSITIVE" else "POSITIVE" + mat_usage.OffsetFromReferenceLine = -mat_usage.OffsetFromReferenceLine context.view_layer.update() diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index e2b73dfe3d..ffc47669bf 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -189,6 +189,12 @@ class LaunchTypeManager(bpy.types.Operator): op.relating_type = relating_type["id"] +def object_menu(self, context): + if context.active_object and tool.Ifc.get_entity(context.active_object): + self.layout.separator() + self.layout.operator("bim.mirror_geometry", icon="MOD_MIRROR") + + class BIM_PT_authoring(Panel): bl_label = "Architectural" bl_idname = "BIM_PT_authoring"