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 b6476b764e
commit 60a0411e96
3 changed files with 25 additions and 2 deletions
@@ -367,6 +367,8 @@ def register():
)
bpy.types.VIEW3D_MT_add.prepend(ui.add_menu)
bpy.types.VIEW3D_MT_object_context_menu.append(ui.object_menu)
bpy.types.VIEW3D_MT_object.append(ui.object_menu)
bpy.app.handlers.load_post.append(handler.load_post)
workspace.load_custom_icons()
@@ -399,5 +401,7 @@ def unregister():
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.object_menu)
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
workspace.unload_custom_icons()
+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()
+6
View File
@@ -189,6 +189,12 @@ class LaunchTypeManager(bpy.types.Operator):
op.relating_type = relating_type["id"]
def object_menu(self, context):
if context.active_object and tool.Ifc.get_entity(context.active_object):
self.layout.separator()
self.layout.operator("bim.mirror_geometry", icon="MOD_MIRROR")
class BIM_PT_authoring(Panel):
bl_label = "Architectural"
bl_idname = "BIM_PT_authoring"