Fix mirror axis mismatch when ref object is rotated

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.
This commit is contained in:
Ryan Schultz
2026-03-10 21:42:43 -05:00
parent 916c7596a7
commit e4ca42001a
+30 -9
View File
@@ -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"