mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
Fix BRep and opening element mirroring
Three fixes in TrueMirrorElements / _apply_opening_mirror: 1. IfcFacetedBrep support: implement mirroring in mirror_item by negating vertex coordinates along the flipped axes and reversing face winding for an odd number of flipped axes (restores outward normals). 2. Routing: add type_has_reps guard to is_assign_type_path so elements whose IfcTypeProduct carries no RepresentationMaps fall through to direct geometry mirroring instead of a no-op type swap. 3. Opening placement: use H@R@H conjugation (not direct Householder on columns) for the rotation part of _apply_opening_mirror. Direct Householder yields det=-1; IFC's Y=Z×X normalization then introduces a spurious 180°Z rotation. Conjugation keeps det=+1 and produces the correct mirrored rotation. Also call builder.mirror() on each opening representation item so local vertex data is mirrored alongside the placement frame. Generated with the assistance of an AI coding tool. AI effort: 8/10
This commit is contained in:
@@ -740,15 +740,8 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
1.0 if abs(mirror_normal_local.y) > 0.5 else 0.0,
|
||||
1.0 if abs(mirror_normal_local.z) > 0.5 else 0.0,
|
||||
)
|
||||
print(f"[mirror] --- {obj.name} ---")
|
||||
print(f"[mirror] mirror_ref='{mirror_ref.name}'")
|
||||
print(f"[mirror] mirror_ref rotation: {mirror_ref.matrix_world.to_euler()}")
|
||||
print(f"[mirror] mirror normal (world): {mirror_normal_world}")
|
||||
print(f"[mirror] mirror normal (obj local):{mirror_normal_local}")
|
||||
print(f"[mirror] mirror_axes: {mirror_axes}")
|
||||
else:
|
||||
mirror_axes = (1, 0, 0)
|
||||
print(f"[mirror] --- {obj.name} --- no mirror_ref, mirror_axes={mirror_axes}")
|
||||
|
||||
# Snapshot opening world placements AND slab placement BEFORE any geometry or origin changes.
|
||||
# We work in the slab's LOCAL coordinate space so the mirrored relative offset is correct
|
||||
@@ -761,23 +754,15 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
opening = rel.RelatedOpeningElement
|
||||
M = ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement)
|
||||
opening_placements_before[opening.id()] = M.copy()
|
||||
print(f"[mirror] opening #{opening.id()} pre-mirror world pos={M[:3, 3].tolist()}")
|
||||
M_rel = np.linalg.inv(M_slab_before) @ M
|
||||
print(f"[mirror] opening #{opening.id()} pre-mirror slab-local pos={M_rel[:3, 3].tolist()}")
|
||||
|
||||
usage_type = tool.Model.get_usage_type(element)
|
||||
print(f"[mirror] usage_type={usage_type} type_element=#{type_element.id() if type_element else None}")
|
||||
is_assign_type_path = bool(type_element and element.id() != type_element.id() and usage_type != "LAYER3")
|
||||
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)
|
||||
if is_assign_type_path:
|
||||
# obj has a type, use / create inverted type and assign it
|
||||
print(f"[mirror] -> assign_inverted_type path")
|
||||
self.assign_inverted_type(element, mirror_axes)
|
||||
M_elem_after_assign = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
print(f"[mirror] element IFC pos after assign_inverted_type: {M_elem_after_assign[:3, 3].tolist()}")
|
||||
else:
|
||||
# invert representation of entity directly;
|
||||
# LAYER3 (slabs) store geometry on the instance, not the type, so always use this path
|
||||
print(f"[mirror] -> invert_representation path (direct)")
|
||||
# LAYER3 (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:
|
||||
@@ -789,17 +774,14 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if mirror_ref:
|
||||
# Reflect object origin about the mirror reference's YZ plane
|
||||
origin_in_mirror = mirror_ref.matrix_world.inverted() @ obj.matrix_world.translation
|
||||
print(f"[mirror] origin before: {obj.matrix_world.translation}")
|
||||
origin_in_mirror.x *= -1
|
||||
obj.matrix_world.translation = mirror_ref.matrix_world @ origin_in_mirror
|
||||
print(f"[mirror] origin after: {obj.matrix_world.translation}")
|
||||
else:
|
||||
# Fall back: nudge in place to compensate for bounding box shift after inversion
|
||||
mirrored_bb_data = tool.Blender.get_object_bounding_box(obj)
|
||||
x_correction_factor = mirrored_bb_data["min_x"] - bb_data["min_x"]
|
||||
x_correction_vec = (obj.matrix_world @ Vector((x_correction_factor, 0, 0, 0))).xyz
|
||||
obj.location -= x_correction_vec
|
||||
print(f"[mirror] fallback x_correction={x_correction_factor}")
|
||||
|
||||
if element.is_a("IfcElement") and element.FillsVoids:
|
||||
tool.Model.update_simple_openings(element)
|
||||
@@ -817,30 +799,12 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# 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),
|
||||
# the wrong representation will be used.
|
||||
if opening_placements_before:
|
||||
for rel in element.HasOpenings:
|
||||
op = rel.RelatedOpeningElement
|
||||
M_pre_switch = ifcopenshell.util.placement.get_local_placement(op.ObjectPlacement)
|
||||
print(f"[mirror] opening #{op.id()} IFC world PRE switch_representation: {M_pre_switch[:3, 3].tolist()}")
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=ifcopenshell.util.representation.get_representation(element, active_context),
|
||||
)
|
||||
if opening_placements_before:
|
||||
for rel in element.HasOpenings:
|
||||
op = rel.RelatedOpeningElement
|
||||
M_post_switch = ifcopenshell.util.placement.get_local_placement(op.ObjectPlacement)
|
||||
print(f"[mirror] opening #{op.id()} IFC world POST switch_representation: {M_post_switch[:3, 3].tolist()}")
|
||||
op_obj = tool.Ifc.get_object(op)
|
||||
if op_obj:
|
||||
print(f"[mirror] opening #{op.id()} Blender world POST switch_representation: {list(op_obj.matrix_world.translation)}")
|
||||
else:
|
||||
print(f"[mirror] opening #{op.id()} has no Blender object yet")
|
||||
print(f"[mirror] element Blender world POST switch_representation: {list(obj.matrix_world.translation)}")
|
||||
M_elem_post_switch = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
print(f"[mirror] element IFC world POST switch_representation: {M_elem_post_switch[:3, 3].tolist()}")
|
||||
|
||||
def _apply_opening_mirror(self, element, mirror_axes, M_slab_before, opening_placements_before, frame_change):
|
||||
"""Mirror IfcOpeningElement placements in element-local space.
|
||||
@@ -879,15 +843,9 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
M_rel_new[:3, :3] = frame_change @ R_mirrored
|
||||
|
||||
M_abs_new = M_slab_before @ M_rel_new
|
||||
print(f"[mirror] opening #{opening.id()} slab-local: {M_rel[:3, 3].tolist()} -> {M_rel_new[:3, 3].tolist()}")
|
||||
print(f"[mirror] opening #{opening.id()} world: {M_abs_old[:3, 3].tolist()} -> {M_abs_new[:3, 3].tolist()}")
|
||||
print(f"[mirror] opening #{opening.id()} rot_before X={M_rel[:3, 0].tolist()} Z={M_rel[:3, 2].tolist()}")
|
||||
print(f"[mirror] opening #{opening.id()} rot_after X={M_rel_new[:3, 0].tolist()} Z={M_rel_new[:3, 2].tolist()}")
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
tool.Ifc.get(), product=opening, matrix=M_abs_new, is_si=False
|
||||
)
|
||||
M_after = ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement)
|
||||
print(f"[mirror] opening #{opening.id()} IFC world AFTER: pos={M_after[:3, 3].tolist()} X={M_after[:3, 0].tolist()} Z={M_after[:3, 2].tolist()}")
|
||||
|
||||
# Mirror the opening's own representation geometry (local vertices/profile).
|
||||
# edit_object_placement only moves the frame; the local shape must also be
|
||||
@@ -901,48 +859,68 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
for item in rep.Items:
|
||||
try:
|
||||
builder.mirror(item, mirror_axes_2d, create_copy=False)
|
||||
print(f"[mirror] opening #{opening.id()} geometry mirrored: #{item.id()} {item.is_a()}")
|
||||
except Exception as e:
|
||||
print(f"[mirror] opening #{opening.id()} geometry mirror failed for #{item.id()} {item.is_a()}: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def invert_general_object(self, element, mirror_axes=(1, 0, 0)):
|
||||
# ShapeBuilder.mirror works in 2D; use only the XY components
|
||||
mirror_axes_2d = mirror_axes[:2]
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
print(f"[mirror:invert_general] element=#{element.id()} {element.is_a()} mirror_axes={mirror_axes} mirror_axes_2d={mirror_axes_2d}")
|
||||
|
||||
def mirror_item(item, depth=0):
|
||||
indent = " " * depth
|
||||
print(f"[mirror:invert_general] {indent}mirror_item #{item.id()} type={item.is_a()}")
|
||||
def mirror_item(item):
|
||||
if item.is_a("IfcBooleanResult"):
|
||||
print(f"[mirror:invert_general] {indent} -> IfcBooleanResult operator={item.Operator}")
|
||||
print(f"[mirror:invert_general] {indent} -> FirstOperand #{item.FirstOperand.id()} {item.FirstOperand.is_a()}")
|
||||
mirror_item(item.FirstOperand, depth + 1)
|
||||
print(f"[mirror:invert_general] {indent} -> SecondOperand #{item.SecondOperand.id()} {item.SecondOperand.is_a()}")
|
||||
mirror_item(item.FirstOperand)
|
||||
try:
|
||||
mirror_item(item.SecondOperand, depth + 1)
|
||||
except Exception as e:
|
||||
print(f"[mirror:invert_general] {indent} -> SecondOperand skipped: {e}")
|
||||
mirror_item(item.SecondOperand)
|
||||
except Exception:
|
||||
pass
|
||||
elif item.is_a("IfcFacetedBrep") or item.is_a("IfcFacetedBrepWithVoids"):
|
||||
shells = [item.Outer]
|
||||
if item.is_a("IfcFacetedBrepWithVoids"):
|
||||
shells.extend(item.Voids)
|
||||
|
||||
# Mirror all unique vertex coordinates along the flipped axes
|
||||
points_done = set()
|
||||
for shell in shells:
|
||||
for face in shell.CfsFaces:
|
||||
for bound in face.Bounds:
|
||||
if not bound.Bound.is_a("IfcPolyLoop"):
|
||||
continue
|
||||
for pt in bound.Bound.Polygon:
|
||||
if pt.id() in points_done:
|
||||
continue
|
||||
points_done.add(pt.id())
|
||||
coords = list(pt.Coordinates)
|
||||
for i, flip in enumerate(mirror_axes[: len(coords)]):
|
||||
if flip > 0.0:
|
||||
coords[i] = -coords[i]
|
||||
pt.Coordinates = coords
|
||||
|
||||
# Reverse face winding for an odd number of flipped axes (restores outward normals)
|
||||
num_flipped = sum(1 for v in mirror_axes if v > 0.0)
|
||||
if num_flipped % 2 == 1:
|
||||
for shell in shells:
|
||||
for face in shell.CfsFaces:
|
||||
for bound in face.Bounds:
|
||||
if bound.Bound.is_a("IfcPolyLoop"):
|
||||
bound.Bound.Polygon = list(reversed(bound.Bound.Polygon))
|
||||
elif item.is_a("IfcMappedItem"):
|
||||
for sub in item.MappingSource.MappedRepresentation.Items:
|
||||
mirror_item(sub)
|
||||
else:
|
||||
print(f"[mirror:invert_general] {indent} -> calling builder.mirror()")
|
||||
builder.mirror(item, mirror_axes_2d, create_copy=False)
|
||||
|
||||
if element.is_a("IfcProduct"):
|
||||
if not element.Representation:
|
||||
print(f"[mirror:invert_general] element has no Representation, skipping")
|
||||
return
|
||||
|
||||
for representation in element.Representation.Representations:
|
||||
print(f"[mirror:invert_general] representation #{representation.id()} context={representation.ContextOfItems.ContextIdentifier!r} type={representation.RepresentationType!r} items={len(representation.Items)}")
|
||||
for item in representation.Items:
|
||||
mirror_item(item)
|
||||
elif element.is_a("IfcTypeProduct"):
|
||||
for representation_map in (element.RepresentationMaps or []):
|
||||
print(f"[mirror:invert_general] representation_map #{representation_map.id()} items={len(representation_map.MappedRepresentation.Items)}")
|
||||
for item in representation_map.MappedRepresentation.Items:
|
||||
mirror_item(item)
|
||||
|
||||
print(f"[mirror:invert_general] reloading representation for #{element.id()}")
|
||||
tool.Geometry.reload_representation(tool.Ifc.get_object(element))
|
||||
|
||||
def invert_door_swing(self, element):
|
||||
@@ -995,9 +973,6 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
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"
|
||||
print(f"[mirror] created new inverted type '{inverted_type.Name}'")
|
||||
else:
|
||||
print(f"[mirror] reusing cached type '{inverted_type.Name}', mirror_axes={mirror_axes}")
|
||||
|
||||
bonsai.core.type.assign_type(tool.Ifc, tool.Model, tool.Type, element, inverted_type)
|
||||
|
||||
@@ -1010,14 +985,12 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||
rot_180_z = Matrix.Rotation(math.pi, 4, "Z")
|
||||
obj.matrix_world = rot_180_z @ obj.matrix_world
|
||||
obj.matrix_world.translation = loc
|
||||
print(f"[mirror] applied 180° Z rotation to compensate for Y-flip")
|
||||
elif mirror_axes == (0.0, 0.0, 1.0):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
loc = obj.matrix_world.translation.copy()
|
||||
rot_180_y = Matrix.Rotation(math.pi, 4, "Y")
|
||||
obj.matrix_world = rot_180_y @ obj.matrix_world
|
||||
obj.matrix_world.translation = loc
|
||||
print(f"[mirror] applied 180° Y rotation to compensate for Z-flip")
|
||||
|
||||
|
||||
def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||
|
||||
Reference in New Issue
Block a user