mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 01:11:40 +00:00
Fix #6669. bSDD now supports test dictionaries, refactor domain to dictionary, support zero length keyword searches, multi-dict support, and basic tests.
Feature tests can now have more sophisticated per UIList item spies.
This commit is contained in:
@@ -21,14 +21,13 @@ from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.GetBSDDClassificationProperties,
|
||||
operator.LoadBSDDDomains,
|
||||
operator.LoadBSDDDictionaries,
|
||||
operator.SearchBSDDClass,
|
||||
operator.SetActiveBSDDDictionary,
|
||||
prop.BSDDDomain,
|
||||
prop.BSDDDictionary,
|
||||
prop.BSDDClassification,
|
||||
prop.BSDDPset,
|
||||
prop.BIMBSDDProperties,
|
||||
ui.BIM_UL_bsdd_domains,
|
||||
ui.BIM_UL_bsdd_dictionaries,
|
||||
ui.BIM_UL_bsdd_classifications,
|
||||
ui.BIM_PT_bsdd,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.date
|
||||
import ifcopenshell.util.classification
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def refresh():
|
||||
BSDDData.is_loaded = False
|
||||
|
||||
|
||||
class BSDDData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data["active_dictionary"] = cls.active_dictionary()
|
||||
|
||||
@classmethod
|
||||
def active_dictionary(cls):
|
||||
props = tool.Bsdd.get_bsdd_props()
|
||||
results = [("ALL", "All Dictionaries", "All active dictionaries")]
|
||||
results.extend([(d.uri, d.name, f"{d.status} - {d.version}") for d in props.dictionaries if d.is_active])
|
||||
return results
|
||||
@@ -22,25 +22,13 @@ import bonsai.tool as tool
|
||||
from bonsai.core import bsdd as core
|
||||
|
||||
|
||||
class LoadBSDDDomains(bpy.types.Operator):
|
||||
bl_idname = "bim.load_bsdd_domains"
|
||||
class LoadBSDDDictionaries(bpy.types.Operator):
|
||||
bl_idname = "bim.load_bsdd_dictionaries"
|
||||
bl_label = "Load bSDD Dictionaries"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
core.load_bsdd(bsdd.Client(), tool.Bsdd)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetActiveBSDDDictionary(bpy.types.Operator):
|
||||
bl_idname = "bim.set_active_bsdd_domain"
|
||||
bl_label = "Set bSDD Dictionary as active"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
uri: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
core.set_active_bsdd_dictionary(self.name, self.uri, tool.Bsdd)
|
||||
core.load_bsdd(tool.Bsdd)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -50,17 +38,9 @@ class SearchBSDDClass(bpy.types.Operator):
|
||||
bl_description = "Search for bSDD classes by the provided keyword"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Requirement by buildingSMART bSDD api.
|
||||
if len(context.scene.BIMBSDDProperties.keyword) < 3:
|
||||
cls.poll_message_set("Search query has to be at least 3 characters long.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
keyword = context.scene.BIMBSDDProperties.keyword
|
||||
classes_found = core.search_class(keyword, bsdd.Client(), tool.Bsdd)
|
||||
classes_found = core.search_class(keyword, tool.Bsdd)
|
||||
self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -72,7 +52,7 @@ class GetBSDDClassificationProperties(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
pset_data = core.get_class_properties(bsdd.Client(), tool.Bsdd)
|
||||
pset_data = core.get_class_properties(tool.Bsdd)
|
||||
psets_found = len(pset_data)
|
||||
props_found = sum(len(pset) for pset in pset_data.values())
|
||||
self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.")
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
from bpy.types import PropertyGroup
|
||||
from bonsai.bim.module.bsdd.data import BSDDData
|
||||
from bonsai.bim.prop import Attribute, StrProperty
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -31,21 +32,34 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
class BSDDDomain(PropertyGroup):
|
||||
def get_active_dictionary(self, context):
|
||||
if not BSDDData.is_loaded:
|
||||
BSDDData.load()
|
||||
return BSDDData.data["active_dictionary"]
|
||||
|
||||
|
||||
def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None:
|
||||
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
|
||||
|
||||
|
||||
class BSDDDictionary(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
uri: StringProperty(name="URI")
|
||||
default_language_code: StringProperty(name="Language")
|
||||
organization_name_owner: StringProperty(name="Organization")
|
||||
status: StringProperty(name="Status")
|
||||
version: StringProperty(name="Version")
|
||||
is_active: BoolProperty(
|
||||
name="Is Active", description="Enable to search with this dictionary", default=False, update=update_is_active
|
||||
)
|
||||
|
||||
|
||||
class BSDDClassification(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
reference_code: StringProperty(name="Reference Code")
|
||||
uri: StringProperty(name="Namespace URI")
|
||||
domain_name: StringProperty(name="Domain Name")
|
||||
domain_namespace_uri: StringProperty(name="Domain Namespace URI")
|
||||
dictionary_name: StringProperty(name="Dictionary Name")
|
||||
dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI")
|
||||
|
||||
|
||||
class BSDDPset(PropertyGroup):
|
||||
@@ -54,10 +68,11 @@ class BSDDPset(PropertyGroup):
|
||||
|
||||
|
||||
class BIMBSDDProperties(PropertyGroup):
|
||||
active_domain: StringProperty(name="Active Domain")
|
||||
active_dictionary: StringProperty(name="Active Dictionary")
|
||||
active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary")
|
||||
active_uri: StringProperty(name="Active URI")
|
||||
domains: CollectionProperty(name="Domains", type=BSDDDomain)
|
||||
active_domain_index: IntProperty(name="Active Domain Index")
|
||||
dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary)
|
||||
active_dictionary_index: IntProperty(name="Active Dictionary Index")
|
||||
classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
|
||||
active_classification_index: IntProperty(name="Active Classification Index")
|
||||
keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive")
|
||||
@@ -71,7 +86,13 @@ class BIMBSDDProperties(PropertyGroup):
|
||||
description="Whether to display and assign only properties from IFC dictionary",
|
||||
default=True,
|
||||
)
|
||||
load_preview_domains: BoolProperty(
|
||||
name="Load Preview Domains", description="Whether it should load preview and inactive domains", default=False
|
||||
load_preview_dictionaries: BoolProperty(
|
||||
name="Load Preview Dictionaries", description="Load dictionaries marked as Preview status", default=False
|
||||
)
|
||||
load_inactive_dictionaries: BoolProperty(
|
||||
name="Load Inactive Dictionaries", description="Load dictionaries marked as Inactive status", default=False
|
||||
)
|
||||
load_test_dictionaries: BoolProperty(
|
||||
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
|
||||
)
|
||||
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.bsdd.data import BSDDData
|
||||
from bpy.types import Panel, UIList
|
||||
|
||||
|
||||
@@ -30,65 +31,54 @@ class BIM_PT_bsdd(Panel):
|
||||
bl_parent_id = "BIM_PT_tab_project_setup"
|
||||
|
||||
def draw(self, context):
|
||||
if not BSDDData.is_loaded:
|
||||
BSDDData.load()
|
||||
props = context.scene.BIMBSDDProperties
|
||||
layout = self.layout
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "load_preview_domains")
|
||||
if len(props.domains):
|
||||
row.operator("bim.load_bsdd_domains", text="", icon="FILE_REFRESH")
|
||||
|
||||
if props.active_domain:
|
||||
if len(props.dictionaries):
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + props.active_domain, icon="URL")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
row.operator("bim.load_bsdd_dictionaries", icon="FILE_REFRESH")
|
||||
|
||||
if len(props.domains):
|
||||
if len(props.dictionaries):
|
||||
self.layout.template_list(
|
||||
"BIM_UL_bsdd_domains",
|
||||
"BIM_UL_bsdd_dictionaries",
|
||||
"",
|
||||
props,
|
||||
"domains",
|
||||
"dictionaries",
|
||||
props,
|
||||
"active_domain_index",
|
||||
"active_dictionary_index",
|
||||
)
|
||||
if 0 <= props.active_domain_index < len(props.domains):
|
||||
selected_domain = props.domains[props.active_domain_index]
|
||||
if 0 <= props.active_dictionary_index < len(props.dictionaries):
|
||||
selected_dictionary = props.dictionaries[props.active_dictionary_index]
|
||||
else:
|
||||
selected_domain = None
|
||||
selected_dictionary = None
|
||||
|
||||
if selected_domain:
|
||||
layout.label(text="Selected domain:")
|
||||
if selected_dictionary:
|
||||
layout.label(text="Selected dictionary:")
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
row.label(text="Language")
|
||||
row.label(text=selected_domain.default_language_code)
|
||||
row.label(text=selected_dictionary.default_language_code)
|
||||
row = box.row(align=True)
|
||||
row.label(text="Version")
|
||||
row.label(text=selected_domain.version)
|
||||
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_domain.uri
|
||||
row.label(text=selected_dictionary.version)
|
||||
box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_dictionary.uri
|
||||
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.load_bsdd_domains")
|
||||
row.operator("bim.load_bsdd_dictionaries")
|
||||
|
||||
|
||||
class BIM_UL_bsdd_domains(UIList):
|
||||
class BIM_UL_bsdd_dictionaries(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
props = context.scene.BIMBSDDProperties
|
||||
row = layout.row(align=True)
|
||||
if item.status != "Active":
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner}) - {item.status}", icon="ERROR")
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner}) v{item.version} - {item.status}", icon="ERROR")
|
||||
else:
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner})")
|
||||
if item.uri == props.active_uri:
|
||||
row.label(text="", icon="URL")
|
||||
else:
|
||||
op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.name = item.name
|
||||
op.uri = item.uri
|
||||
row.label(text=f"{item.name} ({item.organization_name_owner}) v{item.version}")
|
||||
row.prop(item, "is_active", icon="CHECKBOX_HLT" if item.is_active else "CHECKBOX_DEHLT", text="", emboss=False)
|
||||
|
||||
|
||||
class BIM_UL_bsdd_classifications(UIList):
|
||||
|
||||
@@ -116,28 +116,26 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMBSDDProperties
|
||||
domain = next((d for d in props.domains if d.name == props.active_domain and d.uri == props.active_uri), None)
|
||||
|
||||
# Maybe user loaded preview domains, set it as active
|
||||
# and then reloaded them without preview domains.
|
||||
if not domain:
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Couldn't find domain '{props.active_domain}' ({props.active_uri}). Try to reload bSDD dictionaries.",
|
||||
if props.active_dictionary == "ALL":
|
||||
dictionaries = [d.uri for d in props.dictionaries if d.is_active]
|
||||
else:
|
||||
dictionaries = [props.active_dictionary]
|
||||
for uri in dictionaries:
|
||||
if not (dictionary := tool.Bsdd.get_dictionary(uri)):
|
||||
continue
|
||||
has_classification = False
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if element.Name == dictionary["name"] or (tool.Classification.get_location(element) == dictionary["uri"]):
|
||||
self.report({"INFO"}, f"Classification '{dictionary['name']}' is already added to the project.")
|
||||
has_classification = True
|
||||
if has_classification:
|
||||
continue
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=dictionary["name"]
|
||||
)
|
||||
return
|
||||
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if element.Name == props.active_domain or (tool.Classification.get_location(element) == domain.uri):
|
||||
self.report({"INFO"}, f"Classification '{props.active_domain}' is already added to the project.")
|
||||
return
|
||||
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=props.active_domain
|
||||
)
|
||||
classification.Source = domain.organization_name_owner
|
||||
classification.Edition = domain.version
|
||||
tool.Classification.set_location(classification, domain.uri)
|
||||
classification.Source = dictionary["organizationNameOwner"]
|
||||
classification.Edition = dictionary["version"]
|
||||
tool.Classification.set_location(classification, dictionary["uri"])
|
||||
|
||||
|
||||
class EnableAddingManualClassification(bpy.types.Operator):
|
||||
@@ -420,17 +418,17 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
classification = None
|
||||
for element in tool.Ifc.get().by_type("IfcClassification"):
|
||||
if element.Name == bsdd_classification.domain_name or (
|
||||
tool.Classification.get_location(element) == bsdd_classification.domain_namespace_uri
|
||||
if element.Name == bsdd_classification.dictionary_name or (
|
||||
tool.Classification.get_location(element) == bsdd_classification.dictionary_namespace_uri
|
||||
):
|
||||
classification = element
|
||||
break
|
||||
|
||||
if not classification:
|
||||
classification = ifcopenshell.api.run(
|
||||
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name
|
||||
"classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.dictionary_name
|
||||
)
|
||||
tool.Classification.set_location(classification, bsdd_classification.domain_namespace_uri)
|
||||
tool.Classification.set_location(classification, bsdd_classification.dictionary_namespace_uri)
|
||||
|
||||
for obj in objects:
|
||||
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
|
||||
|
||||
@@ -78,13 +78,8 @@ class BIM_PT_classifications(Panel):
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
self.bprops = context.scene.BIMBSDDProperties
|
||||
|
||||
if not self.bprops.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
|
||||
row.prop(self.bprops, "active_dictionary", text="")
|
||||
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_classification_from_bsdd", icon="ADD")
|
||||
@@ -163,13 +158,8 @@ class ReferenceUI:
|
||||
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
|
||||
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
if not self.bprops.active_domain:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Active bSDD Domain", icon="ERROR")
|
||||
return
|
||||
|
||||
row = self.layout.row()
|
||||
row.label(text="Active: " + self.bprops.active_domain, icon="URL")
|
||||
row.prop(self.bprops, "active_dictionary", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.bprops, "keyword", text="")
|
||||
|
||||
@@ -435,6 +435,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
props = tool.Project.get_project_props()
|
||||
layout.prop(props, "should_disable_undo_on_save")
|
||||
layout.prop(props, "should_stream")
|
||||
bprops = tool.Bsdd.get_bsdd_props()
|
||||
layout.prop(bprops, "load_preview_dictionaries")
|
||||
layout.prop(bprops, "load_inactive_dictionaries")
|
||||
layout.prop(bprops, "load_test_dictionaries")
|
||||
|
||||
|
||||
# Scene panel groups
|
||||
|
||||
@@ -26,9 +26,9 @@ if TYPE_CHECKING:
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
def get_class_properties(client: bsdd.Client, bsdd: tool.Bsdd) -> dict[str, dict[str, Any]]:
|
||||
def get_class_properties(bsdd: tool.Bsdd) -> dict[str, dict[str, Any]]:
|
||||
bsdd.clear_class_psets()
|
||||
data = bsdd.get_active_class_data(client)
|
||||
data = bsdd.get_active_class_data()
|
||||
pset_dict = bsdd.get_property_dict(data)
|
||||
if pset_dict is None:
|
||||
return {}
|
||||
@@ -36,23 +36,12 @@ def get_class_properties(client: bsdd.Client, bsdd: tool.Bsdd) -> dict[str, dict
|
||||
return pset_dict
|
||||
|
||||
|
||||
def load_bsdd(client: bsdd.Client, bsdd: tool.Bsdd) -> None:
|
||||
bsdd.clear_domains()
|
||||
if bsdd.should_load_preview_domains():
|
||||
dictionaries = bsdd.get_dictionaries(client)
|
||||
else:
|
||||
dictionaries = bsdd.get_dictionaries(client, "Active")
|
||||
bsdd.create_dictionaries(dictionaries)
|
||||
def load_bsdd(bsdd: tool.Bsdd) -> None:
|
||||
bsdd.clear_dictionaries()
|
||||
bsdd.create_dictionaries(bsdd.get_dictionaries())
|
||||
|
||||
|
||||
def search_class(keyword: str, client: bsdd.Client, bsdd: tool.Bsdd) -> int:
|
||||
def search_class(keyword: str, bsdd: tool.Bsdd) -> int:
|
||||
bsdd.clear_classes()
|
||||
related_entities = bsdd.get_related_ifc_entities()
|
||||
active_dictionary_uri = bsdd.get_active_dictionary_uri()
|
||||
classes = bsdd.search_class(client, keyword, [active_dictionary_uri], related_entities)
|
||||
bsdd.create_classes(classes)
|
||||
return len(classes)
|
||||
|
||||
|
||||
def set_active_bsdd_dictionary(name: str, uri: str, bsdd: tool.Bsdd) -> None:
|
||||
bsdd.set_active_bsdd(name, uri)
|
||||
return bsdd.search_class(keyword, related_entities)
|
||||
|
||||
@@ -3,10 +3,16 @@ import bonsai.tool as tool
|
||||
import bpy
|
||||
import json
|
||||
import bsdd
|
||||
from typing import Any, Union, Optional
|
||||
from typing import Any, Union, Optional, TYPE_CHECKING
|
||||
|
||||
|
||||
class Bsdd(bonsai.core.tool.Bsdd):
|
||||
client = bsdd.Client()
|
||||
|
||||
@classmethod
|
||||
def get_bsdd_props(cls):
|
||||
return bpy.context.scene.BIMBSDDProperties
|
||||
|
||||
@classmethod
|
||||
def clear_class_psets(cls) -> None:
|
||||
bpy.context.scene.BIMBSDDProperties.classification_psets.clear()
|
||||
@@ -16,8 +22,8 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
bpy.context.scene.BIMBSDDProperties.classifications.clear()
|
||||
|
||||
@classmethod
|
||||
def clear_domains(cls) -> None:
|
||||
bpy.context.scene.BIMBSDDProperties.domains.clear()
|
||||
def clear_dictionaries(cls) -> None:
|
||||
bpy.context.scene.BIMBSDDProperties.dictionaries.clear()
|
||||
|
||||
@classmethod
|
||||
def create_class_psets(cls, pset_dict: dict[str, dict[str, Any]]) -> None:
|
||||
@@ -42,22 +48,11 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
new2.ifc_class = data["ifc_class"]
|
||||
new2.metadata = data["dictionary"]
|
||||
|
||||
@classmethod
|
||||
def create_classes(cls, class_dict: list[bsdd.ClassSearchResponseClassContractV1]) -> None:
|
||||
props = bpy.context.scene.BIMBSDDProperties
|
||||
for _class in sorted(class_dict, key=lambda c: c["referenceCode"]):
|
||||
prop = props.classifications.add()
|
||||
prop.name = _class["name"]
|
||||
prop.reference_code = _class["referenceCode"]
|
||||
prop.uri = _class["uri"]
|
||||
prop.domain_name = _class["dictionaryName"]
|
||||
prop.domain_namespace_uri = _class["dictionaryUri"]
|
||||
|
||||
@classmethod
|
||||
def create_dictionaries(cls, dictionaries: list[bsdd.DictionaryContractV1]) -> None:
|
||||
props = bpy.context.scene.BIMBSDDProperties
|
||||
for dictionary in sorted(dictionaries, key=lambda d: d["name"]):
|
||||
new = props.domains.add()
|
||||
new = props.dictionaries.add()
|
||||
new.name = dictionary["name"]
|
||||
new.uri = dictionary["uri"]
|
||||
new.default_language_code = dictionary["defaultLanguageCode"]
|
||||
@@ -66,24 +61,35 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
new.version = dictionary["version"]
|
||||
|
||||
@classmethod
|
||||
def get_active_class_data(cls, client: bsdd.Client) -> Union[bsdd.ClassContractV1, dict]:
|
||||
def get_active_class_data(cls) -> Union[bsdd.ClassContractV1, dict]:
|
||||
prop = bpy.context.scene.BIMBSDDProperties
|
||||
bsdd_classification = prop.classifications[prop.active_classification_index]
|
||||
if not bsdd_classification:
|
||||
return {}
|
||||
return client.get_class(bsdd_classification.uri)
|
||||
return cls.client.get_class(bsdd_classification.uri)
|
||||
|
||||
@classmethod
|
||||
def get_active_dictionary_uri(cls) -> str:
|
||||
return bpy.context.scene.BIMBSDDProperties.active_uri
|
||||
|
||||
@classmethod
|
||||
def get_dictionaries(cls, client: bsdd.Client, status: Optional[str] = None) -> list[bsdd.DictionaryContractV1]:
|
||||
response = client.get_dictionary()
|
||||
def get_dictionary(cls, uri: str) -> bsdd.DictionaryContractV1:
|
||||
props = bpy.context.scene.BIMBSDDProperties
|
||||
response = cls.client.get_dictionary(dictionary_uri=uri, include_test_dictionaries=props.load_test_dictionaries)
|
||||
if dicts := response.get("dictionaries"):
|
||||
return dicts[0]
|
||||
|
||||
@classmethod
|
||||
def get_dictionaries(cls) -> list[bsdd.DictionaryContractV1]:
|
||||
props = bpy.context.scene.BIMBSDDProperties
|
||||
response = cls.client.get_dictionary(include_test_dictionaries=props.load_test_dictionaries)
|
||||
dicts = response.get("dictionaries") or []
|
||||
if status is not None:
|
||||
dicts = list(filter(lambda d: d["status"] == status, dicts))
|
||||
return dicts
|
||||
statuses = ["Active"]
|
||||
if props.load_preview_dictionaries:
|
||||
statuses.append("Preview")
|
||||
if props.load_inactive_dictionaries:
|
||||
statuses.append("Inactive")
|
||||
return list(filter(lambda d: d["status"] in statuses, dicts))
|
||||
|
||||
@classmethod
|
||||
def get_property_dict(cls, class_data: Union[bsdd.ClassContractV1, dict]) -> Union[dict[str, dict[str, Any]], None]:
|
||||
@@ -133,31 +139,51 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
@classmethod
|
||||
def search_class(
|
||||
cls,
|
||||
client: bsdd.Client,
|
||||
keyword: str,
|
||||
dictionary_uris: Union[list[str], None],
|
||||
related_ifc_entities: Union[list[str], None],
|
||||
offset: int = 0,
|
||||
) -> list[bsdd.ClassSearchResponseClassContractV1]:
|
||||
response = client.search_class(
|
||||
keyword, dictionary_uris=dictionary_uris, related_ifc_entities=related_ifc_entities, offset=offset
|
||||
limit: int = 100,
|
||||
should_paginate: bool = True,
|
||||
):
|
||||
props = cls.get_bsdd_props()
|
||||
dictionary_uris = (
|
||||
[d.uri for d in props.dictionaries if d.is_active]
|
||||
if props.active_dictionary == "ALL"
|
||||
else [props.active_dictionary]
|
||||
)
|
||||
classes = response.get("classes", [])
|
||||
# If count is 100, it might be hitting the limit.
|
||||
if response["count"] == 100:
|
||||
classes += cls.search_class(client, keyword, dictionary_uris, related_ifc_entities, offset=offset + 100)
|
||||
return classes
|
||||
for dictionary_uri in dictionary_uris:
|
||||
response = cls.client.get_classes(
|
||||
dictionary_uri=dictionary_uri,
|
||||
use_nested_classes=False,
|
||||
search_text=keyword,
|
||||
related_ifc_entity=related_ifc_entities[0] if related_ifc_entities else None,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
)
|
||||
dictionary_name = response.get("name", "")
|
||||
dictionary_namespace_uri = response.get("uri", "")
|
||||
for _class in sorted(response.get("classes", []), key=lambda c: c["referenceCode"]):
|
||||
prop = props.classifications.add()
|
||||
prop.name = _class["name"]
|
||||
prop.reference_code = _class["referenceCode"]
|
||||
prop.uri = _class["uri"]
|
||||
prop.dictionary_name = dictionary_name
|
||||
prop.dictionary_namespace_uri = dictionary_namespace_uri
|
||||
|
||||
total_results = response.get("count", response.get("classesCount"))
|
||||
# For now, hard limit at 1000 results because any more and Blender
|
||||
# starts getting slow and they really should filter better
|
||||
if offset < 1000 and should_paginate and total_results == limit:
|
||||
cls.search_class(keyword, related_ifc_entities, offset=offset + limit, should_paginate=False)
|
||||
|
||||
return offset + total_results
|
||||
|
||||
@classmethod
|
||||
def set_active_bsdd(cls, name: str, uri: str) -> None:
|
||||
props = bpy.context.scene.BIMBSDDProperties
|
||||
props.active_domain = name
|
||||
props.active_dictionary = name
|
||||
props.active_uri = uri
|
||||
|
||||
@classmethod
|
||||
def should_filter_ifc_class(cls) -> bool:
|
||||
return bpy.context.scene.BIMBSDDProperties.should_filter_ifc_class
|
||||
|
||||
@classmethod
|
||||
def should_load_preview_domains(cls) -> bool:
|
||||
return bpy.context.scene.BIMBSDDProperties.load_preview_domains
|
||||
|
||||
@@ -4,6 +4,7 @@ markers =
|
||||
attribute
|
||||
boolean
|
||||
brick
|
||||
bsdd
|
||||
classification
|
||||
context
|
||||
cost
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
@bsdd
|
||||
Feature: bSDD
|
||||
|
||||
Scenario: Load bSDD dictionaries
|
||||
Given an empty Blender session
|
||||
And I look at the "buildingSMART Data Dictionary" panel
|
||||
When I click "Load bSDD Dictionaries"
|
||||
Then I see "Selected dictionary"
|
||||
And I see "LCA" in the "1st" list
|
||||
|
||||
Scenario: Add dictionaries as classification systems
|
||||
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 look at the "Classifications" 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 "Add Classification From bSDD"
|
||||
Then I see "LCA"
|
||||
And I see "BonsaiTestDict"
|
||||
|
||||
Scenario: Add a single dictionary as a classification systems
|
||||
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 "BonsaiTestDict" in the "1st" list
|
||||
And I look at the "Classifications" panel
|
||||
And I set the "classification_source" property to "buildingSMART Data Dictionary"
|
||||
And I set the "active_dictionary" property to "BonsaiTestDict"
|
||||
And I click "Add Classification From bSDD"
|
||||
Then I see "BonsaiTestDict"
|
||||
And I don't see "LCA"
|
||||
@@ -50,6 +50,67 @@ 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()
|
||||
|
||||
|
||||
class PanelSpy:
|
||||
def __init__(self, panel: type[bpy.types.Panel]):
|
||||
self.is_spy_dirty = True
|
||||
@@ -86,8 +147,9 @@ class PanelSpy:
|
||||
"active_dataptr": active_dataptr,
|
||||
"active_propname": active_propname,
|
||||
}
|
||||
self.spied_lists.append(spied_data)
|
||||
return TemplateListSpy(spied_data)
|
||||
template_list = TemplateListSpy(getattr(bpy.types, listtype_name), spied_data)
|
||||
self.spied_lists.append(template_list)
|
||||
return template_list
|
||||
elif self.spied_attr == "context_pointer_set":
|
||||
return lambda *args, **kwargs: None
|
||||
elif self.spied_attr == "label":
|
||||
@@ -157,9 +219,39 @@ class OperatorSpy:
|
||||
self.spied_data["kwargs"][name] = value
|
||||
|
||||
|
||||
class TemplateListSpy:
|
||||
def __init__(self, spied_data):
|
||||
class TemplateListSpy(PanelSpy):
|
||||
def __init__(self, template_list: type[bpy.types.UIList], spied_data: dict):
|
||||
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"])
|
||||
try:
|
||||
self.active_item = self.items[self.active_index]
|
||||
except:
|
||||
self.active_item = None
|
||||
self.panel = template_list
|
||||
|
||||
self.rows = []
|
||||
for item in self.items:
|
||||
self.rows.append(TemplateListItemSpy(self, item))
|
||||
|
||||
|
||||
class TemplateListItemSpy(PanelSpy):
|
||||
def __init__(self, parent: TemplateListSpy, item):
|
||||
self.panel = parent.panel
|
||||
self.spied_attr: Union[str, None] = None
|
||||
self.spied_labels: list[str] = []
|
||||
self.spied_props: list[dict[str, Any]] = []
|
||||
self.spied_operators: list[dict[str, Any]] = []
|
||||
parent.panel.draw_item(
|
||||
self,
|
||||
bpy.context,
|
||||
self,
|
||||
parent.spied_data["dataptr"],
|
||||
item,
|
||||
"",
|
||||
parent.spied_data["active_dataptr"],
|
||||
parent.spied_data["active_propname"],
|
||||
)
|
||||
|
||||
|
||||
ui_name_cache = {}
|
||||
@@ -181,6 +273,8 @@ def create_ui_name_cache():
|
||||
if panel_type.bl_label == "Add" and bl_idname != "VIEW3D_MT_add":
|
||||
continue # Non-unique, but "VIEW3D_MT_add" is the one we care about
|
||||
ui_name_cache[panel_type.bl_label] = bl_idname
|
||||
elif panel_type.bl_rna.base.name == "UIList":
|
||||
ui_name_cache[panel_type.bl_rna.name] = bl_idname
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -293,6 +387,57 @@ def i_see_text(text):
|
||||
assert [l for l in panel_spy.spied_labels if text in l], f"Text {text} not found in {panel_spy.spied_labels}"
|
||||
|
||||
|
||||
@given(parsers.parse('I see "{text}" in the "{nth}" list'))
|
||||
@when(parsers.parse('I see "{text}" in the "{nth}" list'))
|
||||
@then(parsers.parse('I see "{text}" in the "{nth}" list'))
|
||||
def 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 row in template_list.rows:
|
||||
for l in row.spied_labels:
|
||||
debug.append(l)
|
||||
if text in l:
|
||||
return True
|
||||
debug = "\n".join(debug)
|
||||
assert False, f"Could not see '{text}' in any list. 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'))
|
||||
def i_click_button_in_the_row_where_i_see_text_in_the_nth_list(button, text, nth):
|
||||
"""
|
||||
:param button: The text or icon of the button to click.
|
||||
"""
|
||||
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:
|
||||
is_row = False
|
||||
for l in row.spied_labels:
|
||||
debug.append(l)
|
||||
if text in l:
|
||||
is_row = True
|
||||
if is_row:
|
||||
i_click_button_on_panel(button, row)
|
||||
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}"'))
|
||||
@@ -564,15 +709,7 @@ def i_press_operator(operator):
|
||||
assert False, f"Failed to run operator bpy.ops.{operator} because of {e}"
|
||||
|
||||
|
||||
@given(parsers.parse('I click "{button}"'))
|
||||
@when(parsers.parse('I click "{button}"'))
|
||||
@then(parsers.parse('I click "{button}"'))
|
||||
def i_click_button(button):
|
||||
"""
|
||||
:param button: The text or icon of the button to click.
|
||||
"""
|
||||
assert panel_spy
|
||||
panel_spy.refresh_spy()
|
||||
def i_click_button_on_panel(button, panel_spy):
|
||||
for spied_operator in panel_spy.spied_operators:
|
||||
if spied_operator["text"] == button or spied_operator["icon"] == button:
|
||||
spied_operator["operator"]("INVOKE_DEFAULT", **spied_operator["kwargs"])
|
||||
@@ -592,6 +729,18 @@ def i_click_button(button):
|
||||
assert False, f"Could not find {button}:\n{debug}"
|
||||
|
||||
|
||||
@given(parsers.parse('I click "{button}"'))
|
||||
@when(parsers.parse('I click "{button}"'))
|
||||
@then(parsers.parse('I click "{button}"'))
|
||||
def i_click_button(button):
|
||||
"""
|
||||
:param button: The text or icon of the button to click.
|
||||
"""
|
||||
assert panel_spy
|
||||
panel_spy.refresh_spy()
|
||||
i_click_button_on_panel(button, panel_spy)
|
||||
|
||||
|
||||
@given(parsers.parse('I click the "{button}" after the text "{text}"'))
|
||||
@when(parsers.parse('I click the "{button}" after the text "{text}"'))
|
||||
@then(parsers.parse('I click the "{button}" after the text "{text}"'))
|
||||
|
||||
+20
-9
@@ -610,15 +610,15 @@ class Client:
|
||||
print(f"function 'Client.Unit' is deprecated, use 'Client.get_units' instead")
|
||||
return self.get(f"api/Unit/{version}")
|
||||
|
||||
def get_dictionary(self, dictionary_uri: str = "", version: int = 1) -> DictionaryResponseContractV1:
|
||||
def get_dictionary(
|
||||
self, dictionary_uri: str = "", include_test_dictionaries: bool = "False", version: int = 1
|
||||
) -> DictionaryResponseContractV1:
|
||||
"""
|
||||
Get list of available Dictionaries
|
||||
This API replaces Domain
|
||||
"""
|
||||
endpoint = f"Dictionary/v{version}"
|
||||
params = {
|
||||
"Uri": dictionary_uri,
|
||||
}
|
||||
params = {"Uri": dictionary_uri, "IncludeTestDictionaries": "true" if include_test_dictionaries else "false"}
|
||||
return self.get(endpoint, params)
|
||||
|
||||
def get_classes(
|
||||
@@ -626,6 +626,8 @@ class Client:
|
||||
dictionary_uri: str,
|
||||
use_nested_classes: bool = True,
|
||||
class_type: ClassTypes = "Class",
|
||||
search_text: str = "",
|
||||
related_ifc_entity: str = "",
|
||||
language_code: str = "",
|
||||
version: int = 1,
|
||||
offset=0,
|
||||
@@ -636,14 +638,23 @@ class Client:
|
||||
This API replaces Domain
|
||||
"""
|
||||
endpoint = f"Dictionary/v{version}/Classes"
|
||||
params = {
|
||||
"Uri": dictionary_uri,
|
||||
params = {"Uri": dictionary_uri}
|
||||
for param, value in {
|
||||
"UseNestedClasses": use_nested_classes,
|
||||
"ClassType": class_type,
|
||||
"languageCode": language_code,
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
}
|
||||
}.items():
|
||||
if value:
|
||||
params[param] = value
|
||||
if not use_nested_classes:
|
||||
for param, value in {
|
||||
"SearchText": search_text,
|
||||
"RelatedIfcEntity": related_ifc_entity,
|
||||
}.items():
|
||||
if value:
|
||||
params[param] = value
|
||||
return self.get(endpoint, params)
|
||||
|
||||
def get_properties(
|
||||
@@ -781,8 +792,8 @@ class Client:
|
||||
this API replaces ClassificationSearch
|
||||
"""
|
||||
|
||||
if len(search_text) < 3:
|
||||
raise ValueError("Search text must be at least 3 characters long.")
|
||||
if len(search_text) < 1:
|
||||
raise ValueError("Search text must be at least 1 characters long.")
|
||||
|
||||
if related_ifc_entities is None:
|
||||
related_ifc_entities = []
|
||||
|
||||
Reference in New Issue
Block a user