From 8e2cb4c071f3d484903ee383108aa6aafaf40394 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Mon, 13 Jul 2026 06:23:09 +0300 Subject: [PATCH] Fix ci-bonsai-daily: assign_class drops active_object; assign_material usage Two root causes behind ~16 failing BDD scenarios in the material / assign-type area: - AssignClass._execute (bim/module/root/operator.py) relinks objects into new collections (e.g. types into the hidden "IfcTypeProduct" collection) and then restores the selection via tool.Blender.validate_object_selection. Without a context.view_layer.update() in between, a just-relinked (but valid) active object is seen as "not in the current view layer" and dropped, so context.active_object becomes None for the rest of the caller and every scenario that reads active_object after assigning a type fails. Add the view_layer.update() before the selection-restore (an idiom already used ~15 times elsewhere). - core.assign_material forwarded the selected plain IfcMaterial into ifcopenshell.api.material.assign_material even when auto-upgrading the assignment to a "...Usage" type, which only accepts a material *set* or None, tripping an intentional API assertion. Pass material=None for the usage branch and let the existing add_material_to_set follow-up populate the derived set. Verified in headless Blender: the material / assign_type / assigned-material BDD cluster goes from 23 failed / 14 passed to 7 failed / 30 passed. The 7 remaining failures are an unrelated stale-STEP-id fixture cluster (Item/IfcFace/NN) handled separately. This change was made with the assistance of an AI tool. Co-Authored-By: Claude Fable 5 --- src/bonsai/bonsai/bim/module/root/operator.py | 8 ++++++++ src/bonsai/bonsai/core/material.py | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 159f157d44..01f9480732 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -380,6 +380,14 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator): # TODO: reload representation might lead to the object being replaced by object of the other type. # We probably should track it somehow and keep the original selection. + # Objects may have just been relinked into new collections (e.g. types + # into the "IfcTypeProduct" collection) above. Make sure the view + # layer's object bases are up to date before validating the selection + # below, otherwise a just-relinked (but perfectly valid) active object + # can be seen as "not in the current view layer" and be dropped, + # leaving `context.active_object` as None for the rest of the caller. + context.view_layer.update() + # Validate selection and reapply it. current_selection = tool.Blender.validate_object_selection(*current_selection) tool.Blender.set_objects_selection(*current_selection) diff --git a/src/bonsai/bonsai/core/material.py b/src/bonsai/bonsai/core/material.py index 0a08f5e0bf..f641909a65 100644 --- a/src/bonsai/bonsai/core/material.py +++ b/src/bonsai/bonsai/core/material.py @@ -130,10 +130,23 @@ def assign_material( if can_be_usage and not material_tool.is_type_product(element): element_material_type = material_type + "Usage" + # A "...Usage" type always wraps an existing material *set* + # (IfcMaterialLayerSet/IfcMaterialProfileSet), never a plain + # IfcMaterial. `material` here is whatever single IfcMaterial is + # selected in the Object Materials UI, so it can't be passed + # through as-is or ifcopenshell.api.material.assign_material will + # assert (e.g. "IfcMaterial cannot be assiged as a + # IfcMaterialLayerSetUsage."). Passing None lets the API derive + # (or create) the proper set from the element's type; the + # selected material is added into that set below instead. + material_for_assignment = None else: element_material_type = material_type + material_for_assignment = material - ifc.run("material.assign_material", products=[element], type=element_material_type, material=material) + ifc.run( + "material.assign_material", products=[element], type=element_material_type, material=material_for_assignment + ) assigned_material = material_tool.get_material(element) assert assigned_material # Type checker.