Add LAYER2 wall mirror support

Three changes:

1. Mirror LAYER2 (wall) geometry correctly: exclude LAYER2 from the
   assign_inverted_type path (which only flips the type geometry and
   leaves the instance body unchanged) and route it through
   invert_representation like LAYER3 slabs.  For Y-axis flips also
   invert DirectionSense and negate OffsetFromReferenceLine on the
   IfcMaterialLayerSetUsage so regenerated walls stay correct.

2. Surface bim.mirror_geometry in the right-click context menu and
   Object menu so it is reachable without activating the BIM Tool.

3. Guard bpy.ops.bim.extend_to_underside.__doc__ with try/except so a
   missing operator (branch without that feature) does not crash the
   entire tool-header draw and hide all other buttons.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Ryan Schultz
2026-05-02 13:00:12 -05:00
parent 98ea6a7daf
commit 643aa6303f
+15 -2
View File
@@ -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()