diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 723c0f3b7c..7750befda9 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -50,8 +50,9 @@ class LaunchTypeManager(bpy.types.Operator): def draw(self, context): props = context.scene.BIMModelProperties - row = self.layout.row() + row = self.layout.row(align=True) prop_with_search(row, props, "type_class", text="") + row.operator("bim.purge_unused_types", icon="TRASH", text="") columns = self.layout.column_flow(columns=3) row = columns.row() diff --git a/src/blenderbim/blenderbim/bim/module/type/__init__.py b/src/blenderbim/blenderbim/bim/module/type/__init__.py index a9ab8550f7..50accd37ea 100644 --- a/src/blenderbim/blenderbim/bim/module/type/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/type/__init__.py @@ -25,6 +25,7 @@ classes = ( operator.DisableEditingType, operator.DuplicateType, operator.EnableEditingType, + operator.PurgeUnusedTypes, operator.RemoveType, operator.SelectSimilarType, operator.SelectType, diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index 4816d4eb61..a9f3918593 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -378,3 +378,12 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): new.Name += " Copy" bpy.ops.bim.load_type_thumbnails(ifc_class=new.is_a()) return {"FINISHED"} + + +class PurgeUnusedTypes(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.purge_unused_types" + bl_label = "Purge Unused Types" + bl_options = {"REGISTER"} + + def _execute(self, context): + core.purge_unused_types(tool.Ifc, tool.Type) diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index 3d209024f5..35f5708a94 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -89,6 +89,5 @@ def select_container(ifc, spatial, obj=None): def select_similar_container(ifc, spatial, obj=None): element = ifc.get_entity(obj) - if not element: - return - spatial.select_products(products=spatial.get_decomposed_elements(spatial.get_container(element))) + if element: + spatial.select_products(spatial.get_decomposed_elements(spatial.get_container(element))) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 4cde186194..0c67e9529e 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -642,14 +642,12 @@ class Spatial: def get_object_matrix(cls, obj): pass def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass def get_selected_product_types(cls): pass - def get_selected_products_and_types(cls): pass - def get_selected_products(cls): pass def get_selected_products(cls): pass def import_containers(cls, parent=None): pass def run_root_copy_class(cls, obj=None): pass def run_spatial_assign_container(cls, structure_obj=None, element_obj=None): pass def select_object(cls, obj): pass - def select_products(cls, products, unhide): pass + def select_products(cls, products, unhide=False): pass def set_active_object(cls, obj): pass def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass def show_scene_objects(cls): pass @@ -728,10 +726,13 @@ class Type: def get_body_context(cls): pass def get_body_representation(cls, element): pass def get_ifc_representation_class(cls, element): pass + def get_model_types(cls): pass def get_object_data(cls, obj): pass def get_profile_set_usage(cls, element): pass def get_representation_context(cls, representation): pass + def get_type_occurrences(cls, element_type): pass def has_material_usage(cls, element): pass + def remove_object(cls, obj): pass def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass def run_geometry_switch_representation(cls, obj=None, representation=None, should_reload=None, is_global=None): pass diff --git a/src/blenderbim/blenderbim/core/type.py b/src/blenderbim/blenderbim/core/type.py index ec2f9ba53a..6368cb1302 100644 --- a/src/blenderbim/blenderbim/core/type.py +++ b/src/blenderbim/blenderbim/core/type.py @@ -29,3 +29,13 @@ def assign_type(ifc, type_tool, element=None, type=None): if type_data: type_tool.change_object_data(obj, type_data, is_global=False) type_tool.disable_editing(obj) + + +def purge_unused_types(ifc, type): + for element_type in type.get_model_types(): + if not type.get_type_occurrences(element_type): + ifc.run("root.remove_product", product=element_type) + obj = ifc.get_object(element_type) + if obj: + ifc.unlink(obj=obj) + type.remove_object(obj) diff --git a/src/blenderbim/blenderbim/tool/type.py b/src/blenderbim/blenderbim/tool/type.py index 67bdf5fce7..0dc1e18b82 100644 --- a/src/blenderbim/blenderbim/tool/type.py +++ b/src/blenderbim/blenderbim/tool/type.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import bpy import ifcopenshell import blenderbim.core.tool import blenderbim.core.geometry @@ -59,6 +60,10 @@ class Type(blenderbim.core.tool.Type): elif material.is_a("IfcMaterialLayerSetUsage"): return "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" + @classmethod + def get_model_types(cls): + return tool.Ifc.get().by_type("IfcElementType") + @classmethod def get_object_data(cls, obj): return obj.data @@ -74,6 +79,10 @@ class Type(blenderbim.core.tool.Type): def get_representation_context(cls, representation): return representation.ContextOfItems + @classmethod + def get_type_occurrences(cls, element_type): + return ifcopenshell.util.element.get_types(element_type) + @classmethod def has_material_usage(cls, element): material = ifcopenshell.util.element.get_material(element) @@ -81,6 +90,10 @@ class Type(blenderbim.core.tool.Type): return "Usage" in material.is_a() return False + @classmethod + def remove_object(cls, obj): + bpy.data.objects.remove(obj) + @classmethod def run_geometry_add_representation( cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None diff --git a/src/blenderbim/test/bim/feature/type.feature b/src/blenderbim/test/bim/feature/type.feature index 3cb1870954..ec6994c864 100644 --- a/src/blenderbim/test/bim/feature/type.feature +++ b/src/blenderbim/test/bim/feature/type.feature @@ -1,6 +1,25 @@ @type Feature: Type +Scenario: Add type - adding via manual class assignment + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" + When I press "bim.assign_class" + Then nothing happens + +Scenario: Add type - add from empty template + Given an empty IFC project + And I press "bim.launch_type_manager" + And I set "scene.BIMModelProperties.type_class" to "IfcWallType" + And I set "scene.BIMModelProperties.type_predefined_type" to "SOLIDWALL" + And I set "scene.BIMModelProperties.type_template" to "EMPTY" + When I press "bim.add_type" + Then the object "IfcWallType/TYPEX" is an "IfcWallType" + And the object "IfcWallType/TYPEX" has no data + Scenario: Enable editing type Given an empty IFC project And I add a cube @@ -171,3 +190,15 @@ Scenario: Select similar type And I press "bim.select_similar_type" Then the object "IfcWall/Cube" is selected And the object "IfcWall/Cube.001" is selected + +Scenario: Purge unused types + Given an empty IFC project + And I press "bim.launch_type_manager" + And I set "scene.BIMModelProperties.type_class" to "IfcWallType" + And I set "scene.BIMModelProperties.type_predefined_type" to "SOLIDWALL" + And I set "scene.BIMModelProperties.type_template" to "EMPTY" + When I press "bim.add_type" + Then the object "IfcWallType/TYPEX" is an "IfcWallType" + And the object "IfcWallType/TYPEX" has no data + When I press "bim.purge_unused_types" + Then the object "IfcWallType/TYPEX" does not exist diff --git a/src/blenderbim/test/core/test_spatial.py b/src/blenderbim/test/core/test_spatial.py index 0c8e37e571..d8d52c9220 100644 --- a/src/blenderbim/test/core/test_spatial.py +++ b/src/blenderbim/test/core/test_spatial.py @@ -124,6 +124,5 @@ class TestSelectSimilarContainer: ifc.get_entity("obj").should_be_called().will_return("element") spatial.get_container("element").should_be_called().will_return("container") spatial.get_decomposed_elements("container").should_be_called().will_return(["contained_element"]) - ifc.get_object("contained_element").should_be_called().will_return("contained_obj") - spatial.select_object("contained_obj").should_be_called() + spatial.select_products(["contained_element"]).should_be_called() subject.select_similar_container(ifc, spatial, obj="obj") diff --git a/src/blenderbim/test/core/test_type.py b/src/blenderbim/test/core/test_type.py index a603a6297d..0a084f8438 100644 --- a/src/blenderbim/test/core/test_type.py +++ b/src/blenderbim/test/core/test_type.py @@ -39,3 +39,14 @@ class TestAssignType: ifc.get_object("element").should_be_called().will_return("obj") type.disable_editing("obj").should_be_called() subject.assign_type(ifc, type, element="element", type="type") + + +class TestPurgeUnusedTypes: + def test_run(self, ifc, type): + type.get_model_types().should_be_called().will_return(["element_type"]) + type.get_type_occurrences("element_type").should_be_called().will_return([]) + ifc.run("root.remove_product", product="element_type").should_be_called() + ifc.get_object("element_type").should_be_called().will_return("obj") + ifc.unlink(obj="obj").should_be_called() + type.remove_object("obj").should_be_called() + subject.purge_unused_types(ifc, type) diff --git a/src/blenderbim/test/tool/test_type.py b/src/blenderbim/test/tool/test_type.py index 2f8d1ea09e..5e49f0d487 100644 --- a/src/blenderbim/test/tool/test_type.py +++ b/src/blenderbim/test/tool/test_type.py @@ -114,6 +114,14 @@ class TestGetIfcRepresentationClass(NewFile): assert subject.get_ifc_representation_class(ifc.createIfcColumn()) is None +class TestGetModelTypes(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") + assert subject.get_model_types() == [wall_type] + + class TestGetObjectData(NewFile): def test_run(self): data = bpy.data.meshes.new("Mesh") @@ -139,6 +147,16 @@ class TestGetRepresentationContext(NewFile): assert subject.get_representation_context(representation) == context +class TestGetTypeOccurrences(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") + wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") + ifcopenshell.api.run("type.assign_type", ifc, related_object=wall, relating_type=wall_type) + assert subject.get_type_occurrences(wall_type) == (wall,) + + class TestHasMaterialUsage(NewFile): def test_getting_a_profile_set_usage(self): ifc = ifcopenshell.file() @@ -149,6 +167,13 @@ class TestHasMaterialUsage(NewFile): assert subject.has_material_usage(element) is True +class TestRemoveObject(NewFile): + def test_run(self): + obj = bpy.data.objects.new("Object", None) + subject.remove_object(obj) + assert not bpy.data.objects.get("Object") + + class TestRunGeometryAddRepresentation(NewFile): def test_nothing(self): pass