From 0ea513818bf018c8eab5dc647fda3e7a3ed7d317 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 15 Jan 2025 18:40:33 +0500 Subject: [PATCH] Support assigning material sets as usages from UI #5933 It's on alt+click in Materials UI, see example - https://imgur.com/a/8gP7iAZ --- .../bonsai/bim/module/material/operator.py | 26 +++- .../api/material/assign_material.py | 126 +++++++++++------- .../test/api/material/test_assign_material.py | 24 ++++ 3 files changed, 127 insertions(+), 49 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index c0908ead72..7baa6addc3 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -29,7 +29,7 @@ import bonsai.core.style import bonsai.core.material as core import bonsai.bim.module.model.profile as model_profile from bonsai.bim.ifc import IfcStore -from typing import Any, Union +from typing import Any, Union, TYPE_CHECKING class LoadMaterials(bpy.types.Operator): @@ -202,9 +202,17 @@ class RemoveMaterialSet(bpy.types.Operator, tool.Ifc.Operator): class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_material_to_selected" bl_label = "Assign Material To Selected" - bl_description = "Assign currently selected material in Materials UI to the selected objects" + bl_description = ( + "Assign currently selected material in Materials UI to the selected objects.\n\n" + "ALT+CLICK to assign material as a usage." + ) bl_options = {"REGISTER", "UNDO"} material: bpy.props.IntProperty(name="Material IFC ID") + assign_as_usage: bpy.props.BoolProperty( + name="Assign Material As A Usage", + default=False, + options={"SKIP_SAVE"}, + ) @classmethod def poll(cls, context): @@ -213,13 +221,25 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator): return False return True + def invoke(self, context, event): + if event.type == "LEFTMOUSE" and event.alt: + material_class = tool.Ifc.get().by_id(self.material).is_a() + if material_class not in ("IfcMaterialProfileSet", "IfcMaterialLayerSet"): + self.report({"ERROR"}, f"{material_class} cannot be assigned as a usage.") + return {"CANCELLED"} + self.assign_as_usage = True + return self.execute(context) + def _execute(self, context): material = tool.Ifc.get().by_id(self.material) objects = tool.Blender.get_selected_objects() + material_type = material.is_a() + if self.assign_as_usage: + material_type += "Usage" core.assign_material( tool.Ifc, tool.Material, - material_type=tool.Material.get_active_material_type(), + material_type=material_type, objects=objects, material=material, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py index 5303fdba56..fe10220a87 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py @@ -22,7 +22,8 @@ import ifcopenshell.api.material import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.representation -from typing import Optional, Union +from collections import defaultdict +from typing import Optional, Union, Any def assign_material( @@ -156,6 +157,9 @@ def assign_material( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.products: set[ifcopenshell.entity_instance] = set(self.settings["products"]) if not self.products: @@ -167,7 +171,9 @@ class Usecase: ifcopenshell.api.material.unassign_material(self.file, products=products_to_unassign_material) if self.settings["type"] == "IfcMaterial" or ( - self.settings["material"] and not self.settings["material"].is_a("IfcMaterial") + self.settings["material"] + and not self.settings["material"].is_a("IfcMaterial") + and not self.settings["type"].endswith("Usage") ): return self.assign_ifc_material() @@ -180,11 +186,6 @@ class Usecase: return self.create_material_association(material_set) elif self.settings["type"] == "IfcMaterialLayerSetUsage": - # NOTE: might return list of rels - types_to_products_layers_sets: dict[ - tuple[Union[ifcopenshell.entity_instance, None], str], tuple[ifcopenshell.entity_instance, list] - ] = dict() - AXIS3_CLASSES = [ "IfcSlab", "IfcSlabStandardCase", @@ -196,28 +197,47 @@ class Usecase: "IfcCovering", "IfcFurniture", ] + + provided_material_set = None + if self.settings["material"]: + provided_material_set = self.settings["material"] + material_set_class = provided_material_set.is_a() + assert ( + material_set_class == "IfcMaterialLayerSet" + ), f"{material_set_class} cannot be assiged as a IfcMaterialLayerSetUsage." + + layer_types_to_products: defaultdict[ + tuple[ifcopenshell.entity_instance, str], list[ifcopenshell.entity_instance] + ] + layer_types_to_products = defaultdict(list) + types_to_material_sets: dict[Union[ifcopenshell.entity_instance, None], ifcopenshell.entity_instance] + types_to_material_sets = {} + for product in self.products: - element_type = ifcopenshell.util.element.get_type(product) - layer_set_direction = "AXIS3" if product.is_a() in AXIS3_CLASSES else "AXIS2" - material_layer_type = (element_type, layer_set_direction) - - if material_layer_type in types_to_products_layers_sets: - types_to_products_layers_sets[material_layer_type][1].append(product) - continue - - if element_type: - element_type_material = ifcopenshell.util.element.get_material(element_type) - if element_type_material and element_type_material.is_a("IfcMaterialLayerSet"): - material_set = element_type_material - else: - material_set = self.file.create_entity("IfcMaterialLayerSet") + # Figure what material set to assign. + if provided_material_set is not None: + material_set = provided_material_set else: - material_set = self.file.create_entity("IfcMaterialLayerSet") - types_to_products_layers_sets[material_layer_type] = (material_set, [product]) + # If material set is not provided, derive it from the type. + element_type = ifcopenshell.util.element.get_type(product) + if element_type in types_to_material_sets: + material_set = types_to_material_sets[element_type] + else: + element_type_material = None + if element_type is not None: + element_type_material = ifcopenshell.util.element.get_material(element_type) + if element_type_material and element_type_material.is_a("IfcMaterialLayerSet"): + material_set = element_type_material + else: + material_set = self.file.create_entity("IfcMaterialLayerSet") + + layer_set_direction = "AXIS3" if product.is_a() in AXIS3_CLASSES else "AXIS2" + material_layer_type = (material_set, layer_set_direction) + layer_types_to_products[material_layer_type].append(product) rels = [ self.create_layer_set_usage(material_set, layer_set_direction, products) - for (_, layer_set_direction), (material_set, products) in types_to_products_layers_sets.items() + for (material_set, layer_set_direction), products in layer_types_to_products.items() ] return rels[0] if len(rels) == 1 else rels @@ -226,27 +246,41 @@ class Usecase: return self.create_material_association(material_set) elif self.settings["type"] == "IfcMaterialProfileSetUsage": - # NOTE: might return list of rels - types_to_products_profile_sets: dict[ - Union[ifcopenshell.entity_instance, None], tuple[ifcopenshell.entity_instance, list] - ] = dict() - for product in self.products: - element_type = ifcopenshell.util.element.get_type(product) - if element_type in types_to_products_profile_sets: - types_to_products_profile_sets[element_type][1].append(product) - continue - if element_type: - element_type_material = ifcopenshell.util.element.get_material(element_type) - if element_type_material and element_type_material.is_a("IfcMaterialProfileSet"): - material_set = element_type_material - else: - material_set = self.file.create_entity("IfcMaterialProfileSet") - else: - material_set = self.file.create_entity("IfcMaterialProfileSet") - types_to_products_profile_sets[element_type] = (material_set, [product]) + provided_material_set = None + if self.settings["material"]: + provided_material_set = self.settings["material"] + material_set_class = provided_material_set.is_a() + assert ( + material_set_class == "IfcMaterialProfileSet" + ), f"{material_set_class} cannot be assiged as a IfcMaterialProfileSetUsage." - rels = [] - for _, (material_set, products) in types_to_products_profile_sets.items(): + material_sets_to_products: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] + material_sets_to_products = defaultdict(list) + types_to_material_sets: dict[Union[ifcopenshell.entity_instance, None], ifcopenshell.entity_instance] + types_to_material_sets = {} + + for product in self.products: + # Figure what material set to assign. + if provided_material_set is not None: + material_set = provided_material_set + else: + # If material set is not provided, derive it from the type. + element_type = ifcopenshell.util.element.get_type(product) + if element_type in types_to_material_sets: + material_set = types_to_material_sets[element_type] + else: + element_type_material = None + if element_type is not None: + element_type_material = ifcopenshell.util.element.get_material(element_type) + if element_type_material and element_type_material.is_a("IfcMaterialProfileSet"): + material_set = element_type_material + else: + material_set = self.file.create_entity("IfcMaterialProfileSet") + + material_sets_to_products[material_set].append(product) + + rels: list[ifcopenshell.entity_instance] = [] + for material_set, products in material_sets_to_products.items(): self.update_representation_profile(material_set, products) material_set_usage = self.create_profile_set_usage(material_set) rels.append(self.create_material_association(material_set_usage, products)) @@ -286,7 +320,7 @@ class Usecase: "LayerSetDirection": layer_set_direction, "DirectionSense": "POSITIVE", "OffsetFromReferenceLine": 0, - } + }, ) return self.create_material_association(usage, products) @@ -317,7 +351,7 @@ class Usecase: "OwnerHistory": ifcopenshell.api.owner.create_owner_history(self.file), "RelatedObjects": products, "RelatingMaterial": relating_material, - } + }, ) def get_rel_associates_material( diff --git a/src/ifcopenshell-python/test/api/material/test_assign_material.py b/src/ifcopenshell-python/test/api/material/test_assign_material.py index 822ac57803..2baeedcb50 100644 --- a/src/ifcopenshell-python/test/api/material/test_assign_material.py +++ b/src/ifcopenshell-python/test/api/material/test_assign_material.py @@ -139,6 +139,18 @@ class TestAssignMaterialIFC2X3(test.bootstrap.IFC2X3): assert material_list.Materials[0] == material assert ifcopenshell.util.element.get_material(element2) == material_list + def test_assign_element_material_layer_set_usage_with_provided_material_set(self): + element_type1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element1], relating_type=element_type1) + ifcopenshell.api.material.assign_material(self.file, products=[element_type1], type="IfcMaterialLayerSet") + + provided_material_set = self.file.create_entity("IfcMaterialLayerSet") + ifcopenshell.api.material.assign_material( + self.file, products=[element1], material=provided_material_set, type="IfcMaterialLayerSetUsage" + ) + assert ifcopenshell.util.element.get_material(element1, should_skip_usage=True) == provided_material_set + class TestAssignMaterialIFC4(test.bootstrap.IFC4, TestAssignMaterialIFC2X3): def test_assign_type_material_profile_set(self): @@ -204,3 +216,15 @@ class TestAssignMaterialIFC4(test.bootstrap.IFC4, TestAssignMaterialIFC2X3): assert material_set.is_a("IfcMaterialConstituentSet") assert not material_set.MaterialConstituents assert ifcopenshell.util.element.get_material(element2) == material_set + + def test_assign_element_material_profile_set_usage_with_provided_material_set(self): + element_type1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element1], relating_type=element_type1) + ifcopenshell.api.material.assign_material(self.file, products=[element_type1], type="IfcMaterialProfileSet") + + provided_material_set = self.file.create_entity("IfcMaterialProfileSet") + ifcopenshell.api.material.assign_material( + self.file, products=[element1], material=provided_material_set, type="IfcMaterialProfileSetUsage" + ) + assert ifcopenshell.util.element.get_material(element1, should_skip_usage=True) == provided_material_set