From b71354ce199e7961c70255dcbd05f078add3039c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 12 Aug 2026 10:28:15 +1000 Subject: [PATCH] Fix assigning a plain material to an occurrence as a layer set Assigning a material to an occurrence with a set material type has raised "IfcMaterial cannot be assiged as a IfcMaterialLayerSetUsage" since the default changed to assigning usages to occurrences. The type is upgraded to a usage but the material is passed on unchanged, and material.assign_material only accepts a material for a usage when that material is already the set, whereas the Object Materials dropdown gives us a plain IfcMaterial. Pass nothing in that case and let the API make the set, as it does when asked for a usage with no material. Look the set up past the usage afterwards, so the material the user picked is added to it. get_material returns the usage, which is not a material set, so neither branch of the repair below matched and the picked material was dropped, leaving the set empty. This is a stopgap and is commented as such: the real problem is that assign_material builds sets with no items in them and ignores the material it was given, which is not valid IFC and leaves callers patching up after it. Also register "I evaluate expression" as a Then step. It has only ever been a Given and a When, so the last line of the scenario covering this could never run; it is the only Then of its kind in the suite. test/bim goes from 16 failures to 15, with none introduced. Co-Authored-By: Claude Opus 5 (1M context) --- src/bonsai/bonsai/core/material.py | 31 +++++++++++++++++++++++++++-- src/bonsai/bonsai/core/tool.py | 2 +- src/bonsai/bonsai/tool/material.py | 9 +++++++-- src/bonsai/test/bim/test_feature.py | 1 + 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/core/material.py b/src/bonsai/bonsai/core/material.py index 96c624de64..b70bed7bff 100644 --- a/src/bonsai/bonsai/core/material.py +++ b/src/bonsai/bonsai/core/material.py @@ -142,8 +142,35 @@ def assign_material( else: element_material_type = material_type - ifc.run("material.assign_material", products=[element], type=element_material_type, material=material) - assigned_material = material_tool.get_material(element) + # TODO: this whole dance is a stopgap and wants rewriting. + # + # material.assign_material creates material sets with no items in them, + # ignoring the material it was handed -- an IfcMaterialLayerSet with no + # MaterialLayers is not valid IFC, since the list is mandatory and + # [1:?]. So we repair it below, after the fact. Worse, the API rejects a + # plain IfcMaterial outright when asked for a usage, which is exactly + # what the Object Materials dropdown gives us, so we cannot even pass it + # on and have to let the API invent an empty set and then fill it in. + # + # The fix is for assign_material to build the set around the material it + # is given, rather than leaving an invalid one behind for its callers to + # patch up. That is a wider change than it looks: add_material_set has + # the same behaviour, and the create-empty-then-add-items idiom is + # spread through the API's own docstrings, examples and tests. Until + # that is untangled, keep the repair here where it is at least visible. + + # Only a usage refuses a plain IfcMaterial; every other type still wants + # it, and IfcMaterial and IfcMaterialList cannot be created without it. + pass_material = material_tool.is_a_material_set(material) or not element_material_type.endswith("Usage") + ifc.run( + "material.assign_material", + products=[element], + type=element_material_type, + material=material if pass_material else None, + ) + # A usage points at the set rather than being one, and it is the set + # that needs an item adding to it below. + assigned_material = material_tool.get_material(element, should_skip_usage=True) assert assigned_material # Type checker. if material_tool.is_a_material_set(material): diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 6280e78309..7ba5e715db 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -651,7 +651,7 @@ class Material: def get_default_material(cls): pass def get_elements_by_material(cls, material): pass def get_material_attributes(cls): pass - def get_material(cls, element, should_inherit: bool = False): pass + def get_material(cls, element, should_inherit: bool = False, should_skip_usage: bool = False): pass def get_object_ui_active_material(cls): pass def get_object_ui_material_type(cls): pass def get_style(cls, material): pass diff --git a/src/bonsai/bonsai/tool/material.py b/src/bonsai/bonsai/tool/material.py index 300df8d4ce..695759227c 100644 --- a/src/bonsai/bonsai/tool/material.py +++ b/src/bonsai/bonsai/tool/material.py @@ -220,9 +220,14 @@ class Material(bonsai.core.tool.Material): @classmethod def get_material( - cls, element: ifcopenshell.entity_instance, should_inherit: bool = False + cls, + element: ifcopenshell.entity_instance, + should_inherit: bool = False, + should_skip_usage: bool = False, ) -> Union[ifcopenshell.entity_instance, None]: - return ifcopenshell.util.element.get_material(element, should_inherit=should_inherit) + return ifcopenshell.util.element.get_material( + element, should_inherit=should_inherit, should_skip_usage=should_skip_usage + ) @classmethod def is_a_material_set(cls, material: ifcopenshell.entity_instance) -> bool: diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 34150fbe98..88697b7c92 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -1000,6 +1000,7 @@ def i_click_button_and_expect_error_error_msg(button, error_msg): @given(parsers.parse('I evaluate expression "{expression}"')) @when(parsers.parse('I evaluate expression "{expression}"')) +@then(parsers.parse('I evaluate expression "{expression}"')) def i_evaluate_expression(expression): expression = replace_variables(expression) exec(expression)