diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py index 6c6dd30226..9f347356d0 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py @@ -21,6 +21,7 @@ from . import ui, prop, operator classes = ( operator.AssignObject, + operator.UnassignObject, operator.EnableEditingAggregate, operator.DisableEditingAggregate, operator.AddAggregate, diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/data.py b/src/blenderbim/blenderbim/bim/module/aggregate/data.py index 4ae27e5ae4..d65f631586 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/data.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/data.py @@ -13,7 +13,11 @@ class AggregateData: @classmethod def load(cls): - cls.data = {"has_aggregate": cls.has_aggregate(), "label": cls.get_label()} + cls.data = { + "has_aggregate": cls.has_aggregate(), + "label": cls.get_label(), + "relating_object_id": cls.get_relating_object_id(), + } cls.is_loaded = True @classmethod @@ -25,3 +29,9 @@ class AggregateData: aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object)) if aggregate: return f"{aggregate.is_a()}/{aggregate.Name or ''}" + + @classmethod + def get_relating_object_id(cls): + aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object)) + if aggregate: + return aggregate.id() diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index 9a43886430..22ea870446 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -47,6 +47,22 @@ class AssignObject(bpy.types.Operator, Operator): ) +class UnassignObject(bpy.types.Operator, Operator): + bl_idname = "bim.unassign_object" + bl_label = "Unassign Object" + bl_options = {"REGISTER", "UNDO"} + relating_object: bpy.props.IntProperty() + related_object: bpy.props.IntProperty() + + def _execute(self, context): + core.unassign_object( + tool.Ifc, + tool.Collector, + relating_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object)), + related_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.related_object)), + ) + + class EnableEditingAggregate(bpy.types.Operator, Operator): bl_idname = "bim.enable_editing_aggregate" bl_label = "Enable Editing Aggregate" diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index e107784bcb..be21078f8b 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -60,7 +60,12 @@ class BIM_PT_aggregate(Panel): row = self.layout.row(align=True) if AggregateData.data["has_aggregate"]: row.label(text=AggregateData.data["label"]) + row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="") + row.operator("bim.add_aggregate", icon="ADD", text="") + op = row.operator("bim.unassign_object", icon="X", text="") + op.relating_object = AggregateData.data["relating_object_id"] + op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id else: row.label(text="No Aggregate Found") - row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="") - row.operator("bim.add_aggregate", icon="ADD", text="") + row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="") + row.operator("bim.add_aggregate", icon="ADD", text="") diff --git a/src/blenderbim/blenderbim/core/aggregate.py b/src/blenderbim/blenderbim/core/aggregate.py index 3f1e817db1..e1eda02454 100644 --- a/src/blenderbim/blenderbim/core/aggregate.py +++ b/src/blenderbim/blenderbim/core/aggregate.py @@ -16,3 +16,10 @@ def assign_object(ifc, aggregator, collector, relating_obj=None, related_obj=Non collector.assign(related_obj) aggregator.disable_editing(related_obj) return rel + + +def unassign_object(ifc, collector, relating_obj=None, related_obj=None): + rel = ifc.run("aggregate.unassign_object", product=ifc.get_entity(related_obj)) + collector.assign(relating_obj) + collector.assign(related_obj) + return rel diff --git a/src/blenderbim/blenderbim/tool/collector.py b/src/blenderbim/blenderbim/tool/collector.py index 48cf7120c9..5ac0e7ace3 100644 --- a/src/blenderbim/blenderbim/tool/collector.py +++ b/src/blenderbim/blenderbim/tool/collector.py @@ -43,6 +43,8 @@ class Collector(blenderbim.core.tool.Collector): object_collection.objects.link(obj) if collection_collection and collection_collection.children.find(object_collection.name) == -1: + if bpy.context.scene.collection.children.find(object_collection.name) != -1: + bpy.context.scene.collection.children.unlink(object_collection) for collection in bpy.data.collections: if collection.children.find(object_collection.name) != -1: collection.children.unlink(object_collection) diff --git a/src/blenderbim/test/bim/feature/aggregate.feature b/src/blenderbim/test/bim/feature/aggregate.feature index 8a0f3b9141..f443af4cfd 100644 --- a/src/blenderbim/test/bim/feature/aggregate.feature +++ b/src/blenderbim/test/bim/feature/aggregate.feature @@ -23,6 +23,18 @@ Scenario: Assign object And the variable "related_object" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()" When I press "bim.assign_object(relating_object={relating_object}, related_object={related_object})" Then the object "IfcSite/My Site" is in the collection "IfcSite/My Site" - Then the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey" - Then the collection "IfcBuildingStorey/My Storey" is in the collection "IfcSite/My Site" + And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey" + And the collection "IfcBuildingStorey/My Storey" is in the collection "IfcSite/My Site" +Scenario: Unassign object + Given an empty IFC project + And the object "IfcBuildingStorey/My Storey" is selected + And I press "bim.enable_editing_aggregate" + And the variable "relating_object" is "tool.Ifc.get().by_type('IfcSite')[0].id()" + And the variable "related_object" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()" + And I press "bim.assign_object(relating_object={relating_object}, related_object={related_object})" + And the object "IfcBuildingStorey/My Storey" is selected + When I press "bim.unassign_object(relating_object={relating_object}, related_object={related_object})" + Then the object "IfcSite/My Site" is in the collection "IfcSite/My Site" + And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey" + And the collection "IfcBuildingStorey/My Storey" is in the collection "IfcProject/My Project" diff --git a/src/blenderbim/test/core/test_aggregate.py b/src/blenderbim/test/core/test_aggregate.py index f7f82bc5cb..8a00453b26 100644 --- a/src/blenderbim/test/core/test_aggregate.py +++ b/src/blenderbim/test/core/test_aggregate.py @@ -29,3 +29,12 @@ class TestAssignObject: subject.assign_object(ifc, aggregator, collector, relating_obj="relating_obj", related_obj="related_obj") == "rel" ) + + +class TestUnassignObject: + def test_run(self, ifc, collector): + ifc.get_entity("related_obj").should_be_called().will_return("related_object") + ifc.run("aggregate.unassign_object", product="related_object").should_be_called().will_return("rel") + collector.assign("relating_obj").should_be_called() + collector.assign("related_obj").should_be_called() + assert subject.unassign_object(ifc, collector, relating_obj="relating_obj", related_obj="related_obj") == "rel" diff --git a/src/blenderbim/test/tool/test_collector.py b/src/blenderbim/test/tool/test_collector.py index f94145c05e..72c0622edf 100644 --- a/src/blenderbim/test/tool/test_collector.py +++ b/src/blenderbim/test/tool/test_collector.py @@ -99,3 +99,21 @@ class TestAssign(NewFile): subject.assign(element_obj) assert len(element_obj.users_collection) == 1 assert element_obj.users_collection[0].name == element_obj.name + + def test_in_decomposition_mode_existing_collections_are_reassigned_to_the_correct_place_in_the_hierarchy(self): + bpy.ops.bim.create_project() + space_obj = bpy.data.objects.new("IfcSpace/Name", None) + space_element = tool.Ifc.get().createIfcSpace() + tool.Ifc.link(space_element, space_obj) + space_collection = bpy.data.collections.new("IfcSpace/Name") + bpy.context.scene.collection.children.link(space_collection) + space_collection.objects.link(space_obj) + ifcopenshell.api.run( + "aggregate.assign_object", + tool.Ifc.get(), + relating_object=tool.Ifc.get().by_type("IfcSite")[0], + product=space_element, + ) + subject.assign(space_obj) + assert bpy.context.scene.collection.children.find(space_collection.name) == -1 + assert bpy.data.collections.get("IfcSite/My Site").children.find(space_collection.name) != -1 diff --git a/src/ifcopenshell-python/ifcopenshell/api/aggregate/unassign_object.py b/src/ifcopenshell-python/ifcopenshell/api/aggregate/unassign_object.py index 9034defc29..c4cdf4760f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/aggregate/unassign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/aggregate/unassign_object.py @@ -6,7 +6,6 @@ class Usecase: def __init__(self, file, **settings): self.file = file self.settings = { - "relating_object": None, "product": None, } for key, value in settings.items(): @@ -14,7 +13,7 @@ class Usecase: def execute(self): for rel in self.settings["product"].Decomposes or []: - if not rel.is_a("IfcRelAggregates") or rel.RelatingObject != self.settings["relating_object"]: + if not rel.is_a("IfcRelAggregates"): continue if len(rel.RelatedObjects) == 1: return self.file.remove(rel) diff --git a/src/ifcopenshell-python/test/api/aggregate/test_unassign_object.py b/src/ifcopenshell-python/test/api/aggregate/test_unassign_object.py new file mode 100644 index 0000000000..adfa0a291d --- /dev/null +++ b/src/ifcopenshell-python/test/api/aggregate/test_unassign_object.py @@ -0,0 +1,29 @@ +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.element + + +class TestUnassignObject(test.bootstrap.IFC4): + def test_unassigning_an_object(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.unassign_object", self.file, product=subelement) + assert ifcopenshell.util.element.get_aggregate(subelement) is None + + def test_the_rel_is_kept_if_there_are_more_decomposed_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement1, relating_object=element) + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement2, relating_object=element) + ifcopenshell.api.run("aggregate.unassign_object", self.file, product=subelement1) + assert len(self.file.by_type("IfcRelAggregates")) == 1 + + def test_the_rel_is_purged_if_there_are_no_more_decomposed_elements(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSite") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + ifcopenshell.api.run("aggregate.assign_object", self.file, product=subelement, relating_object=element) + ifcopenshell.api.run("aggregate.unassign_object", self.file, product=subelement) + assert len(self.file.by_type("IfcRelAggregates")) == 0