diff --git a/src/blenderbim/blenderbim/bim/module/void/operator.py b/src/blenderbim/blenderbim/bim/module/void/operator.py index e1276e3ea8..7565d33e68 100644 --- a/src/blenderbim/blenderbim/bim/module/void/operator.py +++ b/src/blenderbim/blenderbim/bim/module/void/operator.py @@ -34,7 +34,18 @@ class AddOpening(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): + self.file = IfcStore.get_file() obj = context.scene.objects.get(self.obj, context.active_object) + if obj is None: + return {"FINISHED"} + element_id = obj.BIMObjectProperties.ifc_definition_id + if not element_id: + return {"FINISHED"} + element = self.file.by_id(element_id) + if element.is_a("IfcOpeningElement"): + self.report({"WARNING"}, "An IfcOpeningElement can't be voided") + return {"FINISHED"} + opening = bpy.data.objects.get(self.opening) if opening is None: return {"FINISHED"} @@ -45,14 +56,22 @@ class AddOpening(bpy.types.Operator): return {"FINISHED"} bpy.ops.bim.assign_class(obj=opening.name, ifc_class="IfcOpeningElement", context_id=body_context.id()) - self.file = IfcStore.get_file() - element_id = obj.BIMObjectProperties.ifc_definition_id + # If the IfcOpeningElement aleady voids another object, remove the boolean modifier + opening_element = self.file.by_id(opening.BIMObjectProperties.ifc_definition_id) + if opening_element.VoidsElements: + other_obj = IfcStore.get_element(opening_element.VoidsElements[0].RelatingBuildingElement.id()) + try: + modifier = next(m for m in other_obj.modifiers if m.type == "BOOLEAN" and m.object == opening) + other_obj.modifiers.remove(modifier) + except StopIteration: + pass + ifcopenshell.api.run( "void.add_opening", self.file, **{ - "opening": self.file.by_id(opening.BIMObjectProperties.ifc_definition_id), - "element": self.file.by_id(element_id), + "opening": opening_element, + "element": element, }, ) Data.load(self.file, element_id) @@ -129,7 +148,7 @@ class AddFilling(bpy.types.Operator): self.file = IfcStore.get_file() element_id = obj.BIMObjectProperties.ifc_definition_id opening_id = opening.BIMObjectProperties.ifc_definition_id - if not element_id or not opening_id: + if not element_id or not opening_id or element_id == opening_id: return {"FINISHED"} ifcopenshell.api.run( "void.add_filling", @@ -154,11 +173,14 @@ class RemoveFilling(bpy.types.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object + if not obj: + return {"FINISHED"} self.file = IfcStore.get_file() - ifcopenshell.api.run( - "void.remove_filling", self.file, **{"element": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)} - ) - Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) + element_id = obj.BIMObjectProperties.ifc_definition_id + if not element_id: + return {"FINISHED"} + ifcopenshell.api.run("void.remove_filling", self.file, **{"element": self.file.by_id(element_id)}) + Data.load(self.file, element_id) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/void/ui.py b/src/blenderbim/blenderbim/bim/module/void/ui.py index 2a66f118ce..72568fef23 100644 --- a/src/blenderbim/blenderbim/bim/module/void/ui.py +++ b/src/blenderbim/blenderbim/bim/module/void/ui.py @@ -40,9 +40,10 @@ class BIM_PT_voids(Panel): def draw(self, context): props = context.active_object.BIMObjectProperties + file = IfcStore.get_file() if props.ifc_definition_id not in Data.products: - Data.load(IfcStore.get_file(), props.ifc_definition_id) - + Data.load(file, props.ifc_definition_id) + active_object_is_an_opening = file.by_id(props.ifc_definition_id).is_a("IfcOpeningElement") row = self.layout.row(align=True) if len(context.selected_objects) == 2: op = row.operator("bim.add_opening", icon="ADD", text="Add Opening") @@ -71,14 +72,16 @@ class BIM_PT_voids(Panel): row.label(text="Select an opening and an element to modify", icon="HELP") opening_ids = Data.products[props.ifc_definition_id] - if not opening_ids: + if not opening_ids and not active_object_is_an_opening: row = self.layout.row(align=True) row.label(text="No Openings", icon="SELECT_SUBTRACT") for opening_id in opening_ids: opening = Data.openings[opening_id] if opening["HasFillings"]: for filling_id in opening["HasFillings"]: - filling = Data.fillings[filling_id] + filling = Data.fillings.get(filling_id) + if filling is None: + continue row = self.layout.row(align=True) row.label(text=opening["Name"], icon="SELECT_SUBTRACT") row.label(text=filling["Name"], icon="SELECT_INTERSECT") @@ -87,8 +90,18 @@ class BIM_PT_voids(Panel): row.label(text=opening["Name"], icon="SELECT_SUBTRACT") op = row.operator("bim.remove_opening", icon="X", text="") op.opening_id = opening_id - - if props.ifc_definition_id not in Data.fillings: + if props.ifc_definition_id in Data.openings: + for filling_id in Data.openings[props.ifc_definition_id]["HasFillings"]: + filling = Data.fillings.get(filling_id) + if filling is None: + continue + row = self.layout.row(align=True) + row.label(text=filling["Name"], icon="SELECT_INTERSECT") + op = row.operator("bim.remove_filling", icon="X", text="") + op.obj = IfcStore.get_element(filling_id).name + if active_object_is_an_opening: + pass + elif props.ifc_definition_id not in Data.fillings: row = self.layout.row(align=True) row.prop(context.scene.VoidProperties, "desired_opening", text="", icon="SELECT_INTERSECT") row.operator("bim.add_filling", icon="ADD", text="") diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 1df58dba4f..78589e6084 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -663,11 +663,17 @@ class OverrideDelete(bpy.types.Operator): return context.window_manager.invoke_confirm(self, event) def _execute(self, context): + file = IfcStore.get_file() for obj in context.selected_objects: if obj.BIMObjectProperties.ifc_definition_id: - element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + element = file.by_id(obj.BIMObjectProperties.ifc_definition_id) + if element.FillsVoids: + self.delete_filling_element(element) if element.is_a("IfcOpeningElement"): - self.delete_opening_element(element) + if element.VoidsElements: + self.delete_opening_element(element) + # for rel in element.HasFillings: + # self.delete_filling_element(rel.RelatedBuildingElement) elif element.HasOpenings: for rel in element.HasOpenings: self.delete_opening_element(rel.RelatedOpeningElement) @@ -677,3 +683,7 @@ class OverrideDelete(bpy.types.Operator): def delete_opening_element(self, element): obj = IfcStore.get_element(element.VoidsElements[0].RelatingBuildingElement.id()) bpy.ops.bim.remove_opening(opening_id=element.id(), obj=obj.name) + + def delete_filling_element(self, element): + obj = IfcStore.get_element(element.id()) + bpy.ops.bim.remove_filling(obj=obj.name) diff --git a/src/blenderbim/test/bim/bootstrap.py b/src/blenderbim/test/bim/bootstrap.py index 42425fe715..5071210569 100644 --- a/src/blenderbim/test/bim/bootstrap.py +++ b/src/blenderbim/test/bim/bootstrap.py @@ -40,9 +40,10 @@ class NewFile: def setup(self): IfcStore.purge() bpy.ops.wm.read_homefile(app_template="") - while bpy.data.objects: - bpy.data.objects.remove(bpy.data.objects[0]) - bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) + if bpy.data.objects: + while bpy.data.objects: + bpy.data.objects.remove(bpy.data.objects[0]) + bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) class NewIfc: @@ -83,7 +84,7 @@ def i_add_a_cube_of_size_size_at_location(size, location): def the_object_name_is_selected(name): - bpy.ops.object.select_all(action="DESELECT") + i_deselect_all_objects() additionally_the_object_name_is_selected(name) @@ -94,6 +95,10 @@ def additionally_the_object_name_is_selected(name): bpy.context.view_layer.objects.active = obj obj.select_set(True) +def i_deselect_all_objects(): + bpy.context.view_layer.objects.active = None + bpy.ops.object.select_all(action="DESELECT") + def i_am_on_frame_number(number): bpy.context.scene.frame_set(int(number)) @@ -278,6 +283,42 @@ def the_object_name_is_not_voided_by_void(name, void): assert False, "A void was found" +def the_object_name_is_not_voided(name): + ifc = IfcStore.get_file() + element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + if any(element.HasOpenings): + assert False, "An opening was found" + + +def the_object_name_is_not_a_void(name): + ifc = IfcStore.get_file() + element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + if any(element.VoidsElements): + assert False, "A void was found" + + +def the_void_name_is_filled_by_filling(name, filling): + ifc = IfcStore.get_file() + element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): + return True + assert False, "No filling found" + + +def the_void_name_is_not_filled_by_filling(name, filling): + ifc = IfcStore.get_file() + element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): + assert False, "A filling was found" + + +def the_object_name_is_not_a_filling(name): + ifc = IfcStore.get_file() + element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + if any(element.FillsVoids): + assert False, "A filling was found" + + def the_object_name_should_display_as_mode(name, mode): assert the_object_name_exists(name).display_type == mode @@ -305,6 +346,7 @@ definitions = { 'I add a cube of size "([0-9]+)" at "(.*)"': i_add_a_cube_of_size_size_at_location, 'the object "(.*)" is selected': the_object_name_is_selected, 'additionally the object "(.*)" is selected': additionally_the_object_name_is_selected, + 'I deselect all objects': i_deselect_all_objects, 'I am on frame "([0-9]+)"': i_am_on_frame_number, 'I set "(.*)" to "(.*)"': i_set_prop_to_value, '"(.*)" is "(.*)"': prop_is_value, @@ -333,10 +375,15 @@ definitions = { 'the object "(.*)" has no boolean difference by "(.*)"': the_object_name1_has_no_boolean_difference_by_name2, 'the object "(.*)" is voided by "(.*)"': the_object_name_is_voided_by_void, 'the object "(.*)" is not voided by "(.*)"': the_object_name_is_not_voided_by_void, + 'the object "(.*)" is not a void': the_object_name_is_not_a_void, + 'the object "(.*)" is not voided': the_object_name_is_not_voided, 'the object "(.*)" should display as "(.*)"': the_object_name_should_display_as_mode, 'the object "(.*)" has "([0-9]+)" vertices': the_object_name_has_number_vertices, 'the object "(.*)" is at "(.*)"': the_object_name_is_at_location, "nothing interesting happens": lambda: None, + 'the void "(.*)" is filled by "(.*)"': the_void_name_is_filled_by_filling, + 'the void "(.*)" is not filled by "(.*)"': the_void_name_is_not_filled_by_filling, + 'the object "(.*)" is not a filling': the_object_name_is_not_a_filling, } diff --git a/src/blenderbim/test/bim/module/void/test_operator.py b/src/blenderbim/test/bim/module/void/test_operator.py index dc824f6c9b..742678d50b 100644 --- a/src/blenderbim/test/bim/module/void/test_operator.py +++ b/src/blenderbim/test/bim/module/void/test_operator.py @@ -38,6 +38,99 @@ class TestAddOpening(test.bim.bootstrap.NewFile): And the object "IfcWall/Cube" is voided by "Cube" """ + @test.bim.bootstrap.scenario + def test_adding_an_opening_on_an_opening(self): + """An opening can't legally be voided by another opening""" + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And the object "IfcOpeningElement/Cube" is selected + And additionally the object "IfcOpeningElement/Cube.001" is selected + And I press "bim.add_opening(opening='IfcOpeningElement/Cube', obj='IfcOpeningElement/Cube.001')" + Then the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" + And the object "IfcOpeningElement/Cube" should display as "WIRE" + And the object "IfcOpeningElement/Cube" is not a void + Then the object "IfcOpeningElement/Cube.001" is an "IfcOpeningElement" + And the object "IfcOpeningElement/Cube.001" should display as "WIRE" + Then the object "IfcOpeningElement/Cube.001" has no boolean difference by "IfcOpeningElement/Cube" + And the object "IfcOpeningElement/Cube.001" is not voided by "Cube" + """ + + @test.bim.bootstrap.scenario + def test_adding_an_opening_on_a_null_object(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I deselect all objects + And I press "bim.add_opening(opening='IfcOpeningElement/Cube')" + Then the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" + And the object "IfcOpeningElement/Cube" should display as "WIRE" + And the object "IfcOpeningElement/Cube" is not a void + """ + + @test.bim.bootstrap.scenario + def test_adding_an_opening_on_a_non_ifc_object(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I press "bim.add_opening(opening='IfcOpeningElement/Cube', obj='Cube')" + Then the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" + And the object "IfcOpeningElement/Cube" should display as "WIRE" + And the object "IfcOpeningElement/Cube" is not a void + And the object "Cube" is not an IFC element + """ + + @test.bim.bootstrap.scenario + def test_adding_an_opening_with_a_null_object(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And I deselect all objects + And I press "bim.add_opening(obj='IfcWall/Cube')" + Then the object "IfcWall/Cube" is an "IfcWall" + And the object "IfcWall/Cube" is not voided + """ + + @test.bim.bootstrap.scenario + def test_adding_an_opening_to_element_b_with_a_void_that_already_voids_element_a(self): + """An opening can legally void at most one element""" + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And I add a cube + And I press "bim.add_opening(opening='Cube', obj='IfcWall/Cube')" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And I press "bim.add_opening(opening='IfcOpeningElement/Cube', obj='IfcWall/Cube.001')" + Then the object "IfcWall/Cube" is not voided + And the object "IfcWall/Cube" has no boolean difference by "IfcOpeningElement/Cube" + And the object "IfcWall/Cube.001" is voided by "Cube" + And the object "IfcWall/Cube.001" has a boolean difference by "IfcOpeningElement/Cube" + """ + class TestRemoveOpening(test.bim.bootstrap.NewFile): @test.bim.bootstrap.scenario @@ -115,3 +208,197 @@ class TestRemoveOpening(test.bim.bootstrap.NewFile): And I delete the selected objects Then the object "Cube" is not an IFC element """ + + +class TestAddFilling(test.bim.bootstrap.NewFile): + @test.bim.bootstrap.scenario + def test_adding_a_filling(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcDoor" + And I press "bim.assign_class" + And the object "IfcOpeningElement/Cube" is selected + And I press "bim.add_filling(opening='IfcOpeningElement/Cube', obj='IfcDoor/Cube')" + Then the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" + And the object "IfcOpeningElement/Cube" should display as "WIRE" + And the object "IfcDoor/Cube" is an "IfcDoor" + And the void "IfcOpeningElement/Cube" is filled by "Cube" + """ + + @test.bim.bootstrap.scenario + def test_adding_a_filling_on_null_object(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcDoor" + And I press "bim.assign_class" + And the object "IfcDoor/Cube" is selected + And I press "bim.add_filling(obj='IfcDoor/Cube')" + Then the object "IfcDoor/Cube" is an "IfcDoor" + And the object "IfcDoor/Cube" is not a filling + """ + + @test.bim.bootstrap.scenario + def test_adding_a_filling_on_itself(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And the object "IfcOpeningElement/Cube" is selected + And I press "bim.add_filling(opening='IfcOpeningElement/Cube', obj='IfcOpeningElement/Cube')" + Then the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" + And the void "IfcOpeningElement/Cube" is not filled by "Cube" + And the object "IfcOpeningElement/Cube" is not a filling + """ + + @test.bim.bootstrap.scenario + def test_adding_a_filling_with_a_non_ifc_object(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I add a cube + And the object "IfcOpeningElement/Cube" is selected + And I press "bim.add_filling(opening='IfcOpeningElement/Cube', obj='Cube')" + Then the object "Cube" is not an IFC element + And the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" + And the void "IfcOpeningElement/Cube" is not filled by "Cube" + """ + + @test.bim.bootstrap.scenario + def test_adding_a_filling_on_a_non_ifc_object(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcDoor" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I press "bim.add_filling(opening='Cube', obj='IfcDoor/Cube')" + Then the object "Cube" is not an IFC element + And the object "IfcDoor/Cube" is an "IfcDoor" + """ + + @test.bim.bootstrap.scenario + def test_adding_a_filling_on_opening_b_when_the_filling_alread_fills_element_a(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcDoor" + And I press "bim.assign_class" + And I press "bim.add_filling(opening='IfcOpeningElement/Cube', obj='IfcDoor/Cube')" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I press "bim.add_filling(opening='IfcOpeningElement/Cube.001', obj='IfcDoor/Cube')" + Then the void "IfcOpeningElement/Cube" is not filled by "Cube" + And the void "IfcOpeningElement/Cube.001" is filled by "Cube" + """ + + +class TestRemoveFilling(test.bim.bootstrap.NewFile): + @test.bim.bootstrap.scenario + def test_removing_a_filling(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcDoor" + And I press "bim.assign_class" + And I press "bim.add_filling(opening='IfcOpeningElement/Cube', obj='IfcDoor/Cube')" + And I press "bim.remove_filling(obj='IfcDoor/Cube')" + Then the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" + And the object "IfcDoor/Cube" is an "IfcDoor" + And the void "IfcOpeningElement/Cube" is not filled by "Cube" + """ + + @test.bim.bootstrap.scenario + def test_removing_a_filling_which_is_not_an_ifc_object(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I press "bim.remove_filling(obj='Cube')" + Then the object "Cube" is not an IFC element + """ + + @test.bim.bootstrap.scenario + def test_removing_a_filling_which_is_not_a_filling(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcDoor" + And I press "bim.assign_class" + And I press "bim.remove_filling(obj='IfcDoor/Cube')" + Then the object "IfcDoor/Cube" is an "IfcDoor" + And the object "IfcDoor/Cube" is not a filling + """ + + @test.bim.bootstrap.scenario + def test_removing_a_filling_which_is_null(self): + return """ + Given an empty IFC project + And I press "bim.remove_filling()" + Then nothing interesting happens + """ + + @test.bim.bootstrap.scenario + def test_removing_a_filling_using_deletion_on_the_filling(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcDoor" + And I press "bim.assign_class" + And I press "bim.add_filling(opening='IfcOpeningElement/Cube', obj='IfcDoor/Cube')" + And the object "IfcDoor/Cube" is selected + And I delete the selected objects + Then the object "IfcOpeningElement/Cube" is an "IfcOpeningElement" + And the void "IfcOpeningElement/Cube" is not filled by "Cube" + """ + + @test.bim.bootstrap.scenario + def test_removing_a_filling_using_deletion_on_the_opening(self): + return """ + Given an empty IFC project + And I add a cube + When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcOpeningElement" + And I press "bim.assign_class" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcDoor" + And I press "bim.assign_class" + And I press "bim.add_filling(opening='IfcOpeningElement/Cube', obj='IfcDoor/Cube')" + And the object "IfcOpeningElement/Cube" is selected + And I delete the selected objects + Then the object "IfcDoor/Cube" is an "IfcDoor" + And the object "IfcDoor/Cube" is not a filling + """