From b9c99b35a562883813a6bfa1228ade38585f72d3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 25 Mar 2023 22:07:18 +1100 Subject: [PATCH] Fix #2881. Support added for referencing nested relationships and material set items. --- src/blenderbim/blenderbim/tool/drawing.py | 7 ++- .../ifcopenshell/util/selector.py | 34 ++++++----- .../test/util/test_selector.py | 56 ++++++++++++++++++- 3 files changed, 79 insertions(+), 18 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 95717916c9..c30f270c68 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -1042,9 +1042,10 @@ class Drawing(blenderbim.core.tool.Drawing): if not product: return text for variable in re.findall("{{.*?}}", text): - text = text.replace( - variable, str(ifcopenshell.util.selector.get_element_value(product, variable[2:-2]) or "") - ) + value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2]) + if isinstance(value, (list, tuple)): + value = ", ".join(str(v) for v in value) + text = text.replace(variable, str(value)) return text @classmethod diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 89aa2bd269..4723ef5903 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -26,9 +26,9 @@ import ifcopenshell.util.element def get_element_value(element, query): l = lark.Lark( """start: WORD | ESCAPED_STRING | keys_regex | keys_quoted | keys_simple - keys_regex: "r" ESCAPED_STRING "." ESCAPED_STRING - keys_quoted: ESCAPED_STRING "." ESCAPED_STRING - keys_simple: /[^\\W][^.=<>]*[^\\W]/ "." /[^\\W][^.=<>]*[^\\W]/ + keys_regex: "r" ESCAPED_STRING ("." ESCAPED_STRING)* + keys_quoted: ESCAPED_STRING ("." ESCAPED_STRING)* + keys_simple: /[^\\W][^.=<>!%*\\]]*/ ("." /[^\\W][^.=<>!%*\\]]*/)* // Embed common.lark for packaging _STRING_INNER: /.*?/ @@ -63,9 +63,9 @@ class Selector: filter: "[" filter_key (comparison filter_value)? "]" filter_key: WORD | ESCAPED_STRING | keys_regex | keys_quoted | keys_simple filter_value: ESCAPED_STRING | SIGNED_FLOAT | SIGNED_INT | BOOLEAN | NULL - keys_regex: "r" ESCAPED_STRING "." ESCAPED_STRING - keys_quoted: ESCAPED_STRING "." ESCAPED_STRING - keys_simple: /[^\\W][^.=<>]*[^\\W]/ "." /[^\\W][^.=<>]*[^\\W]/ + keys_regex: "r" ESCAPED_STRING ("." ESCAPED_STRING)* + keys_quoted: ESCAPED_STRING ("." ESCAPED_STRING)* + keys_simple: /[^\\W][^.=<>!%*\\]]*/ ("." /[^\\W][^.=<>!%*\\]]*/)* lfunction: and | or inverse_relationship: types | decomposed_by | bounded_by | grouped_by types: "*" @@ -243,21 +243,29 @@ class Selector: keys = [keys] elif keys.data == "keys_regex": is_regex = True - keys = [keys.children[0][1:-1].replace("\\\"", '"'), keys.children[1][1:-1].replace("\\\"", '"')] + keys = [k[1:-1].replace("\\\"", '"') for k in keys.children] elif keys.data == "keys_quoted": - keys = [keys.children[0][1:-1].replace("\\\"", '"'), keys.children[1][1:-1].replace("\\\"", '"')] + keys = [k[1:-1].replace("\\\"", '"') for k in keys.children] elif keys.data == "keys_simple": - keys = [keys.children[0], keys.children[1]] + keys = keys.children return {"keys": keys, "is_regex": is_regex} @classmethod def get_element_value(cls, element, keys, is_regex=False): value = element for key in keys: + key = key.strip() if key == "type": - value = ifcopenshell.util.element.get_type(element) - elif key == "material": - value = ifcopenshell.util.element.get_material(element, should_skip_usage=True) + value = ifcopenshell.util.element.get_type(value) + elif key in ("material", "mat"): + value = ifcopenshell.util.element.get_material(value, should_skip_usage=True) + elif key in ("item", "i"): + if value.is_a("IfcMaterialLayerSet"): + value = value.MaterialLayers + elif value.is_a("IfcMaterialProfileSet"): + value = value.MaterialProfiles + elif value.is_a("IfcMaterialConstituentSet"): + value = value.MaterialConstituents elif key == "container": value = ifcopenshell.util.element.get_container(element) elif isinstance(value, ifcopenshell.entity_instance): @@ -287,7 +295,7 @@ class Selector: value = results else: value = value.get(key, None) - elif isinstance(value, list): # If we use regex + elif isinstance(value, (list, tuple)): # If we use regex results = [] for v in value: subvalue = cls.get_element_value(v, [key], is_regex=is_regex) diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 2991f71b7c..ec6368ae8f 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -28,6 +28,27 @@ class TestGetElementValue(test.bootstrap.IFC4): element.Name = "Foobar" assert subject.get_element_value(element, "Name") == "Foobar" + def test_selecting_using_a_multiple_key_query(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + material2 = ifcopenshell.api.run("material.add_material", self.file, name="CON02") + material_set = ifcopenshell.api.run( + "material.add_material_set", self.file, name="FOO", set_type="IfcMaterialLayerSet" + ) + layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) + layer.Name = "L1" + layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) + layer.Name = "L2" + ifcopenshell.api.run("material.edit_layer", self.file, layer=layer, attributes={"LayerThickness": 13}) + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + assert subject.get_element_value(element, "material.MaterialLayers.Name") == ["L1", "L2"] + # Allow to use "item" to generically select an item in a material set + assert subject.get_element_value(element, "material.item.Name") == ["L1", "L2"] + assert subject.get_element_value(element, '"material"."item"."Name"') == ["L1", "L2"] + assert subject.get_element_value(element, 'r"material"."item"."Name"') == ["L1", "L2"] + # Provide shortform for convenience + assert subject.get_element_value(element, "mat.i.Name") == ["L1", "L2"] + class TestSelector(test.bootstrap.IFC4): def test_selecting_by_class(self): @@ -149,7 +170,7 @@ class TestSelector(test.bootstrap.IFC4): pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="a !%$§&/()?|*-+,€~#@µ^°a") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"a !%$§&/()?|*-+,€~#@µ^°a": "Bar"}) assert subject.Selector.parse( - self.file, '.IfcElement[a !%$§&/()?|*-+,€~#@µ^°a.a !%$§&/()?|*-+,€~#@µ^°a="Bar"]' + self.file, '.IfcElement["a !%$§&/()?|*-+,€~#@µ^°a"."a !%$§&/()?|*-+,€~#@µ^°a"="Bar"]' ) == [element] def test_selecting_a_property_which_includes_a_dot(self): @@ -212,9 +233,40 @@ class TestSelector(test.bootstrap.IFC4): ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Baz"}) assert subject.Selector.parse(self.file, '.IfcElement[r"Foo.*"."F.*"="Bar"]') == [element] - def test_selecting_a_property_via_a_type_attribute(self): + def test_selecting_an_attribute_via_a_type(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") element_type.Name = "Foo" ifcopenshell.api.run("type.assign_type", self.file, related_object=element, relating_type=element_type) assert set(subject.Selector.parse(self.file, '.IfcWall[type.Name="Foo"]')) == {element} + + def test_selecting_via_a_material(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + assert set(subject.Selector.parse(self.file, '.IfcWall[material.Name="CON01"]')) == {element} + + def test_selecting_via_a_material_set(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + material_set = ifcopenshell.api.run( + "material.add_material_set", self.file, name="FOO", set_type="IfcMaterialLayerSet" + ) + layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) + ifcopenshell.api.run("material.edit_layer", self.file, layer=layer, attributes={"LayerThickness": 13}) + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + assert set(subject.Selector.parse(self.file, '.IfcWall[material.LayerSetName="FOO"]')) == {element} + + def test_selecting_via_a_material_set_item(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file, name="CON01") + material2 = ifcopenshell.api.run("material.add_material", self.file, name="CON02") + material_set = ifcopenshell.api.run( + "material.add_material_set", self.file, name="FOO", set_type="IfcMaterialLayerSet" + ) + layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material) + layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=material_set, material=material2) + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material_set) + assert set(subject.Selector.parse(self.file, '.IfcWall[material.item.Material.Name="CON01"]')) == {element} + assert set(subject.Selector.parse(self.file, '.IfcWall[material.item.Material.Name="CON02"]')) == {element} + assert set(subject.Selector.parse(self.file, '.IfcWall[material.item.Material.Name="CON03"]')) == set()