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) <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-08-12 10:28:15 +10:00
parent 17042f6f80
commit b71354ce19
4 changed files with 38 additions and 5 deletions
+29 -2
View File
@@ -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):
+1 -1
View File
@@ -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
+7 -2
View File
@@ -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:
+1
View File
@@ -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)