Resolve nested complex quantity paths in the selector (#2041)

get_element_value could not reach the members of an IfcPhysicalComplexQuantity
(or IfcComplexProperty) by their natural path. util.element expands a complex
quantity into a dict whose nested members live under a "properties" sub-dict,
but the selector's dict navigation only looked at the top level, so
"Qto_Custom.Layer1.Width" returned None and IfcCsv exported nothing for it.
Only the internal "Qto_Custom.Layer1.properties.Width" path worked.

When a key is not a direct member of the value dict, descend into its
"properties" sub-dict so nested quantities/properties resolve with the
natural "Set.Complex.Nested" path. Direct keys still take priority, so the
explicit ".properties." path stays backward compatible and the regex branch
is untouched.

Verified: Qto_Custom.Layer1.Width -> 0.1 and Layer1.Height -> 2.5 (were
None), the sibling simple NetArea still resolves, the legacy .properties.
path still works, and IfcCsv now exports the nested value. test_selector.py:
38 passed (adds test_selecting_a_nested_complex_quantity).

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Petru Conduraru
2026-07-12 13:21:32 +03:00
committed by Massimo Fabbro
parent d63a99b70c
commit 21122c0d28
2 changed files with 39 additions and 1 deletions
@@ -540,8 +540,15 @@ def _get_element_value(element: ifcopenshell.entity_instance, keys: list[str]) -
value = results or None
if value and len(value) == 1:
value = value[0]
elif key in value:
value = value[key]
else:
value = value.get(key, None)
# A nested complex quantity/property (IfcPhysicalComplexQuantity /
# IfcComplexProperty) is represented as a dict whose nested members
# live under a "properties" sub-dict. Descend into it so that nested
# values are reachable with the natural "Qto.Complex.Nested" path.
subprops = value.get("properties")
value = subprops.get(key, None) if isinstance(subprops, dict) else None
elif isinstance(value, (list, tuple, set)): # If we use regex
if isinstance(key, str) and key.isnumeric():
try:
@@ -195,6 +195,37 @@ class TestGetElementValue(test.bootstrap.IFC4):
assert subject.get_element_value(element, "/Pset_.*Common/.Status") == ["New"]
assert subject.get_element_value(element, "/Pset_.*Common/.Status.0") == "New"
def test_selecting_a_nested_complex_quantity(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
complex_quantity = self.file.create_entity(
"IfcPhysicalComplexQuantity",
Name="Layer1",
Discrimination="layer",
HasQuantities=[
self.file.create_entity("IfcQuantityLength", Name="Width", LengthValue=0.1),
self.file.create_entity("IfcQuantityLength", Name="Height", LengthValue=2.5),
],
)
quantity = self.file.create_entity(
"IfcElementQuantity",
GlobalId=ifcopenshell.guid.new(),
Name="Qto_Custom",
Quantities=[complex_quantity, self.file.create_entity("IfcQuantityArea", Name="NetArea", AreaValue=5.0)],
)
self.file.create_entity(
"IfcRelDefinesByProperties",
GlobalId=ifcopenshell.guid.new(),
RelatedObjects=[element],
RelatingPropertyDefinition=quantity,
)
# A simple quantity in the same set still resolves normally.
assert subject.get_element_value(element, "Qto_Custom.NetArea") == 5.0
# Nested quantities of a complex quantity are reachable with the natural path.
assert subject.get_element_value(element, "Qto_Custom.Layer1.Width") == 0.1
assert subject.get_element_value(element, "Qto_Custom.Layer1.Height") == 2.5
# The explicit "properties" path is preserved for backwards compatibility.
assert subject.get_element_value(element, "Qto_Custom.Layer1.properties.Width") == 0.1
class TestFilterElements(test.bootstrap.IFC4):
def test_selecting_by_globalid(self):