mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
Bonsai: preserve material usage direction across a type change (#6676)
Changing an element's type flattened its material layer set direction, e.g. a covering (or wall) with LayerSetDirection AXIS2 became AXIS3. The underlying ifcopenshell.api recreates the IfcMaterialLayerSetUsage from scratch and defaults its direction from the occurrence class (AXIS3_CLASSES includes IfcCovering). Bonsai's core.assign_type already records the old usage attributes and restores them, but the restore was gated behind `model.get_usage_type(type)`, which returns None for classes it does not special-case (e.g. IfcCoveringType), so the restore was skipped and the AXIS3 default stuck. Ungate the restore so it runs whenever usage attributes were recorded, independent of get_usage_type(type). restore_material_usage_attributes is self-guarding (it only writes when the element still carries a usage of the recorded type), so this is safe for types with no recognized usage. Also drops a redundant get_usage_type call. Verified live in headless Blender on the reporter's model: an IfcCovering with a manually set AXIS2 usage kept AXIS2 (and its DirectionSense/offset) across a type change, where before it flattened to AXIS3; reassigning to the same or another type preserves the direction with no regression. Core test_type.py: 5 passed (incl. a new regression test). Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,13 @@ def assign_type(
|
||||
usage_attributes = type_tool.record_material_usage_attributes(element)
|
||||
ifc.run("type.assign_type", related_objects=[element], relating_type=type)
|
||||
obj = ifc.get_object(element)
|
||||
if (usage := model.get_usage_type(type)) and usage_attributes:
|
||||
# Reassigning the type recreates the material usage from scratch, defaulting
|
||||
# its LayerSetDirection/DirectionSense/offset to the values derived from the
|
||||
# occurrence class (e.g. AXIS3 for IfcCovering). Restore the recorded usage
|
||||
# attributes so a manually-set direction (e.g. AXIS2) is preserved. The
|
||||
# restore is a no-op when the element no longer carries a matching usage, so
|
||||
# it is safe regardless of what get_usage_type() reports for the new type.
|
||||
if usage_attributes:
|
||||
type_tool.restore_material_usage_attributes(element, usage_attributes)
|
||||
if (usage := model.get_usage_type(type)) == "PROFILE":
|
||||
model.regenerate_profile(obj)
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestAssignType:
|
||||
def test_assigning_and_switching_to_an_existing_type_data(self, ifc, model, type):
|
||||
type.record_material_usage_attributes("element").should_be_called().will_return(None)
|
||||
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
|
||||
model.get_usage_type("type").should_be_called(2).will_return(None)
|
||||
model.get_usage_type("type").should_be_called().will_return(None)
|
||||
ifc.get_object("type").should_be_called().will_return("type_obj")
|
||||
type.get_object_data("type_obj").should_be_called().will_return("type_obj_data")
|
||||
type.change_object_data("obj", "type_obj_data", is_global=False).should_be_called()
|
||||
@@ -35,13 +35,32 @@ class TestAssignType:
|
||||
def test_assigning_and_not_changing_data_if_the_type_has_no_data(self, ifc, model, type):
|
||||
type.record_material_usage_attributes("element").should_be_called().will_return(None)
|
||||
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
|
||||
model.get_usage_type("type").should_be_called(2).will_return(None)
|
||||
model.get_usage_type("type").should_be_called().will_return(None)
|
||||
ifc.get_object("type").should_be_called().will_return("type_obj")
|
||||
type.get_object_data("type_obj").should_be_called().will_return(None)
|
||||
ifc.get_object("element").should_be_called().will_return("obj")
|
||||
type.disable_editing("obj").should_be_called()
|
||||
subject.assign_type(ifc, model, type, element="element", type="type")
|
||||
|
||||
def test_restoring_recorded_material_usage_attributes_when_the_type_has_no_recognized_usage(
|
||||
self, ifc, model, type
|
||||
):
|
||||
# Regression test for #6676: changing the type of an element carrying a
|
||||
# material usage (e.g. an IfcCovering with LayerSetDirection AXIS2)
|
||||
# recreates the usage with a class-default direction. The recorded usage
|
||||
# attributes must be restored even when get_usage_type() does not
|
||||
# recognize the new type's class (returns None), so the direction is
|
||||
# preserved instead of flattening (AXIS2 -> AXIS3).
|
||||
type.record_material_usage_attributes("element").should_be_called().will_return({"type": "usage"})
|
||||
ifc.run("type.assign_type", related_objects=["element"], relating_type="type").should_be_called()
|
||||
ifc.get_object("element").should_be_called().will_return("obj")
|
||||
type.restore_material_usage_attributes("element", {"type": "usage"}).should_be_called()
|
||||
model.get_usage_type("type").should_be_called().will_return(None)
|
||||
ifc.get_object("type").should_be_called().will_return("type_obj")
|
||||
type.get_object_data("type_obj").should_be_called().will_return(None)
|
||||
type.disable_editing("obj").should_be_called()
|
||||
subject.assign_type(ifc, model, type, element="element", type="type")
|
||||
|
||||
|
||||
class TestPurgeUnusedTypes:
|
||||
def test_purge_types_obj_found(self, ifc, type, geometry):
|
||||
|
||||
Reference in New Issue
Block a user