From 3a8facf01965050c13907a9435efab54330a519f Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sun, 12 Jul 2026 08:37:44 +0300 Subject: [PATCH] Bonsai: copy-property-to-selection must not edit the inherited type pset (#7412) "Copy property to selection" edited the value on the element's TYPE instead of creating an override on the occurrence. core.copy_property_to_selection resolved the target via tool.Pset.get_element_pset, which uses ifcopenshell.util.element.get_pset with should_inherit=True (the default). When the occurrence has no pset of its own but its type does, get_pset returns the type's pset, so pset.edit_pset then changed the value for the type and every occurrence sharing it. Give get_element_pset a should_inherit parameter (default True, so the ~30 other callers are unchanged) and pass should_inherit=False from copy_property_to_selection. Now only the element's own pset is considered; if none exists the existing fallback creates an override pset on the occurrence. The core interface stub and the four Prophecy mock expectations are updated to match. Verified live in headless Blender: with a type-level Pset_Custom.MyProp, copying MyProp to two occurrences leaves the type value untouched and creates an own-pset override on each occurrence. Core tests (test_pset.py) pass. Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 --- src/bonsai/bonsai/core/pset.py | 6 +++++- src/bonsai/bonsai/core/tool.py | 2 +- src/bonsai/bonsai/tool/pset.py | 4 ++-- src/bonsai/test/core/test_pset.py | 8 ++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/core/pset.py b/src/bonsai/bonsai/core/pset.py index 62517b7ab2..04df419e68 100644 --- a/src/bonsai/bonsai/core/pset.py +++ b/src/bonsai/bonsai/core/pset.py @@ -40,7 +40,11 @@ def copy_property_to_selection( element = ifc.get_entity(obj) if not element: return - ifc_pset = pset.get_element_pset(element, pset_name) + # Only consider the element's own property set, never one inherited from its + # type. Otherwise copying an occurrence override would edit the type's pset + # (and thus every occurrence) instead of creating/updating the override on + # this element. See https://github.com/IfcOpenShell/IfcOpenShell/issues/7412. + ifc_pset = pset.get_element_pset(element, pset_name, should_inherit=False) if not ifc_pset: ifc_pset = ifc.run("pset.add_pset" if is_pset else "pset.add_qto", product=element, name=pset_name) if is_pset: diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 7056e5ec34..7448616ce7 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -814,7 +814,7 @@ class Pset: def cast_string_to_primitive(cls, value: str): pass def clear_blender_pset_properties(cls, props): pass def enable_proposed_pset(cls, props, pset_name, pset_type, has_template): pass - def get_element_pset(cls, element, pset_name): pass + def get_element_pset(cls, element, pset_name, should_inherit=True): pass def get_prop_template_primitive_type(cls, prop_template): pass def get_pset_name(cls, obj, obj_type, pset_type): pass def get_pset_template(cls, name): pass diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py index aa72a85e6f..c26677fee7 100644 --- a/src/bonsai/bonsai/tool/pset.py +++ b/src/bonsai/bonsai/tool/pset.py @@ -70,9 +70,9 @@ class Pset(bonsai.core.tool.Pset): @classmethod def get_element_pset( - cls, element: ifcopenshell.entity_instance, pset_name: str + cls, element: ifcopenshell.entity_instance, pset_name: str, should_inherit: bool = True ) -> Union[ifcopenshell.entity_instance, None]: - pset = ifcopenshell.util.element.get_pset(element, pset_name) + pset = ifcopenshell.util.element.get_pset(element, pset_name, should_inherit=should_inherit) if pset: return tool.Ifc.get().by_id(pset["id"]) diff --git a/src/bonsai/test/core/test_pset.py b/src/bonsai/test/core/test_pset.py index 9ebf82bb8d..e96e432196 100644 --- a/src/bonsai/test/core/test_pset.py +++ b/src/bonsai/test/core/test_pset.py @@ -29,7 +29,7 @@ class TestCopyPropertyToSelection: def test_copying_the_property_to_an_existing_pset(self, ifc, pset): ifc.get_entity("obj").should_be_called().will_return("element") - pset.get_element_pset("element", "pset_name").should_be_called().will_return("pset") + pset.get_element_pset("element", "pset_name", should_inherit=False).should_be_called().will_return("pset") ifc.run("pset.edit_pset", pset="pset", properties={"prop_name": "prop_value"}).should_be_called() subject.copy_property_to_selection( ifc, pset, is_pset=True, obj="obj", pset_name="pset_name", prop_name="prop_name", prop_value="prop_value" @@ -37,7 +37,7 @@ class TestCopyPropertyToSelection: def test_creating_a_new_pset_if_it_doesnt_exist(self, ifc, pset): ifc.get_entity("obj").should_be_called().will_return("element") - pset.get_element_pset("element", "pset_name").should_be_called().will_return(None) + pset.get_element_pset("element", "pset_name", should_inherit=False).should_be_called().will_return(None) ifc.run("pset.add_pset", product="element", name="pset_name").should_be_called().will_return("pset") ifc.run("pset.edit_pset", pset="pset", properties={"prop_name": "prop_value"}).should_be_called() subject.copy_property_to_selection( @@ -46,7 +46,7 @@ class TestCopyPropertyToSelection: def test_copying_the_quantity_to_an_existing_qto(self, ifc, pset): ifc.get_entity("obj").should_be_called().will_return("element") - pset.get_element_pset("element", "qto_name").should_be_called().will_return("qto") + pset.get_element_pset("element", "qto_name", should_inherit=False).should_be_called().will_return("qto") ifc.run("pset.edit_qto", qto="qto", properties={"prop_name": "prop_value"}).should_be_called() subject.copy_property_to_selection( ifc, pset, is_pset=False, obj="obj", pset_name="qto_name", prop_name="prop_name", prop_value="prop_value" @@ -54,7 +54,7 @@ class TestCopyPropertyToSelection: def test_creating_a_new_qto_if_it_doesnt_exist(self, ifc, pset): ifc.get_entity("obj").should_be_called().will_return("element") - pset.get_element_pset("element", "qto_name").should_be_called().will_return(None) + pset.get_element_pset("element", "qto_name", should_inherit=False).should_be_called().will_return(None) ifc.run("pset.add_qto", product="element", name="qto_name").should_be_called().will_return("qto") ifc.run("pset.edit_qto", qto="qto", properties={"prop_name": "prop_value"}).should_be_called() subject.copy_property_to_selection(