diff --git a/src/bonsai/bonsai/bim/module/bsdd/prop.py b/src/bonsai/bonsai/bim/module/bsdd/prop.py index 72fa337e00..0b2bfda6a1 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/prop.py +++ b/src/bonsai/bonsai/bim/module/bsdd/prop.py @@ -84,7 +84,7 @@ class BIMBSDDProperties(PropertyGroup): use_only_ifc_properties: BoolProperty( name="Only IFC Properties", description="Whether to display and assign only properties from IFC dictionary", - default=True, + default=False, ) load_preview_dictionaries: BoolProperty( name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False diff --git a/src/bonsai/test/bim/feature/bsdd.feature b/src/bonsai/test/bim/feature/bsdd.feature index 9deb49621b..29e6eb59e0 100644 --- a/src/bonsai/test/bim/feature/bsdd.feature +++ b/src/bonsai/test/bim/feature/bsdd.feature @@ -8,7 +8,7 @@ Scenario: Load bSDD dictionaries Then I see "Selected dictionary" And I see "LCA" in the "1st" list -Scenario: Add dictionaries as classification systems +Scenario: Add classification from bSDD - add all dictionaries Given an empty IFC project And I look at the "buildingSMART Data Dictionary" panel And I click "Load bSDD Dictionaries" @@ -21,7 +21,7 @@ Scenario: Add dictionaries as classification systems Then I see "LCA" And I see "BonsaiTestDict" -Scenario: Add a single dictionary as a classification systems +Scenario: Add classification from bSDD - add single dictionary Given an empty IFC project And I look at the "buildingSMART Data Dictionary" panel And I click "Load bSDD Dictionaries" @@ -32,3 +32,74 @@ Scenario: Add a single dictionary as a classification systems And I click "Add Classification From bSDD" Then I see "BonsaiTestDict" And I don't see "LCA" + +Scenario: Search bSDD classifications - search all dictionaries + Given an empty IFC project + And I look at the "buildingSMART Data Dictionary" panel + And I click "Load bSDD Dictionaries" + When I click "is_active" in the row where I see "LCA" in the "1st" list + And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list + And I select the object "IfcSite/My Site" + And I look at the "Classification References" panel + And I set the "classification_source" property to "buildingSMART Data Dictionary" + And I set the "active_dictionary" property to "All Dictionaries" + And I click "VIEWZOOM" + Then I see "Acidification" in the "1st" list + And I see "BonsaiReferenceA" in the "1st" list + +Scenario: Search bSDD classifications - search single dictionary + Given an empty IFC project + And I look at the "buildingSMART Data Dictionary" panel + And I click "Load bSDD Dictionaries" + And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list + And I select the object "IfcSite/My Site" + And I look at the "Classification References" panel + When I set the "classification_source" property to "buildingSMART Data Dictionary" + And I set the "active_dictionary" property to "BonsaiTestDict" + And I click "VIEWZOOM" + Then I see "BonsaiReferenceA" in the "1st" list + And I don't see "Acidification" in the "1st" list + +Scenario: Add classification reference from bSDD - add classification only + Given an empty IFC project + And I look at the "buildingSMART Data Dictionary" panel + And I click "Load bSDD Dictionaries" + When I click "is_active" in the row where I see "LCA" in the "1st" list + And I click "is_active" in the row where I see "BonsaiTestDict" in the "1st" list + And I select the object "IfcSite/My Site" + And I look at the "Classification References" panel + And I set the "classification_source" property to "buildingSMART Data Dictionary" + And I set the "active_dictionary" property to "All Dictionaries" + And I click "VIEWZOOM" + Then I see "Acidification" in the "1st" list + When I select the row where I see "Acidification" in the "1st" list + And I click "Add Classification Reference" + Then I see "Acidification" + And I don't see "BonsaiReferenceA" + +Scenario: Get bSDD classification properties - get associated properties then add classification + Given an empty IFC project + And I look at the "buildingSMART Data Dictionary" panel + And I click "Load bSDD Dictionaries" + When I click "is_active" in the row where I see "LCA" in the "1st" list + And I select the object "IfcSite/My Site" + And I look at the "Classification References" panel + And I set the "classification_source" property to "buildingSMART Data Dictionary" + And I set the "active_dictionary" property to "All Dictionaries" + And I click "VIEWZOOM" + When I select the row where I see "Acidification" in the "1st" list + And I click "COPY_ID" + Then I see "SizeSet" + And I see "Height" + And I see "Volume" + And I see "No References" + When I set the "Height" property to "42" + And I click "Add Classification Reference" + Then I see "Acidification" + And I don't see "No References" + When I look at the "Property Sets" panel + Then I see "SizeSet" + And I see "Height" + And I see "42" + And I see "Volume" + And I see "0" diff --git a/src/bonsai/test/bim/stub.py b/src/bonsai/test/bim/stub.py new file mode 100644 index 0000000000..a5c498f81e --- /dev/null +++ b/src/bonsai/test/bim/stub.py @@ -0,0 +1,274 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2025 Dion Moult +# +# This file is part of Bonsai. +# +# Bonsai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Bonsai is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Bonsai. If not, see . + + +from typing import Union, Any, Optional + + +class bSDDClientStub: + def get_dictionary(self, dictionary_uri=None, include_test_dictionaries=False): + dicts = { + "dictionaries": [ + { + "availableLanguages": [{"code": "EN", "name": "English"}], + "code": "LCA", + "uri": "https://identifier.buildingsmart.org/uri/LCA", + "name": "LCA indicators and modules", + "version": "3.0", + "organizationCodeOwner": "LCA", + "organizationNameOwner": "buildingSMART Sustainability Strategic Group", + "defaultLanguageCode": "EN", + "isLatestVersion": True, + "isVerified": False, + "isPrivate": False, + "license": "No license (rights reserved)", + "licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/", + "qualityAssuranceProcedure": "EN ISO 23386:2020", + "status": "Active", + "moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/", + "releaseDate": "2023-12-01T14:14:19Z", + "lastUpdatedUtc": "2023-12-01T14:17:53Z", + }, + { + "availableLanguages": [{"code": "EN", "name": "English"}], + "code": "BonsaiTestDict", + "uri": "https://identifier.buildingsmart.org/uri/BonsaiTestDict", + "name": "BonsaiTestDict", + "version": "3.0", + "organizationCodeOwner": "LCA", + "organizationNameOwner": "buildingSMART Sustainability Strategic Group", + "defaultLanguageCode": "EN", + "isLatestVersion": True, + "isVerified": False, + "isPrivate": False, + "license": "No license (rights reserved)", + "licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/", + "qualityAssuranceProcedure": "EN ISO 23386:2020", + "status": "Active", + "moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/", + "releaseDate": "2023-12-01T14:14:19Z", + "lastUpdatedUtc": "2023-12-01T14:17:53Z", + }, + ], + "totalCount": 2, + "offset": 0, + "count": 2, + } + if not dictionary_uri: + return dicts + for dictionary in dicts["dictionaries"]: + if dictionary["uri"] == dictionary_uri: + dicts["dictionaries"] = [dictionary] + return dicts + assert False, f"Could not find dictionary uri {dictionary_uri}" + + def get_classes( + self, + dictionary_uri: str, + use_nested_classes: bool = True, + class_type="Class", + search_text: str = "", + related_ifc_entity: str = "", + language_code: str = "", + version: int = 1, + offset=0, + limit=1000, + ): + classes = { + "classes": [], + "classesTotalCount": 2, + "classesOffset": 0, + "classesCount": 2, + "code": "LCA", + "uri": "https://identifier.buildingsmart.org/uri/LCA/LCA/3.0", + "name": "LCA indicators and modules", + "version": "3.0", + "organizationCodeOwner": "LCA", + "organizationNameOwner": "buildingSMART Sustainability Strategic Group", + "defaultLanguageCode": "EN", + "isLatestVersion": True, + "isVerified": False, + "isPrivate": False, + "license": "No license (rights reserved)", + "licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/", + "qualityAssuranceProcedure": "EN ISO 23386:2020", + "status": "Preview", + "moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/", + "releaseDate": "2023-12-01T14:14:19Z", + "lastUpdatedUtc": "2023-12-01T14:17:53Z", + } + if dictionary_uri == "https://identifier.buildingsmart.org/uri/LCA": + classes["classes"].extend( + [ + { + "uri": "https://identifier.buildingsmart.org/uri/LCA/LCA/3.0/class/Acidification", + "code": "Acidification", + "name": "Acidification", + "classType": "Class", + "referenceCode": "Acidification", + "descriptionPart": "", + }, + { + "uri": "https://identifier.buildingsmart.org/uri/LCA/LCA/3.0/class/Biogeniccarboncontentatthefactorygate", + "code": "Biogeniccarboncontentatthefactorygate", + "name": "Biogenic carbon content at the factory gate", + "classType": "Class", + "referenceCode": "Biogeniccarboncontentatthefactorygate", + "descriptionPart": "", + }, + ] + ) + elif dictionary_uri == "https://identifier.buildingsmart.org/uri/BonsaiTestDict": + classes["classes"].extend( + [ + { + "uri": "https://identifier.buildingsmart.org/uri/BonsaiTestDict/class/BonsaiReferenceA", + "code": "BonsaiReferenceA", + "name": "BonsaiReferenceA", + "classType": "Class", + "referenceCode": "BonsaiReferenceA", + "descriptionPart": "", + }, + { + "uri": "https://identifier.buildingsmart.org/uri/BonsaiTestDict/class/BonsaiReferenceB", + "code": "BonsaiReferenceB", + "name": "BonsaiReferenceB", + "classType": "Class", + "referenceCode": "BonsaiReferenceB", + "descriptionPart": "", + }, + ] + ) + if offset != 0: + classes["classes"].clear() + classes["classesTotalCount"] = 0 + classes["classesCount"] = 0 + return classes + + def get_class( + self, + class_uri: str, + include_class_properties: bool = True, + include_child_class_reference: bool = True, + include_class_relations: bool = True, + include_reverse_relations: bool = False, + reverse_relation_dictionary_uris: Optional[list[str]] = None, + language_code: str = "", + version: int = 1, + ): + result = { + "classType": "Class", + "referenceCode": "apple", + "relatedIfcEntityNames": ["IfcCommunicationsAppliance"], + "parentClassReference": { + "uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/fruit", + "name": "Fruit", + "code": "fruit", + }, + "hierarchy": [ + { + "level": 1, + "name": "Fruit", + "code": "fruit", + "uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/fruit", + }, + { + "level": 2, + "name": "Apple", + "code": "apple", + "uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/apple", + }, + ], + "classProperties": [ + { + "name": "Height", + "uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/apple/prop/SizeSet/height", + "description": "The height of an apple", + "definition": "Distance from bottom to top of something standing upright", + "dataType": "Real", + "dimension": "1 0 0 0 0 0 0", + "dimensionLength": 1, + "dimensionMass": 0, + "dimensionTime": 0, + "dimensionElectricCurrent": 0, + "dimensionThermodynamicTemperature": 0, + "dimensionAmountOfSubstance": 0, + "dimensionLuminousIntensity": 0, + "isRequired": True, + "isWritable": True, + "maxInclusive": 25, + "minExclusive": 1, + "physicalQuantity": "Height", + "propertyCode": "height", + "propertyDictionaryName": "Fruit and vegetables", + "propertyDictionaryUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1", + "propertyUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/prop/height", + "propertySet": "SizeSet", + "propertyStatus": "Active", + "propertyValueKind": "Single", + "units": ["cm"], + "qudtCodes": ["CentiM"], + }, + { + "name": "Volume", + "uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/apple/prop/SizeSet/volume", + "description": "The volume of an apple", + "definition": "Volume is a scalar quantity expressing the amount of three-dimensional space enclosed by a closed surface. Volume is often quantified numerically using the SI derived unit, the cubic metre", + "dataType": "Real", + "dimension": "3 0 0 0 0 0 0", + "dimensionLength": 3, + "dimensionMass": 0, + "dimensionTime": 0, + "dimensionElectricCurrent": 0, + "dimensionThermodynamicTemperature": 0, + "dimensionAmountOfSubstance": 0, + "dimensionLuminousIntensity": 0, + "example": "For example, the space that a substance or 3D shape occupies or contains.", + "isRequired": True, + "isWritable": True, + "maxInclusive": 20, + "minExclusive": 1, + "physicalQuantity": "Volume", + "propertyCode": "volume", + "propertyDictionaryName": "Fruit and vegetables", + "propertyDictionaryUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1", + "propertyUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/prop/volume", + "propertySet": "SizeSet", + "propertyStatus": "Active", + "propertyValueKind": "Single", + "units": ["cm³"], + "qudtCodes": ["CentiM3"], + }, + ], + "dictionaryUri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1", + "activationDateUtc": "2022-09-26T00:00:00Z", + "code": "apple", + "creatorLanguageCode": "en-GB", + "countriesOfUse": [], + "definition": "The round fruit of a tree of the rose family, which typically has thin green or red skin and crisp flesh", + "name": "Apple", + "uri": "https://identifier.buildingsmart.org/uri/bs-agri/fruitvegs/1.1/class/apple", + "replacedObjectCodes": [], + "replacingObjectCodes": [], + "revisionNumber": 0, + "status": "Active", + "subdivisionsOfUse": [], + "versionDateUtc": "2022-09-26T00:00:00Z", + "versionNumber": 1, + } + return result diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index ba3caa26d9..e6571b6f8f 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -22,6 +22,7 @@ import pytest import traceback import webbrowser import numpy as np +import test.bim.stub import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.representation @@ -50,65 +51,7 @@ variables = { webbrowser.open = lambda x: True -class bSDDClientStub: - def get_dictionary(self, dictionary_uri=None, include_test_dictionaries=False): - dicts = { - "dictionaries": [ - { - "availableLanguages": [{"code": "EN", "name": "English"}], - "code": "LCA", - "uri": "https://identifier.buildingsmart.org/uri/LCA/LCA/3.0", - "name": "LCA indicators and modules", - "version": "3.0", - "organizationCodeOwner": "LCA", - "organizationNameOwner": "buildingSMART Sustainability Strategic Group", - "defaultLanguageCode": "EN", - "isLatestVersion": True, - "isVerified": False, - "isPrivate": False, - "license": "No license (rights reserved)", - "licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/", - "qualityAssuranceProcedure": "EN ISO 23386:2020", - "status": "Active", - "moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/", - "releaseDate": "2023-12-01T14:14:19Z", - "lastUpdatedUtc": "2023-12-01T14:17:53Z", - }, - { - "availableLanguages": [{"code": "EN", "name": "English"}], - "code": "BonsaiTestDict", - "uri": "https://identifier.buildingsmart.org/uri/BonsaiTestDict", - "name": "BonsaiTestDict", - "version": "3.0", - "organizationCodeOwner": "LCA", - "organizationNameOwner": "buildingSMART Sustainability Strategic Group", - "defaultLanguageCode": "EN", - "isLatestVersion": True, - "isVerified": False, - "isPrivate": False, - "license": "No license (rights reserved)", - "licenseUrl": "https://technical.buildingsmart.org/services/bsdd/license/", - "qualityAssuranceProcedure": "EN ISO 23386:2020", - "status": "Active", - "moreInfoUrl": "https://www.lignum.ch/leistungen/projekte/buildingsmart-data-dictionary-bsdd/", - "releaseDate": "2023-12-01T14:14:19Z", - "lastUpdatedUtc": "2023-12-01T14:17:53Z", - } - ], - "totalCount": 2, - "offset": 0, - "count": 2, - } - if not dictionary_uri: - return dicts - for dictionary in dicts["dictionaries"]: - if dictionary["uri"] == dictionary_uri: - dicts["dictionaries"] = [dictionary] - return dicts - assert False, f"Could not find dictionary uri {dictionary_uri}" - - -tool.Bsdd.client = bSDDClientStub() +tool.Bsdd.client = test.bim.stub.bSDDClientStub() class PanelSpy: @@ -220,7 +163,7 @@ class OperatorSpy: class TemplateListSpy(PanelSpy): - def __init__(self, template_list: type[bpy.types.UIList], spied_data: dict): + def __init__(self, template_list: type[bpy.types.UIList], spied_data: dict) -> None: self.spied_data = spied_data self.items = getattr(self.spied_data["dataptr"], self.spied_data["propname"]) self.active_index = getattr(self.spied_data["active_dataptr"], self.spied_data["active_propname"]) @@ -234,6 +177,14 @@ class TemplateListSpy(PanelSpy): for item in self.items: self.rows.append(TemplateListItemSpy(self, item)) + def set_active_index(self, index: int) -> None: + self.active_index = index + setattr(self.spied_data["active_dataptr"], self.spied_data["active_propname"], self.active_index) + try: + self.active_item = self.items[self.active_index] + except: + assert False, f"Could not set active index {index}" + class TemplateListItemSpy(PanelSpy): def __init__(self, parent: TemplateListSpy, item): @@ -409,6 +360,27 @@ def i_see_text_in_the_nth_list(text, nth): assert False, f"Could not see '{text}' in any list. We saw:\n{debug}" +@given(parsers.parse('I don\'t see "{text}" in the "{nth}" list')) +@when(parsers.parse('I don\'t see "{text}" in the "{nth}" list')) +@then(parsers.parse('I don\'t see "{text}" in the "{nth}" list')) +def i_dont_see_text_in_the_nth_list(text, nth): + assert panel_spy + panel_spy.refresh_spy() + nth = int("".join([c for c in nth if c.isnumeric()])) + if len(panel_spy.spied_lists) < nth: + assert False, f"{nth} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}" + debug = [] + for i, template_list in enumerate(panel_spy.spied_lists): + if i + 1 != nth: + continue + for row in template_list.rows: + for l in row.spied_labels: + debug.append(l) + if text in l: + debug = "\n".join(debug) + assert False, f"We see saw '{text}' in the {nth} list but should not have. We saw:\n{debug}" + + @given(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list')) @when(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list')) @then(parsers.parse('I click "{button}" in the row where I see "{text}" in the "{nth}" list')) @@ -438,6 +410,32 @@ def i_click_button_in_the_row_where_i_see_text_in_the_nth_list(button, text, nth assert False, f"Could not see '{text}' in any list. We saw:\n{debug}" +@given(parsers.parse('I select the row where I see "{text}" in the "{nth}" list')) +@when(parsers.parse('I select the row where I see "{text}" in the "{nth}" list')) +@then(parsers.parse('I select the row where I see "{text}" in the "{nth}" list')) +def i_select_the_row_where_i_see_text_in_the_nth_list(text, nth): + assert panel_spy + panel_spy.refresh_spy() + nth = int("".join([c for c in nth if c.isnumeric()])) + if len(panel_spy.spied_lists) < nth: + assert False, f"{nth} list does not exist. Actual number of lists: {len(panel_spy.spied_lists)}" + debug = [] + for i, template_list in enumerate(panel_spy.spied_lists): + if i + 1 != nth: + continue + for i, row in enumerate(template_list.rows): + is_row = False + for l in row.spied_labels: + debug.append(l) + if text in l: + is_row = True + if is_row: + template_list.set_active_index(i) + return True + debug = "\n".join(debug) + assert False, f"Could not see '{text}' in any list. We saw:\n{debug}" + + @given(parsers.parse('I don\'t see "{text}"')) @when(parsers.parse('I don\'t see "{text}"')) @then(parsers.parse('I don\'t see "{text}"'))