diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index a81a15174e..1890714b04 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -17,13 +17,13 @@ # along with BlenderBIM Add-on. If not, see . import bpy -import bmesh import math +import bmesh +import mathutils.geometry import ifcopenshell import ifcopenshell.util.type import ifcopenshell.util.unit import ifcopenshell.util.element -import mathutils.geometry import blenderbim.bim.handler import blenderbim.tool as tool import blenderbim.core.type @@ -100,9 +100,7 @@ def ensure_solid(usecase_path, ifc_file, settings): material = ifcopenshell.util.element.get_material(product) if material and material.is_a("IfcMaterialProfileSetUsage"): settings["profile_set_usage"] = material - else: - return - settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage" + settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage" class DumbProfileGenerator: diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index a2986d248a..68acc39b05 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -51,7 +51,7 @@ class AssignType(bpy.types.Operator, Operator): else context.selected_objects or [context.active_object] ) for obj in related_objects: - core.assign_type(tool.Ifc, tool.Geometry, tool.Type, element=tool.Ifc.get_entity(obj), type=type) + core.assign_type(tool.Ifc, tool.Type, element=tool.Ifc.get_entity(obj), type=type) oprops = obj.BIMObjectProperties Data.load(IfcStore.get_file(), oprops.ifc_definition_id) GeometryData.load(IfcStore.get_file(), oprops.ifc_definition_id) diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py index 87a61d8544..5414041ffe 100644 --- a/src/blenderbim/blenderbim/core/geometry.py +++ b/src/blenderbim/blenderbim/core/geometry.py @@ -74,12 +74,7 @@ def add_representation( def switch_representation( - geometry, - obj=None, - representation=None, - should_reload=True, - enable_dynamic_voids=True, - is_global=True, + geometry, obj=None, representation=None, should_reload=True, enable_dynamic_voids=True, is_global=True ): representation = geometry.resolve_mapped_representation(representation) data = geometry.get_representation_data(representation) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 6a70b536ad..9c3b94f216 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -197,8 +197,15 @@ class Surveyor: class Type: def disable_editing(cls, obj): pass def get_any_representation(cls, element): pass + def get_body_context(cls): pass def get_body_representation(cls, element): pass + def get_ifc_representation_class(cls, element): pass + def get_profile_set_usage(cls, element): pass + def get_representation_context(cls, representation): pass def has_dynamic_voids(cls, obj): pass + def has_material_usage(cls, element): 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, enable_dynamic_voids=None, is_global=None): pass @interface diff --git a/src/blenderbim/blenderbim/core/type.py b/src/blenderbim/blenderbim/core/type.py index 7d88898d87..5076ee5e93 100644 --- a/src/blenderbim/blenderbim/core/type.py +++ b/src/blenderbim/blenderbim/core/type.py @@ -19,18 +19,34 @@ import blenderbim.core.geometry -def assign_type(ifc, geometry, type_tool, element=None, type=None): +def assign_type(ifc, type_tool, element=None, type=None): ifc.run("type.assign_type", related_object=element, relating_type=type) - representation = type_tool.get_body_representation(element) - if not representation: - representation = type_tool.get_any_representation(element) obj = ifc.get_object(element) + if type_tool.has_material_usage(element): + representation = type_tool.get_body_representation(element) + if representation: + body_context = type_tool.get_representation_context(representation) + ifc.run("geometry.unassign_representation", product=element, representation=representation) + ifc.run("geometry.remove_representation", representation=representation) + else: + body_context = type_tool.get_body_context() + representation = type_tool.run_geometry_add_representation( + obj=obj, + context=body_context, + ifc_representation_class=type_tool.get_ifc_representation_class(element), + profile_set_usage=type_tool.get_profile_set_usage(element), + ) + should_reload = True + else: + representation = type_tool.get_body_representation(element) + if not representation: + representation = type_tool.get_any_representation(element) + should_reload = False if representation: - blenderbim.core.geometry.switch_representation( - geometry, + type_tool.run_geometry_switch_representation( obj=obj, representation=representation, - should_reload=False, + should_reload=should_reload, enable_dynamic_voids=type_tool.has_dynamic_voids(obj), is_global=False, ) diff --git a/src/blenderbim/blenderbim/tool/type.py b/src/blenderbim/blenderbim/tool/type.py index d2bddd3486..a4885d359c 100644 --- a/src/blenderbim/blenderbim/tool/type.py +++ b/src/blenderbim/blenderbim/tool/type.py @@ -18,6 +18,7 @@ import ifcopenshell import blenderbim.core.tool +import blenderbim.core.geometry import blenderbim.tool as tool import blenderbim.bim.helper @@ -34,6 +35,10 @@ class Type(blenderbim.core.tool.Type): elif element.is_a("IfcTypeProduct") and element.RepresentationMaps: return element.RepresentationMaps[0].MappedRepresentation + @classmethod + def get_body_context(cls): + return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") + @classmethod def get_body_representation(cls, element): if element.is_a("IfcProduct") and element.Representation and element.Representation.Representations: @@ -45,9 +50,64 @@ class Type(blenderbim.core.tool.Type): if representation_map.MappedRepresentation.ContextOfItems.ContextIdentifier == "Body": return representation_map.MappedRepresentation + @classmethod + def get_ifc_representation_class(cls, element): + material = ifcopenshell.util.element.get_material(element) + if material: + if material.is_a("IfcMaterialProfileSetUsage"): + return "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage" + elif material.is_a("IfcMaterialLayerSetUsage"): + return "IfcExtrudedAreaSolid/IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" + + @classmethod + def get_profile_set_usage(cls, element): + material = ifcopenshell.util.element.get_material(element) + if material: + if material.is_a("IfcMaterialProfileSetUsage"): + return material + + @classmethod + def get_representation_context(cls, representation): + return representation.ContextOfItems + @classmethod def has_dynamic_voids(cls, obj): for modifier in obj.modifiers: if modifier.name == "IfcOpeningElement" and modifier.type == "BOOLEAN": return True return False + + @classmethod + def has_material_usage(cls, element): + material = ifcopenshell.util.element.get_material(element) + if material: + return "Usage" in material.is_a() + return False + + @classmethod + def run_geometry_add_representation( + cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None + ): + return blenderbim.core.geometry.add_representation( + tool.Ifc, + tool.Geometry, + tool.Style, + tool.Surveyor, + obj=obj, + context=context, + ifc_representation_class=ifc_representation_class, + profile_set_usage=profile_set_usage, + ) + + @classmethod + def run_geometry_switch_representation( + cls, obj=None, representation=None, should_reload=None, enable_dynamic_voids=None, is_global=None + ): + return blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=obj, + representation=representation, + should_reload=should_reload, + enable_dynamic_voids=enable_dynamic_voids, + is_global=is_global, + ) diff --git a/src/blenderbim/test/core/test_type.py b/src/blenderbim/test/core/test_type.py index 79bce1b781..d117fb67fd 100644 --- a/src/blenderbim/test/core/test_type.py +++ b/src/blenderbim/test/core/test_type.py @@ -17,45 +17,84 @@ # along with BlenderBIM Add-on. If not, see . import blenderbim.core.type as subject -from test.core.bootstrap import ifc, geometry, type +from test.core.bootstrap import ifc, type class TestAssignType: - def test_assigning_and_switching_preferably_to_a_body_representation(self, ifc, geometry, type): + def test_assigning_and_switching_preferably_to_a_body_representation(self, ifc, type): ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() + type.has_material_usage("element").should_be_called().will_return(False) type.get_body_representation("element").should_be_called().will_return("mapped_rep") ifc.get_object("element").should_be_called().will_return("obj") type.has_dynamic_voids("obj").should_be_called().will_return(False) - - geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") - geometry.get_representation_data("representation").should_be_called().will_return("data") - geometry.change_object_data("obj", "data", is_global=False).should_be_called() - geometry.clear_dynamic_voids("obj").should_be_called() - + type.run_geometry_switch_representation( + obj="obj", representation="mapped_rep", should_reload=False, enable_dynamic_voids=False, is_global=False + ).should_be_called() type.disable_editing("obj").should_be_called() + subject.assign_type(ifc, type, element="element", type="type") - subject.assign_type(ifc, geometry, type, element="element", type="type") - - def test_assigning_and_switching_to_any_representation_as_a_fallback(self, ifc, geometry, type): + def test_assigning_and_switching_to_any_representation_as_a_fallback(self, ifc, type): ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() + type.has_material_usage("element").should_be_called().will_return(False) type.get_body_representation("element").should_be_called().will_return(None) type.get_any_representation("element").should_be_called().will_return("mapped_rep") ifc.get_object("element").should_be_called().will_return("obj") type.has_dynamic_voids("obj").should_be_called().will_return(False) - - geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") - geometry.get_representation_data("representation").should_be_called().will_return("data") - geometry.change_object_data("obj", "data", is_global=False).should_be_called() - geometry.clear_dynamic_voids("obj").should_be_called() - + type.run_geometry_switch_representation( + obj="obj", representation="mapped_rep", should_reload=False, enable_dynamic_voids=False, is_global=False + ).should_be_called() type.disable_editing("obj").should_be_called() + subject.assign_type(ifc, type, element="element", type="type") - subject.assign_type(ifc, geometry, type, element="element", type="type") - - def test_assigning_and_not_changing_representation_if_not_available(self, ifc, type): + def test_assigning_and_not_changing_representation_if_there_is_no_representation_to_change_to(self, ifc, type): ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() + type.has_material_usage("element").should_be_called().will_return(False) type.get_body_representation("element").should_be_called().will_return(None) type.get_any_representation("element").should_be_called().will_return(None) ifc.get_object("element").should_be_called().will_return("obj") type.disable_editing("obj").should_be_called() - subject.assign_type(ifc, geometry, type, element="element", type="type") + subject.assign_type(ifc, type, element="element", type="type") + + def test_updating_an_existing_body_if_there_is_a_parametric_material_usage(self, ifc, type): + ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() + + type.has_material_usage("element").should_be_called().will_return(True) + type.get_body_representation("element").should_be_called().will_return("representation") + type.get_representation_context("representation").should_be_called().will_return("context") + ifc.run( + "geometry.unassign_representation", product="element", representation="representation" + ).should_be_called() + ifc.run("geometry.remove_representation", representation="representation").should_be_called() + type.get_ifc_representation_class("element").should_be_called().will_return("class") + type.get_profile_set_usage("element").should_be_called().will_return("usage") + type.run_geometry_add_representation( + obj="obj", context="context", ifc_representation_class="class", profile_set_usage="usage" + ).should_be_called().will_return("mapped_rep") + + ifc.get_object("element").should_be_called().will_return("obj") + type.has_dynamic_voids("obj").should_be_called().will_return(False) + type.run_geometry_switch_representation( + obj="obj", representation="mapped_rep", should_reload=True, enable_dynamic_voids=False, is_global=False + ).should_be_called() + type.disable_editing("obj").should_be_called() + subject.assign_type(ifc, type, element="element", type="type") + + def test_creating_a_new_body_if_there_is_a_parametric_material_usage(self, ifc, type): + ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() + + type.has_material_usage("element").should_be_called().will_return(True) + type.get_body_representation("element").should_be_called().will_return(None) + type.get_body_context().should_be_called().will_return("context") + type.get_ifc_representation_class("element").should_be_called().will_return("class") + type.get_profile_set_usage("element").should_be_called().will_return("usage") + type.run_geometry_add_representation( + obj="obj", context="context", ifc_representation_class="class", profile_set_usage="usage" + ).should_be_called().will_return("mapped_rep") + + ifc.get_object("element").should_be_called().will_return("obj") + type.has_dynamic_voids("obj").should_be_called().will_return(False) + type.run_geometry_switch_representation( + obj="obj", representation="mapped_rep", should_reload=True, enable_dynamic_voids=False, is_global=False + ).should_be_called() + type.disable_editing("obj").should_be_called() + subject.assign_type(ifc, type, element="element", type="type") diff --git a/src/blenderbim/test/tool/test_type.py b/src/blenderbim/test/tool/test_type.py index 7aeffd4f8c..87c7f27f71 100644 --- a/src/blenderbim/test/tool/test_type.py +++ b/src/blenderbim/test/tool/test_type.py @@ -53,6 +53,16 @@ class TestGetAnyRepresentation(NewFile): assert subject.get_any_representation(element) == representation +class TestGetBodyContext(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + context = ifc.createIfcGeometricRepresentationSubContext( + ContextType="Model", ContextIdentifier="Body", TargetView="MODEL_VIEW" + ) + tool.Ifc.set(ifc) + assert subject.get_body_context() == context + + class TestGetBodyRepresentation(NewFile): def test_get_product_representation(self): ifc = ifcopenshell.file() @@ -78,6 +88,49 @@ class TestGetBodyRepresentation(NewFile): assert subject.get_body_representation(element) == body_rep +class TestGetIfcRepresentationClass(NewFile): + def test_detecting_profile_set_representations(self): + ifc = ifcopenshell.file() + element = ifc.createIfcColumn() + ifc.createIfcRelAssociatesMaterial( + RelatingMaterial=ifc.createIfcMaterialProfileSetUsage(), RelatedObjects=[element] + ) + assert subject.get_ifc_representation_class(element) == "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage" + + def test_detecting_layer_set_representations(self): + ifc = ifcopenshell.file() + element = ifc.createIfcColumn() + ifc.createIfcRelAssociatesMaterial( + RelatingMaterial=ifc.createIfcMaterialLayerSetUsage(), RelatedObjects=[element] + ) + assert ( + subject.get_ifc_representation_class(element) + == "IfcExtrudedAreaSolid/IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" + ) + + def test_returning_null_for_non_parametric_represntations(self): + ifc = ifcopenshell.file() + assert subject.get_ifc_representation_class(ifc.createIfcColumn()) is None + + +class TestGetProfileSetUsage(NewFile): + def test_getting_a_profile_set_usage(self): + ifc = ifcopenshell.file() + element = ifc.createIfcColumn() + assert subject.get_profile_set_usage(element) is None + usage = ifc.createIfcMaterialProfileSetUsage() + ifc.createIfcRelAssociatesMaterial(RelatingMaterial=usage, RelatedObjects=[element]) + assert subject.get_profile_set_usage(element) == usage + + +class TestGetRepresentationContext(NewFile): + def test_getting_a_profile_set_usage(self): + ifc = ifcopenshell.file() + context = ifc.createIfcGeometricRepresentationSubContext() + representation = ifc.createIfcShapeRepresentation(ContextOfItems=context) + assert subject.get_representation_context(representation) == context + + class TestHasDynamicVoids(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) @@ -87,3 +140,23 @@ class TestHasDynamicVoids(NewFile): obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) obj.modifiers.new("IfcOpeningElement", "BOOLEAN") assert subject.has_dynamic_voids(obj) is True + + +class TestHasMaterialUsage(NewFile): + def test_getting_a_profile_set_usage(self): + ifc = ifcopenshell.file() + element = ifc.createIfcColumn() + assert subject.has_material_usage(element) is False + usage = ifc.createIfcMaterialProfileSetUsage() + ifc.createIfcRelAssociatesMaterial(RelatingMaterial=usage, RelatedObjects=[element]) + assert subject.has_material_usage(element) is True + + +class TestRunGeometryAddRepresentation(NewFile): + def test_nothing(self): + pass + + +class TestRunGeometrySwitchRepresentation(NewFile): + def test_nothing(self): + pass diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py index 1288096963..60517f3312 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py @@ -22,7 +22,8 @@ class Usecase: ifcopenshell.util.element.remove_deep2( self.file, self.settings["representation"], - extra_subgraph_elements=list(styled_items | presentation_layer_assignments), + also_consider=list(styled_items | presentation_layer_assignments), + do_not_delete=self.file.by_type("IfcGeometricRepresentationContext"), ) for element in styled_items: diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 6c3eac9b15..9d496d8464 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -136,17 +136,21 @@ def remove_deep(ifc_file, element): ifc_file.unbatch() -def remove_deep2(ifc_file, element, extra_subgraph_elements=[]): +def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]): # Experimental remove deep proposal. No batch for now until this is more certain. See #1812. # ifc_file.batch() to_delete = set() subgraph = list(ifc_file.traverse(element, breadth_first=True)) - subgraph.extend(extra_subgraph_elements) + subgraph.extend(also_consider) subgraph_set = set(subgraph) subelement_queue = ifc_file.traverse(element, max_levels=1) while subelement_queue: subelement = subelement_queue.pop(0) - if subelement.id() and len(set(ifc_file.get_inverse(subelement)) - subgraph_set) == 0: + if ( + subelement.id() + and len(set(ifc_file.get_inverse(subelement)) - subgraph_set) == 0 + and subelement not in do_not_delete + ): to_delete.add(subelement) subelement_queue.extend(ifc_file.traverse(subelement, max_levels=1)[1:]) for subelement in to_delete: diff --git a/src/ifcopenshell-python/test/api/geometry/test_remove_representation.py b/src/ifcopenshell-python/test/api/geometry/test_remove_representation.py index da889be3db..8f5ddecdf7 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_remove_representation.py +++ b/src/ifcopenshell-python/test/api/geometry/test_remove_representation.py @@ -77,3 +77,9 @@ class TestRemoveRepresentation(test.bootstrap.IFC4): ifcopenshell.api.run("geometry.remove_representation", self.file, representation=representation) assert len(self.file.by_type("IfcPresentationLayerAssignment")) == 1 assert len(self.file.by_type("IfcShapeRepresentation")) == 1 + + def test_not_purging_geometric_representation_contexts(self): + context = self.file.createIfcGeometricRepresentationSubContext() + representation = self.file.createIfcShapeRepresentation(ContextOfItems=context) + ifcopenshell.api.run("geometry.remove_representation", self.file, representation=representation) + assert len(self.file.by_type("IfcGeometricRepresentationContext")) == 1