From 08e33fe572613c55f973dbb33d8982304d1fc5e9 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Thu, 16 Apr 2026 22:33:47 -0500 Subject: [PATCH] Closes #7943: Add regenerate_wall_to_underside operator When extend_walls_to_underside is applied to a wall and the roof/slab is later moved, pressing Shift+G now re-clips the wall to the slab's new position. The IFC relationship created by connect_wall_to_slab (IfcRelConnectsElements, Description="TOP") is used to look up which slabs a wall is clipped to. On regeneration, the existing manual booleans (IfcPolygonalFaceSet operands) are cleanly removed via remove_representation_item, then clip_wall_to_slab is re-applied for each connected slab. Shift+G on a LAYER2 wall that has a TOP connection now calls bim.regenerate_wall_to_underside; walls without a connection continue to call bim.recalculate_wall as before. Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/model/__init__.py | 1 + src/bonsai/bonsai/bim/module/model/wall.py | 18 +++++++++++++ .../bonsai/bim/module/model/workspace.py | 8 +++++- src/bonsai/bonsai/core/model.py | 25 +++++++++++++++++++ src/bonsai/bonsai/core/tool.py | 2 ++ src/bonsai/bonsai/tool/model.py | 23 +++++++++++++++++ 6 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 5e2ef55e5c..c281e14ec0 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -85,6 +85,7 @@ classes = ( wall.EnableEditingWall, wall.ExtendWallHeightToCursor, wall.ExtendWallsToUnderside, + wall.RegenerateWallToUnderside, wall.ExtendWallsToWall, wall.ExtendWallsToPolylinePoint, wall.ExtendWallToCursor, diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 8ec429b333..ce86b97e26 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -316,6 +316,24 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator): self.report({"ERROR"}, "Please select at least one LAYER2 element and an active element") +class RegenerateWallToUnderside(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.regenerate_wall_to_underside" + bl_label = "Regenerate Wall to Underside" + bl_description = "Re-clip selected walls to their connected underside objects after the slab has moved" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + wall_objs = [ + obj + for obj in tool.Blender.get_selected_objects() + if (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER2" + ] + if wall_objs: + core.regenerate_wall_to_underside(tool.Ifc, tool.Geometry, tool.Model, wall_objs) + else: + self.report({"ERROR"}, "Please select at least one LAYER2 element") + + class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.extend_walls_to_wall" bl_label = "Extend Walls To Wall" diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 0d9e6305ad..a6e5794c11 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1294,7 +1294,13 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.generate_space() return if self.active_material_usage == "LAYER2": - bpy.ops.bim.recalculate_wall() + if element and any( + rel.is_a("IfcRelConnectsElements") and rel.Description == "TOP" + for rel in element.ConnectedFrom + ): + bpy.ops.bim.regenerate_wall_to_underside() + else: + bpy.ops.bim.recalculate_wall() elif self.active_material_usage == "LAYER3": bpy.ops.bim.recalculate_slab() elif tool.System.get_ports(element): diff --git a/src/bonsai/bonsai/core/model.py b/src/bonsai/bonsai/core/model.py index 61f46cf789..6b62b076e2 100644 --- a/src/bonsai/bonsai/core/model.py +++ b/src/bonsai/bonsai/core/model.py @@ -161,6 +161,31 @@ def align_objects( model.align_objects(reference_obj, objs, align_type) +def regenerate_wall_to_underside( + ifc: type[tool.Ifc], + geometry: type[tool.Geometry], + model: type[tool.Model], + wall_objs: list[bpy.types.Object], +) -> None: + """Re-clip walls to their connected underside objects after the slab has moved.""" + clipped_objs = [] + for obj in wall_objs: + wall = ifc.get_entity(obj) + slab_objs = model.get_connected_slab_objs(wall) + if not slab_objs: + continue + if ifc.is_moved(obj): + geometry.run_edit_object_placement(obj=obj) + model.remove_wall_to_underside_booleans(wall) + for slab_obj in slab_objs: + clip = model.get_slab_clipping_bmesh(slab_obj) + if clip: + model.clip_wall_to_slab(wall, clip) + clipped_objs.append(obj) + if clipped_objs: + model.reload_body_representation(clipped_objs) + + def extend_wall_to_slab( ifc: type[tool.Ifc], geometry: type[tool.Geometry], diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index d3260fa278..f1b1807bbd 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -681,6 +681,7 @@ class Model: def export_profile(cls, obj, position=None): pass def generate_occurrence_name(cls, element_type, ifc_class): pass def get_extrusion(cls, representation): pass + def get_connected_slab_objs(cls, wall): pass def get_manual_booleans(cls, element): pass def get_material_layer_parameters(cls, element): pass def get_slab_clipping_bmesh(cls, obj): pass @@ -696,6 +697,7 @@ class Model: def regenerate_profile(cls, obj): pass def regenerate_slab(cls, obj): pass def reload_body_representation(cls, obj_or_objects): pass + def remove_wall_to_underside_booleans(cls, wall): pass def replace_object_ifc_representation(cls, ifc_file, ifc_context, obj, new_representation): pass diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 2bae0b5a89..502b7fb01d 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -843,6 +843,29 @@ class Model(bonsai.core.tool.Model): items.append(item.FirstOperand) return booleans + @classmethod + def get_connected_slab_objs(cls, wall: ifcopenshell.entity_instance) -> list[bpy.types.Object]: + """Return Blender objects for slabs connected to wall via IfcRelConnectsElements(TOP).""" + result = [] + for rel in wall.ConnectedFrom: + if rel.is_a("IfcRelConnectsElements") and rel.Description == "TOP": + slab_obj = tool.Ifc.get_object(rel.RelatingElement) + if slab_obj: + result.append(slab_obj) + return result + + @classmethod + def remove_wall_to_underside_booleans(cls, wall: ifcopenshell.entity_instance) -> None: + """Remove all IfcBooleanResult items previously added by extend_walls_to_underside.""" + manual_booleans = cls.get_manual_booleans(wall) + if not manual_booleans: + return + mesh_operands = [ + b.SecondOperand for b in manual_booleans if b.SecondOperand.is_a("IfcTessellatedFaceSet") + ] + for mesh in mesh_operands: + tool.Geometry.remove_representation_item(mesh, wall) + @classmethod def get_manual_booleans( cls, element: ifcopenshell.entity_instance, representation: Optional[ifcopenshell.entity_instance] = None