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.core.material as core
import bonsai.bim.module.model.profile as model_profile import bonsai.bim.module.model.profile as model_profile
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
from typing import Any, Union from typing import Any, Union, TYPE_CHECKING
class LoadMaterials(bpy.types.Operator): 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): class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_material_to_selected" bl_idname = "bim.assign_material_to_selected"
bl_label = "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"} bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material IFC ID") 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 @classmethod
def poll(cls, context): def poll(cls, context):
@@ -213,13 +221,25 @@ class AssignMaterialToSelected(bpy.types.Operator, tool.Ifc.Operator):
return False return False
return True 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): def _execute(self, context):
material = tool.Ifc.get().by_id(self.material) material = tool.Ifc.get().by_id(self.material)
objects = tool.Blender.get_selected_objects() objects = tool.Blender.get_selected_objects()
material_type = material.is_a()
if self.assign_as_usage:
material_type += "Usage"
core.assign_material( core.assign_material(
tool.Ifc, tool.Ifc,
tool.Material, tool.Material,
material_type=tool.Material.get_active_material_type(), material_type=material_type,
objects=objects, objects=objects,
material=material, material=material,
) )
@@ -22,7 +22,8 @@ import ifcopenshell.api.material
import ifcopenshell.guid import ifcopenshell.guid
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.representation import ifcopenshell.util.representation
from typing import Optional, Union from collections import defaultdict
from typing import Optional, Union, Any
def assign_material( def assign_material(
@@ -156,6 +157,9 @@ def assign_material(
class Usecase: class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self): def execute(self):
self.products: set[ifcopenshell.entity_instance] = set(self.settings["products"]) self.products: set[ifcopenshell.entity_instance] = set(self.settings["products"])
if not self.products: if not self.products:
@@ -167,7 +171,9 @@ class Usecase:
ifcopenshell.api.material.unassign_material(self.file, products=products_to_unassign_material) ifcopenshell.api.material.unassign_material(self.file, products=products_to_unassign_material)
if self.settings["type"] == "IfcMaterial" or ( 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() return self.assign_ifc_material()
@@ -180,11 +186,6 @@ class Usecase:
return self.create_material_association(material_set) return self.create_material_association(material_set)
elif self.settings["type"] == "IfcMaterialLayerSetUsage": 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 = [ AXIS3_CLASSES = [
"IfcSlab", "IfcSlab",
"IfcSlabStandardCase", "IfcSlabStandardCase",
@@ -196,28 +197,47 @@ class Usecase:
"IfcCovering", "IfcCovering",
"IfcFurniture", "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: for product in self.products:
element_type = ifcopenshell.util.element.get_type(product) # Figure what material set to assign.
layer_set_direction = "AXIS3" if product.is_a() in AXIS3_CLASSES else "AXIS2" if provided_material_set is not None:
material_layer_type = (element_type, layer_set_direction) material_set = provided_material_set
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")
else: else:
material_set = self.file.create_entity("IfcMaterialLayerSet") # If material set is not provided, derive it from the type.
types_to_products_layers_sets[material_layer_type] = (material_set, [product]) 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 = [ rels = [
self.create_layer_set_usage(material_set, layer_set_direction, products) 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 return rels[0] if len(rels) == 1 else rels
@@ -226,27 +246,41 @@ class Usecase:
return self.create_material_association(material_set) return self.create_material_association(material_set)
elif self.settings["type"] == "IfcMaterialProfileSetUsage": elif self.settings["type"] == "IfcMaterialProfileSetUsage":
# NOTE: might return list of rels provided_material_set = None
types_to_products_profile_sets: dict[ if self.settings["material"]:
Union[ifcopenshell.entity_instance, None], tuple[ifcopenshell.entity_instance, list] provided_material_set = self.settings["material"]
] = dict() material_set_class = provided_material_set.is_a()
for product in self.products: assert (
element_type = ifcopenshell.util.element.get_type(product) material_set_class == "IfcMaterialProfileSet"
if element_type in types_to_products_profile_sets: ), f"{material_set_class} cannot be assiged as a IfcMaterialProfileSetUsage."
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])
rels = [] material_sets_to_products: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]
for _, (material_set, products) in types_to_products_profile_sets.items(): 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) self.update_representation_profile(material_set, products)
material_set_usage = self.create_profile_set_usage(material_set) material_set_usage = self.create_profile_set_usage(material_set)
rels.append(self.create_material_association(material_set_usage, products)) rels.append(self.create_material_association(material_set_usage, products))
@@ -286,7 +320,7 @@ class Usecase:
"LayerSetDirection": layer_set_direction, "LayerSetDirection": layer_set_direction,
"DirectionSense": "POSITIVE", "DirectionSense": "POSITIVE",
"OffsetFromReferenceLine": 0, "OffsetFromReferenceLine": 0,
} },
) )
return self.create_material_association(usage, products) return self.create_material_association(usage, products)
@@ -317,7 +351,7 @@ class Usecase:
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(self.file), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(self.file),
"RelatedObjects": products, "RelatedObjects": products,
"RelatingMaterial": relating_material, "RelatingMaterial": relating_material,
} },
) )
def get_rel_associates_material( def get_rel_associates_material(
@@ -139,6 +139,18 @@ class TestAssignMaterialIFC2X3(test.bootstrap.IFC2X3):
assert material_list.Materials[0] == material assert material_list.Materials[0] == material
assert ifcopenshell.util.element.get_material(element2) == material_list 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): class TestAssignMaterialIFC4(test.bootstrap.IFC4, TestAssignMaterialIFC2X3):
def test_assign_type_material_profile_set(self): 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 material_set.is_a("IfcMaterialConstituentSet")
assert not material_set.MaterialConstituents assert not material_set.MaterialConstituents
assert ifcopenshell.util.element.get_material(element2) == material_set 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