From 5a6476a57f8f685cf220225408bd39915423ede6 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 17 Apr 2025 16:28:05 +1000 Subject: [PATCH] Fix #3673. Bug where a split wall broke flipped doors. Instead of regenerating fills upon splitting walls, placements are preserved exactly. --- src/bonsai/bonsai/bim/module/model/wall.py | 21 ++++++-- src/bonsai/test/bim/feature/model.feature | 62 ++++++++++++++++++++++ src/bonsai/test/bim/test_feature.py | 20 +++++++ 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 5161c14d27..aac50f53de 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -1019,7 +1019,6 @@ class DumbWallJoiner: bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1) axis1 = tool.Model.get_wall_axis(wall1) - axis2 = copy.deepcopy(axis1) intersect, cut_percentage = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"]) if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)): return @@ -1080,12 +1079,28 @@ class DumbWallJoiner: # During the duplication process, filled voids are not copied. So we # only need to check fillings on the original element1. for opening in [r.RelatedOpeningElement for r in element1.HasOpenings if r.RelatedOpeningElement.HasFillings]: - filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement) + rel = opening.HasFillings[0] + filling = rel.RelatedBuildingElement + filling_obj = tool.Ifc.get_object(filling) filling_location = filling_obj.matrix_world.translation _, filling_position = mathutils.geometry.intersect_point_line(filling_location.to_2d(), *axis1["reference"]) if filling_position > cut_percentage: # The filling should be moved from element1 to element2. - FilledOpeningGenerator().generate(filling_obj, wall2, target=filling_obj.matrix_world.translation) + new_opening = ifcopenshell.api.root.copy_class(tool.Ifc.get(), product=opening) + new_opening.VoidsElements[0].RelatingBuildingElement = element2 + if new_opening.ObjectPlacement and new_opening.ObjectPlacement.is_a("IfcLocalPlacement"): + if element2.ObjectPlacement: + new_opening.ObjectPlacement.PlacementRelTo = element2.ObjectPlacement + # For now, we do copy opening representations + if opening.Representation: + new_opening.Representation = ifcopenshell.util.element.copy_deep( + tool.Ifc.get(), opening.Representation, exclude=["IfcGeometricRepresentationContext"] + ) + + rel.RelatedBuildingElement = element2 + + # Remove the old opening + ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening) p1, p2 = ifcopenshell.util.representation.get_reference_line(element1) p3 = (wall1.matrix_world.inverted() @ intersect.to_3d()).to_2d() / unit_scale diff --git a/src/bonsai/test/bim/feature/model.feature b/src/bonsai/test/bim/feature/model.feature index 729bd52bd5..4154a3a535 100644 --- a/src/bonsai/test/bim/feature/model.feature +++ b/src/bonsai/test/bim/feature/model.feature @@ -224,6 +224,68 @@ Scenario: Regenerate a wall - after doing nothing interesting And the object "IfcWall/Wall" dimensions are "1,0.1,3" And the object "IfcWall/Wall" bottom left corner is at "0,0,0" +Scenario: Insert door into wall + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.add_occurrence" + And the object "IfcWall/Wall" is selected + And the cursor is at "10,0,0" + # Extend the wall + And I press "bim.hotkey(hotkey='S_E')" + When I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcDoorType') if e.Name == 'DT01'][0].id()" + And the cursor is at "7,0,0" + And I press "bim.add_occurrence" + Then the object "IfcDoor/Door" is at "7,0,0" + And the object "IfcWall/Wall" is filled by "IfcDoor/Door" + +Scenario: Flip a door inserted in a wall + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.add_occurrence" + And the object "IfcWall/Wall" is selected + And the cursor is at "10,0,0" + # Extend the wall + And I press "bim.hotkey(hotkey='S_E')" + # Insert a door + And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcDoorType') if e.Name == 'DT01'][0].id()" + And the cursor is at "7,0,0" + And I press "bim.add_occurrence" + When the object "IfcDoor/Door" is selected + And I press "bim.hotkey(hotkey='S_F')" + Then the object "IfcDoor/Door" is at "8.01,0.1,0" + +Scenario: Split a wall which has a flipped door + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.add_occurrence" + And the object "IfcWall/Wall" is selected + And the cursor is at "10,0,0" + # Extend the wall + And I press "bim.hotkey(hotkey='S_E')" + # Insert a door + And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcDoorType') if e.Name == 'DT01'][0].id()" + And the cursor is at "7,0,0" + And I press "bim.add_occurrence" + # Flip the door + And the object "IfcDoor/Door" is selected + And I press "bim.hotkey(hotkey='S_F')" + When the cursor is at "5,0,0" + And the object "IfcWall/Wall" is selected + And I press "bim.hotkey(hotkey='S_K')" + Then the object "IfcDoor/Door" is at "8.01,0.1,0" + Scenario: Add a slab Given an empty IFC project And I load the demo construction library diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 121ce197b0..565c82b575 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -1057,6 +1057,25 @@ def the_object_name_has_number_vertices(name, number): assert total == int(number), f"We found {total} vertices" +@then(parsers.parse('the object "{name}" is filled by "{name2}"')) +def the_object_name_is_filled_by_filling(name, name2): + ifc = tool.Ifc.get() + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) + debug = {} + for rel in ifcopenshell.util.element.get_openings(element): + filling = None + opening = rel.RelatedOpeningElement + for rel2 in opening.HasFillings or []: + filling = rel2.RelatedBuildingElement + break + debug[opening] = filling + filling_obj = tool.Ifc.get_object(filling) + if filling_obj and filling_obj.name == name2: + return True + debug = "\n".join([f"{k} filled by {v}" for k, v in debug.items()]) + assert False, f"Object {name} is not filled by {name2}.\n{debug}" + + @then(parsers.parse('the void "{name}" is filled by "{filling}"')) def the_void_name_is_filled_by_filling(name, filling): ifc = tool.Ifc.get() @@ -1357,6 +1376,7 @@ def i_add_a_construction_library(): @given(parsers.parse('the cursor is at "{location}"')) +@when(parsers.parse('the cursor is at "{location}"')) def the_cursor_is_at_location(location): bpy.context.scene.cursor.location = [float(co) for co in location.split(",")]