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.
This commit is contained in:
Ryan Schultz
2026-04-16 22:33:47 -05:00
parent 06e364377d
commit c74b4d0f91
6 changed files with 76 additions and 1 deletions
@@ -83,6 +83,7 @@ classes = (
wall.EnableEditingWall,
wall.ExtendWallHeightToCursor,
wall.ExtendWallsToUnderside,
wall.RegenerateWallToUnderside,
wall.ExtendWallsToWall,
wall.ExtendWallsToPolylinePoint,
wall.ExtendWallToCursor,
@@ -216,6 +216,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"
@@ -1300,7 +1300,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):
+25
View File
@@ -143,6 +143,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],
+2
View File
@@ -668,6 +668,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
@@ -683,6 +684,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
+23
View File
@@ -811,6 +811,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