mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
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:
@@ -19,6 +19,7 @@
|
||||
# pyright: reportUnnecessaryTypeIgnoreComment=error
|
||||
|
||||
import json
|
||||
import math
|
||||
from typing import TYPE_CHECKING, Any, Literal, get_args
|
||||
|
||||
import bmesh
|
||||
@@ -727,12 +728,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()
|
||||
|
||||
@@ -761,7 +774,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
|
||||
@@ -769,12 +782,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))
|
||||
|
||||
@@ -811,26 +824,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
|
||||
|
||||
|
||||
def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||
box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW")
|
||||
|
||||
Reference in New Issue
Block a user