From e4ca42001a0b226f45cd159555411248ae454147 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Tue, 10 Mar 2026 21:42:43 -0500 Subject: [PATCH] Fix mirror axis mismatch when ref object is rotated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The geometry inversion was hardcoded to the object's local X axis regardless of the mirror reference orientation. When the reference was rotated (e.g. a wall at 90°), the inversion axis and the position reflection were perpendicular, producing a rotation artefact. Fix by computing the mirror plane normal in the object's local space and passing it as the inversion axis for untyped elements. For typed elements, the cached inverted type is always X-flipped; a 180° Z rotation is applied to the object when a Y-flip is needed instead (flip_Y = Rotate_Z_180 ∘ flip_X). Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/bim/module/model/product.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 70701a4408..00657cd0c5 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -730,12 +730,24 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator): bb_data = tool.Blender.get_object_bounding_box(obj) + # Compute the geometry inversion axis from the mirror reference's local X (= mirror plane normal). + # Without a reference, always fall back to inverting along the object's local X. + if mirror_ref: + mirror_normal_world = mirror_ref.matrix_world.to_3x3().col[0].normalized() + mirror_normal_local = obj.matrix_world.to_3x3().inverted() @ mirror_normal_world + mirror_axes = ( + 1.0 if abs(mirror_normal_local.x) > 0.5 else 0.0, + 1.0 if abs(mirror_normal_local.y) > 0.5 else 0.0, + ) + else: + mirror_axes = (1, 0) + if type_element and element.id() != type_element.id(): # obj has a type, use / create inverted type and assign it - self.assign_inverted_type(element) + self.assign_inverted_type(element, mirror_axes) else: # invert representation of entity directly - self.invert_representation(element) + self.invert_representation(element, mirror_axes) context.view_layer.update() @@ -764,7 +776,7 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator): representation=ifcopenshell.util.representation.get_representation(element, active_context), ) - def invert_general_object(self, element): + def invert_general_object(self, element, mirror_axes=(1, 0)): if element.is_a("IfcProduct"): if not element.Representation: return @@ -772,12 +784,12 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator): 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) + builder.mirror(item, mirror_axes, create_copy=False) elif element.is_a("IfcTypeProduct"): for representation_map in (element.RepresentationMaps or []): for item in representation_map.MappedRepresentation.Items: builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) - builder.mirror(item, (1, 0), create_copy=False) + builder.mirror(item, mirror_axes, create_copy=False) tool.Geometry.reload_representation(tool.Ifc.get_object(element)) @@ -814,26 +826,35 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator): tool.Model.mark_thumbnail_for_update(element) - def invert_representation(self, element): + def invert_representation(self, element, mirror_axes=(1, 0)): if ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data"): self.invert_door_swing(element) else: - self.invert_general_object(element) + self.invert_general_object(element, mirror_axes) - def assign_inverted_type(self, element): + def assign_inverted_type(self, element, mirror_axes=(1, 0)): type_element = ifcopenshell.util.element.get_type(element) inverted_type = tool.Blender.Modifier.has_mirrored_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_representation(inverted_type) + self.invert_representation(inverted_type) # always X-flip for the cached type tool.Blender.Modifier.set_mirrored_type(inverted_type, type_element) tool.Blender.Modifier.set_mirrored_type(type_element, inverted_type) inverted_type.Name = f"{inverted_type.Name}.Mirror" bonsai.core.type.assign_type(tool.Ifc, tool.Model, tool.Type, element, inverted_type) + # The cached type is always X-flipped. If we need Y-flip, rotate 180° around Z to compensate + # (flip_Y = Rotate_Z_180 ∘ flip_X) + if mirror_axes == (0.0, 1.0): + obj = tool.Ifc.get_object(element) + loc = obj.matrix_world.translation.copy() + rot_180_z = Matrix.Rotation(math.pi, 4, "Z") + obj.matrix_world = rot_180_z @ obj.matrix_world + obj.matrix_world.translation = loc + class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.mirror_geometry"