Support assigning material sets as usages from UI #5933

It's on alt+click in Materials UI, see example - https://imgur.com/a/8gP7iAZ
This commit is contained in:
Andrej730
2025-01-15 18:40:33 +05:00
parent 2fdcb41c4e
commit 0ea513818b
3 changed files with 127 additions and 49 deletions
@@ -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,
)
@@ -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(
@@ -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