Big bSDD overhaul for properties and classifications. See description.

* bSDD now supports a couple more args when querying the API.
 * Fetching properties via the classification UI has been removed
 * The bSDD dictionary selector has been merged into the classification
source dropdown
 * The bSDD dictionary selector merged into the property set name
dropdown
 * You can now search for properties in the bSDD directly when adding
properties
 * You can browse properties via groups or classifications (based on
assigned classifications)
 * You can browse properties via keyword (with some limitations due to
the API)
 * You can selectively choose which properties you then want to add.
Only basic support right now.
This commit is contained in:
Dion Moult
2025-05-28 20:51:02 +10:00
parent e9eb5f892a
commit a929ab0c4f
15 changed files with 448 additions and 85 deletions
@@ -20,15 +20,20 @@ import bpy
from . import ui, prop, operator from . import ui, prop, operator
classes = ( classes = (
operator.GetBSDDClassificationProperties, operator.AddBSDDProperties,
operator.ImportBSDDClasses,
operator.LoadBSDDDictionaries, operator.LoadBSDDDictionaries,
operator.SearchBSDDClass, operator.SearchBSDDClassifications,
operator.SearchBSDDProperties,
prop.BSDDDictionary, prop.BSDDDictionary,
prop.BSDDClassification, prop.BSDDClassification,
prop.BSDDProperty,
prop.BSDDPset, prop.BSDDPset,
prop.BIMBSDDProperties, prop.BIMBSDDProperties,
ui.BIM_UL_bsdd_dictionaries,
ui.BIM_UL_bsdd_classifications, ui.BIM_UL_bsdd_classifications,
ui.BIM_UL_bsdd_dictionaries,
ui.BIM_UL_bsdd_classes,
ui.BIM_UL_bsdd_properties,
ui.BIM_PT_bsdd, ui.BIM_PT_bsdd,
) )
+58 -12
View File
@@ -19,6 +19,7 @@
import bpy import bpy
import bsdd import bsdd
import bonsai.tool as tool import bonsai.tool as tool
import ifcopenshell.util.element
from bonsai.core import bsdd as core from bonsai.core import bsdd as core
@@ -32,28 +33,73 @@ class LoadBSDDDictionaries(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class SearchBSDDClass(bpy.types.Operator): class SearchBSDDClassifications(bpy.types.Operator):
bl_idname = "bim.search_bsdd_classifications" bl_idname = "bim.search_bsdd_classifications"
bl_label = "Search bSDD Class" bl_label = "Search bSDD Class"
bl_description = "Search for bSDD classes by the provided keyword" bl_description = "Search for bSDD classes by the provided keyword"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
keyword = context.scene.BIMBSDDProperties.keyword total = core.search_bsdd_class(tool.Bsdd, context.scene.BIMBSDDProperties.keyword)
classes_found = core.search_class(keyword, tool.Bsdd) self.report({"INFO"}, f"{total} bSDD classes found.")
self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.")
return {"FINISHED"} return {"FINISHED"}
class GetBSDDClassificationProperties(bpy.types.Operator): class ImportBSDDClasses(bpy.types.Operator):
bl_idname = "bim.get_bsdd_classification_properties" bl_idname = "bim.import_bsdd_classes"
bl_label = "Search bSDD Class Properties" bl_label = "Import bSDD Classes"
bl_description = "Search for bSDD class properties for the currently selected class" bl_description = "Load bSDD classes that apply to the current element"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context): def execute(self, context):
pset_data = core.get_class_properties(tool.Bsdd) props = tool.Bsdd.get_bsdd_props()
psets_found = len(pset_data) core.import_bsdd_classes(tool.Bsdd, self.obj, self.obj_type)
props_found = sum(len(pset) for pset in pset_data.values()) self.report({"INFO"}, f"{len(props.classes)} bSDD classes found.")
self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.")
return {"FINISHED"} return {"FINISHED"}
class SearchBSDDProperties(bpy.types.Operator):
bl_idname = "bim.search_bsdd_properties"
bl_label = "Search bSDD Properties"
bl_description = "Search for bSDD properties that apply to the current element"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
props = tool.Bsdd.get_bsdd_props()
core.search_bsdd_properties(tool.Bsdd, context.scene.BIMBSDDProperties.keyword, self.obj, self.obj_type)
self.report({"INFO"}, f"{len(props.properties)} bSDD properties found.")
return {"FINISHED"}
class AddBSDDProperties(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_bsdd_properties"
bl_label = "Add bSDD Properties"
bl_description = "Add selected bSDD properties"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = tool.Ifc.get()
bprops = tool.Bsdd.get_bsdd_props()
psets = {}
for selected_property in bprops.selected_properties:
psets.setdefault(selected_property.metadata, {})[selected_property.name] = selected_property.get_value()
props = tool.Pset.get_pset_props(self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
properties = {}
current_psets = ifcopenshell.util.element.get_psets(element, verbose=True)
for pset_name, properties in psets.items():
if pset := current_psets.get(pset_name, None):
pset = self.file.by_id(pset["id"])
else:
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name=pset_name)
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties=properties)
+39 -1
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
import bonsai.tool as tool
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from bonsai.bim.module.bsdd.data import BSDDData from bonsai.bim.module.bsdd.data import BSDDData
from bonsai.bim.prop import Attribute, StrProperty from bonsai.bim.prop import Attribute, StrProperty
@@ -30,6 +31,7 @@ from bpy.props import (
FloatVectorProperty, FloatVectorProperty,
CollectionProperty, CollectionProperty,
) )
from typing import Union
def get_active_dictionary(self, context): def get_active_dictionary(self, context):
@@ -42,6 +44,15 @@ def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary() BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
def update_is_selected(self: "BSDDProperty", context: bpy.types.Context) -> None:
tool.Bsdd.import_selected_properties()
def update_active_class_index(self: "BIMBSDDProperties", context: bpy.types.Context) -> None:
tool.Bsdd.import_class_properties()
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
class BSDDDictionary(PropertyGroup): class BSDDDictionary(PropertyGroup):
name: StringProperty(name="Name") name: StringProperty(name="Name")
uri: StringProperty(name="URI") uri: StringProperty(name="URI")
@@ -57,11 +68,21 @@ class BSDDDictionary(PropertyGroup):
class BSDDClassification(PropertyGroup): class BSDDClassification(PropertyGroup):
name: StringProperty(name="Name") name: StringProperty(name="Name")
reference_code: StringProperty(name="Reference Code") reference_code: StringProperty(name="Reference Code")
uri: StringProperty(name="Namespace URI") uri: StringProperty(name="URI")
dictionary_name: StringProperty(name="Dictionary Name") dictionary_name: StringProperty(name="Dictionary Name")
dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI") dictionary_namespace_uri: StringProperty(name="Dictionary Namespace URI")
class BSDDProperty(PropertyGroup):
name: StringProperty(name="Name")
code: StringProperty(name="Code")
uri: StringProperty(name="URI")
pset: StringProperty(name="Pset")
is_selected: BoolProperty(
name="Is Selected", description="Select to add or edit this property", default=False, update=update_is_selected
)
class BSDDPset(PropertyGroup): class BSDDPset(PropertyGroup):
name: StringProperty(name="Name") name: StringProperty(name="Name")
properties: CollectionProperty(name="Properties", type=Attribute) properties: CollectionProperty(name="Properties", type=Attribute)
@@ -75,6 +96,19 @@ class BIMBSDDProperties(PropertyGroup):
active_dictionary_index: IntProperty(name="Active Dictionary Index") active_dictionary_index: IntProperty(name="Active Dictionary Index")
classifications: CollectionProperty(name="Classifications", type=BSDDClassification) classifications: CollectionProperty(name="Classifications", type=BSDDClassification)
active_classification_index: IntProperty(name="Active Classification Index") active_classification_index: IntProperty(name="Active Classification Index")
property_filter_mode: EnumProperty(
name="Property Filter Mode",
items=[
("CLASS", "By Class", "Browse properties by class or group"),
("KEYWORD", "By Keyword", "Search properties directly using a keyword"),
],
default="CLASS",
)
classes: CollectionProperty(name="Classes", type=BSDDClassification)
active_class_index: IntProperty(name="Active Class Index", update=update_active_class_index)
properties: CollectionProperty(name="Properties", type=BSDDProperty)
active_property_index: IntProperty(name="Active Property Index")
selected_properties: CollectionProperty(name="Selected Properties", type=Attribute)
keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive") keyword: StringProperty(name="Keyword", description="Query for bsdd classes search, case and accent insensitive")
should_filter_ifc_class: BoolProperty( should_filter_ifc_class: BoolProperty(
name="Filter Active IFC Class", name="Filter Active IFC Class",
@@ -96,3 +130,7 @@ class BIMBSDDProperties(PropertyGroup):
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
) )
classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset) classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset)
@property
def active_class(self) -> Union[BSDDClassification, None]:
return tool.Blender.get_active_uilist_element(self.classes, self.active_class_index)
+20 -1
View File
@@ -54,7 +54,7 @@ class BIM_PT_bsdd(Panel):
selected_dictionary = None selected_dictionary = None
if selected_dictionary: if selected_dictionary:
layout.label(text="Selected dictionary:") layout.label(text="Selected Dictionary:")
box = layout.box() box = layout.box()
row = box.row(align=True) row = box.row(align=True)
row.label(text="Language") row.label(text="Language")
@@ -92,3 +92,22 @@ class BIM_UL_bsdd_classifications(UIList):
row.label(text=item.reference_code) row.label(text=item.reference_code)
row.label(text=item.name) row.label(text=item.name)
row.operator("bim.open_uri", text="", icon="URL").uri = item.uri row.operator("bim.open_uri", text="", icon="URL").uri = item.uri
class BIM_UL_bsdd_classes(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=item.name)
row.operator("bim.open_uri", text="", icon="URL").uri = item.uri
class BIM_UL_bsdd_properties(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=item.name)
if item.pset:
row.label(text=item.pset)
row.operator("bim.open_uri", text="", icon="URL").uri = item.uri
row.prop(item, "is_selected", icon="CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT", text="", emboss=False)
@@ -41,6 +41,7 @@ class ClassificationsData:
cls.data["has_classification_file"] = cls.has_classification_file() cls.data["has_classification_file"] = cls.has_classification_file()
cls.data["classifications"] = cls.classifications() cls.data["classifications"] = cls.classifications()
cls.data["available_classifications"] = cls.available_classifications() cls.data["available_classifications"] = cls.available_classifications()
cls.data["classification_source"] = cls.classification_source()
@classmethod @classmethod
def has_classification_file(cls): def has_classification_file(cls):
@@ -62,6 +63,19 @@ class ClassificationsData:
return [] return []
return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")] return [(str(e.id()), e.Name, "") for e in IfcStore.classification_file.by_type("IfcClassification")]
@classmethod
def classification_source(cls):
items = [
("FILE", "IFC File", ""),
("MANUAL", "Manual Entry", ""),
]
bprops = tool.Bsdd.get_bsdd_props()
dictionaries = [(d.uri, f"bSDD: {d.name}", "") for d in bprops.dictionaries if d.is_active]
if dictionaries:
items.append(("BSDD", "All Active bSDDs", ""))
items.extend(dictionaries)
return items
class ReferencesData: class ReferencesData:
@classmethod @classmethod
@@ -115,11 +115,12 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def _execute(self, context):
props = context.scene.BIMBSDDProperties cprops = context.scene.BIMClassificationProperties
if props.active_dictionary == "ALL": bprops = tool.Bsdd.get_bsdd_props()
dictionaries = [d.uri for d in props.dictionaries if d.is_active] if cprops.classification_source == "BSDD":
dictionaries = [d.uri for d in bprops.dictionaries if d.is_active]
else: else:
dictionaries = [props.active_dictionary] dictionaries = [cprops.classification_source]
for uri in dictionaries: for uri in dictionaries:
if not (dictionary := tool.Bsdd.get_dictionary(uri)): if not (dictionary := tool.Bsdd.get_dictionary(uri)):
continue continue
@@ -44,6 +44,12 @@ def get_classifications(self, context):
return ClassificationReferencesData.data["classifications"] return ClassificationReferencesData.data["classifications"]
def get_classification_source(self, context):
if not ClassificationsData.is_loaded:
ClassificationsData.load()
return ClassificationsData.data["classification_source"]
class ClassificationReference(PropertyGroup): class ClassificationReference(PropertyGroup):
name: StringProperty(name="Name") name: StringProperty(name="Name")
identification: StringProperty(name="Identification") identification: StringProperty(name="Identification")
@@ -54,15 +60,7 @@ class ClassificationReference(PropertyGroup):
class BIMClassificationProperties(PropertyGroup): class BIMClassificationProperties(PropertyGroup):
is_adding: BoolProperty(name="Is Adding", default=False) is_adding: BoolProperty(name="Is Adding", default=False)
classification_source: EnumProperty( classification_source: EnumProperty(items=get_classification_source, name="Classification Source")
items=[
("FILE", "IFC File", ""),
("BSDD", "buildingSMART Data Dictionary", ""),
("MANUAL", "Manual Entry", ""),
],
name="Classification Source",
default="FILE",
)
available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications") available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications")
classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute) classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute)
active_classification_id: IntProperty(name="Active Classification Id") active_classification_id: IntProperty(name="Active Classification Id")
@@ -54,10 +54,10 @@ class BIM_PT_classifications(Panel):
if self.props.classification_source == "FILE": if self.props.classification_source == "FILE":
self.draw_add_file_ui(context) self.draw_add_file_ui(context)
elif self.props.classification_source == "BSDD":
self.draw_add_bsdd_ui(context)
elif self.props.classification_source == "MANUAL": elif self.props.classification_source == "MANUAL":
self.draw_add_manual_ui(context) self.draw_add_manual_ui(context)
else:
self.draw_add_bsdd_ui(context)
for classification in ClassificationsData.data["classifications"]: for classification in ClassificationsData.data["classifications"]:
if self.props.active_classification_id == classification["id"]: if self.props.active_classification_id == classification["id"]:
@@ -76,11 +76,6 @@ class BIM_PT_classifications(Panel):
row.operator("bim.enable_adding_manual_classification", text="Add Classification", icon="ADD") row.operator("bim.enable_adding_manual_classification", text="Add Classification", icon="ADD")
def draw_add_bsdd_ui(self, context): def draw_add_bsdd_ui(self, context):
self.bprops = context.scene.BIMBSDDProperties
row = self.layout.row()
row.prop(self.bprops, "active_dictionary", text="")
row = self.layout.row() row = self.layout.row()
row.operator("bim.add_classification_from_bsdd", icon="ADD") row.operator("bim.add_classification_from_bsdd", icon="ADD")
@@ -139,10 +134,10 @@ class ReferenceUI:
if self.sprops.classification_source == "FILE": if self.sprops.classification_source == "FILE":
self.draw_add_file_ui(context) self.draw_add_file_ui(context)
elif self.sprops.classification_source == "BSDD":
self.draw_add_bsdd_ui(context)
elif self.sprops.classification_source == "MANUAL": elif self.sprops.classification_source == "MANUAL":
self.draw_add_manual_ui(context) self.draw_add_manual_ui(context)
else:
self.draw_add_bsdd_ui(context)
def draw_add_manual_ui(self, context): def draw_add_manual_ui(self, context):
row = self.layout.row() row = self.layout.row()
@@ -158,17 +153,11 @@ class ReferenceUI:
row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD") row.operator("bim.enable_adding_manual_classification_reference", text="Add Reference", icon="ADD")
def draw_add_bsdd_ui(self, context): def draw_add_bsdd_ui(self, context):
row = self.layout.row()
row.prop(self.bprops, "active_dictionary", text="")
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.prop(self.bprops, "keyword", text="") row.prop(self.bprops, "keyword", text="")
row.prop(self.bprops, "should_filter_ifc_class", text="", icon="FILTER")
row.operator("bim.search_bsdd_classifications", text="", icon="VIEWZOOM") row.operator("bim.search_bsdd_classifications", text="", icon="VIEWZOOM")
row = self.layout.row()
row.prop(self.bprops, "should_filter_ifc_class")
row.prop(self.bprops, "use_only_ifc_properties")
if len(self.bprops.classifications): if len(self.bprops.classifications):
self.layout.template_list( self.layout.template_list(
"BIM_UL_bsdd_classifications", "BIM_UL_bsdd_classifications",
@@ -189,21 +178,6 @@ class ReferenceUI:
) )
op.obj = self.obj op.obj = self.obj
op.obj_type = self.obj_type op.obj_type = self.obj_type
row.operator("bim.get_bsdd_classification_properties", text="", icon="COPY_ID")
if len(self.bprops.classification_psets):
use_only_ifc_properties = self.bprops.use_only_ifc_properties
for pset in self.bprops.classification_psets:
properties = pset.properties
if use_only_ifc_properties:
properties = [p for p in properties if p.metadata == "IFC"]
if not properties:
continue
box = self.layout.box()
row = box.row()
row.label(text=pset.name, icon="COPY_ID")
bonsai.bim.helper.draw_attributes(properties, box)
def draw_add_file_ui(self, context): def draw_add_file_ui(self, context):
if not self.data.data["active_classification_library"]: if not self.data.data["active_classification_library"]:
+18 -1
View File
@@ -81,7 +81,24 @@ def get_pset_name(self, context):
results = get_profile_pset_names(self, context) results = get_profile_pset_names(self, context)
elif prop_type == "WorkSchedulePsetProperties": elif prop_type == "WorkSchedulePsetProperties":
results = get_work_schedule_pset_names(self, context) results = get_work_schedule_pset_names(self, context)
return [("BBIM_CUSTOM", "Custom Pset", "Create a property set without using a template."), None] + results items = [("BBIM_CUSTOM", "Custom Pset", "Create a property set without using a template.")]
bprops = tool.Bsdd.get_bsdd_props()
dictionaries = [(d.uri, f"bSDD: {d.name}", "") for d in bprops.dictionaries if d.is_active]
if dictionaries:
items.extend(
[
None,
(
"BBIM_BSDD",
"All Data Dictionaries",
"Manage properties from all active buildingSMART Data Dictionaries",
),
]
)
items.extend(dictionaries)
items.append(None)
items.extend(results)
return items
def get_object_pset_name(self, context): def get_object_pset_name(self, context):
+67 -4
View File
@@ -20,7 +20,7 @@ from __future__ import annotations
import bpy import bpy
import bonsai.tool as tool import bonsai.tool as tool
from bpy.types import Panel from bpy.types import Panel
from bonsai.bim.helper import prop_with_search, get_display_value from bonsai.bim.helper import prop_with_search, get_display_value, draw_attribute
from bonsai.bim.module.pset.data import ( from bonsai.bim.module.pset.data import (
ObjectPsetsData, ObjectPsetsData,
ObjectQtosData, ObjectQtosData,
@@ -256,11 +256,74 @@ class BIM_PT_object_psets(Panel):
ObjectPsetsData.load() ObjectPsetsData.load()
props = context.active_object.PsetProperties props = context.active_object.PsetProperties
self.bprops = context.scene.BIMBSDDProperties
row = self.layout.row(align=True) row = self.layout.row(align=True)
prop_with_search(row, props, "pset_name", text="") prop_with_search(row, props, "pset_name", text="")
op = row.operator("bim.add_pset", icon="ADD", text="") if props.pset_name != "BBIM_BSDD" and not props.pset_name.startswith(tool.Bsdd.identifier_url):
op.obj = context.active_object.name op = row.operator("bim.add_pset", icon="ADD", text="")
op.obj_type = "Object" op.obj = context.active_object.name
op.obj_type = "Object"
else:
row = self.layout.row(align=True)
row.prop(self.bprops, "property_filter_mode", text="")
if self.bprops.property_filter_mode == "CLASS":
row.prop(self.bprops, "should_filter_ifc_class", text="", icon="FILTER")
op = row.operator("bim.import_bsdd_classes", text="", icon="FILE_REFRESH")
op.obj = context.active_object.name
op.obj_type = "Object"
if len(self.bprops.classes):
self.layout.template_list(
"BIM_UL_bsdd_classes",
"",
self.bprops,
"classes",
self.bprops,
"active_class_index",
)
if len(self.bprops.properties):
self.layout.template_list(
"BIM_UL_bsdd_properties",
"",
self.bprops,
"properties",
self.bprops,
"active_property_index",
)
else:
row = self.layout.row()
row.label(text="No Results")
else:
row = self.layout.row()
row.label(text="No Results")
elif self.bprops.property_filter_mode == "KEYWORD":
row.prop(self.bprops, "keyword", text="")
op = row.operator("bim.search_bsdd_properties", text="", icon="VIEWZOOM")
op.obj = context.active_object.name
op.obj_type = "Object"
if len(self.bprops.properties):
self.layout.template_list(
"BIM_UL_bsdd_properties",
"",
self.bprops,
"properties",
self.bprops,
"active_property_index",
)
else:
row = self.layout.row()
row.label(text="No Results")
for selected_property in self.bprops.selected_properties:
row = self.layout.row(align=True)
# row.prop(selected_property, "metadata", text="")
draw_attribute(selected_property, row)
row = self.layout.row()
op = row.operator("bim.add_bsdd_properties", icon="ADD")
op.obj = context.active_object.name
op.obj_type = "Object"
global_props = tool.Pset.get_global_pset_props() global_props = tool.Pset.get_global_pset_props()
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET": if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
+7 -9
View File
@@ -26,14 +26,12 @@ if TYPE_CHECKING:
import bonsai.tool as tool import bonsai.tool as tool
def get_class_properties(bsdd: tool.Bsdd) -> dict[str, dict[str, Any]]: def import_bsdd_classes(bsdd: tool.Bsdd, obj, obj_type) -> int:
bsdd.clear_class_psets() return bsdd.import_classes(obj, obj_type)
data = bsdd.get_active_class_data()
pset_dict = bsdd.get_property_dict(data)
if pset_dict is None: def search_bsdd_properties(bsdd: tool.Bsdd, keyword: str, obj, obj_type) -> int:
return {} return bsdd.import_properties(obj, obj_type, keyword)
bsdd.create_class_psets(pset_dict)
return pset_dict
def load_bsdd(bsdd: tool.Bsdd) -> None: def load_bsdd(bsdd: tool.Bsdd) -> None:
@@ -41,7 +39,7 @@ def load_bsdd(bsdd: tool.Bsdd) -> None:
bsdd.create_dictionaries(bsdd.get_dictionaries()) bsdd.create_dictionaries(bsdd.get_dictionaries())
def search_class(keyword: str, bsdd: tool.Bsdd) -> int: def search_bsdd_class(bsdd: tool.Bsdd, keyword: str) -> int:
bsdd.clear_classes() bsdd.clear_classes()
related_entities = bsdd.get_related_ifc_entities() related_entities = bsdd.get_related_ifc_entities()
return bsdd.search_class(keyword, related_entities) return bsdd.search_class(keyword, related_entities)
+174 -7
View File
@@ -5,11 +5,15 @@ import json
import bsdd import bsdd
import ifcopenshell.util.type import ifcopenshell.util.type
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.classification
from typing import Any, Union, Optional, TYPE_CHECKING from typing import Any, Union, Optional, TYPE_CHECKING
class Bsdd(bonsai.core.tool.Bsdd): class Bsdd(bonsai.core.tool.Bsdd):
identifier_url = "https://identifier.buildingsmart.org"
client = bsdd.Client() client = bsdd.Client()
bsdd_classes: dict[str, dict] = {}
bsdd_properties: dict[str, dict] = {}
@classmethod @classmethod
def get_bsdd_props(cls): def get_bsdd_props(cls):
@@ -23,6 +27,10 @@ class Bsdd(bonsai.core.tool.Bsdd):
def clear_classes(cls) -> None: def clear_classes(cls) -> None:
bpy.context.scene.BIMBSDDProperties.classifications.clear() bpy.context.scene.BIMBSDDProperties.classifications.clear()
@classmethod
def clear_properties(cls) -> None:
bpy.context.scene.BIMBSDDProperties.properties.clear()
@classmethod @classmethod
def clear_dictionaries(cls) -> None: def clear_dictionaries(cls) -> None:
bpy.context.scene.BIMBSDDProperties.dictionaries.clear() bpy.context.scene.BIMBSDDProperties.dictionaries.clear()
@@ -94,10 +102,12 @@ class Bsdd(bonsai.core.tool.Bsdd):
return list(filter(lambda d: d["status"] in statuses, dicts)) return list(filter(lambda d: d["status"] in statuses, dicts))
@classmethod @classmethod
def get_property_dict(cls, class_data: Union[bsdd.ClassContractV1, dict]) -> Union[dict[str, dict[str, Any]], None]: def get_class_properties(
cls, class_data: Union[bsdd.ClassContractV1, dict]
) -> Union[dict[str, dict[str, Any]], None]:
properties = class_data.get("classProperties", None) properties = class_data.get("classProperties", None)
if not properties: if not properties:
return None return {}
ifc_class = class_data.get("relatedIfcEntityNames") or "" ifc_class = class_data.get("relatedIfcEntityNames") or ""
if ifc_class: if ifc_class:
@@ -155,11 +165,12 @@ class Bsdd(bonsai.core.tool.Bsdd):
limit: int = 100, limit: int = 100,
should_paginate: bool = True, should_paginate: bool = True,
): ):
props = cls.get_bsdd_props() cprops = bpy.context.scene.BIMClassificationProperties
bprops = cls.get_bsdd_props()
dictionary_uris = ( dictionary_uris = (
[d.uri for d in props.dictionaries if d.is_active] [d.uri for d in bprops.dictionaries if d.is_active]
if props.active_dictionary == "ALL" if cprops.classification_source == "BSDD"
else [props.active_dictionary] else [cprops.classification_source]
) )
for dictionary_uri in dictionary_uris: for dictionary_uri in dictionary_uris:
for related_ifc_entity in related_ifc_entities or [None]: for related_ifc_entity in related_ifc_entities or [None]:
@@ -174,7 +185,7 @@ class Bsdd(bonsai.core.tool.Bsdd):
dictionary_name = response.get("name", "") dictionary_name = response.get("name", "")
dictionary_namespace_uri = response.get("uri", "") dictionary_namespace_uri = response.get("uri", "")
for _class in sorted(response.get("classes", []), key=lambda c: c["referenceCode"]): for _class in sorted(response.get("classes", []), key=lambda c: c["referenceCode"]):
prop = props.classifications.add() prop = bprops.classifications.add()
prop.name = _class["name"] prop.name = _class["name"]
prop.reference_code = _class["referenceCode"] prop.reference_code = _class["referenceCode"]
prop.uri = _class["uri"] prop.uri = _class["uri"]
@@ -198,3 +209,159 @@ class Bsdd(bonsai.core.tool.Bsdd):
@classmethod @classmethod
def should_filter_ifc_class(cls) -> bool: def should_filter_ifc_class(cls) -> bool:
return bpy.context.scene.BIMBSDDProperties.should_filter_ifc_class return bpy.context.scene.BIMBSDDProperties.should_filter_ifc_class
@classmethod
def get_bsdd_class(cls, uri: str) -> dict:
if not (bsdd_class := cls.bsdd_classes.get(uri, {})):
bsdd_class = cls.client.get_class(uri)
cls.bsdd_classes[uri] = bsdd_class
return bsdd_class
@classmethod
def get_bsdd_property(cls, uri: str) -> dict:
if not (bsdd_property := cls.bsdd_properties.get(uri, {})):
bsdd_property = cls.client.get_property(uri, include_classes=True)
cls.bsdd_properties[uri] = bsdd_property
return bsdd_property
@classmethod
def import_classes(cls, obj, obj_type) -> None:
pprops = tool.Pset.get_pset_props(obj, obj_type)
props = cls.get_bsdd_props()
props.classes.clear()
classes = set()
for obj in tool.Blender.get_selected_objects(include_active=True):
if element := tool.Ifc.get_entity(obj):
for reference in ifcopenshell.util.classification.get_references(element):
if (uri := reference.Location) and uri.startswith(cls.identifier_url):
classes.add((reference[1] or reference[2] or "Unnamed", uri))
dictionary_uris = (
[d.uri for d in props.dictionaries if d.is_active]
if pprops.pset_name == "BBIM_BSDD"
else [pprops.pset_name]
)
related_ifc_entities = cls.get_related_ifc_entities()
for dictionary_uri in dictionary_uris:
for related_ifc_entity in related_ifc_entities or [None]:
bsdd_classes = cls.client.get_classes(
dictionary_uri=dictionary_uri,
class_type="GroupOfProperties",
use_nested_classes=False,
related_ifc_entity=related_ifc_entity,
)
for bsdd_class in bsdd_classes["classes"]:
classes.add((bsdd_class["name"], bsdd_class["uri"]))
for bsdd_class in classes:
new = props.classes.add()
new.name = bsdd_class[0]
new.uri = bsdd_class[1]
@classmethod
def import_class_properties(cls) -> None:
props = cls.get_bsdd_props()
props.properties.clear()
if not (active_class := props.active_class):
return
if not (bsdd_class := cls.get_bsdd_class(active_class.uri)):
return
for bsdd_prop in bsdd_class.get("classProperties", []):
if not bsdd_prop.get("propertySet", None):
continue
cls.bsdd_properties[bsdd_prop["uri"]] = bsdd_prop
new = props.properties.add()
new.name = bsdd_prop["name"]
new.pset = bsdd_prop["propertySet"]
new.uri = bsdd_prop["uri"]
@classmethod
def import_properties(cls, obj, obj_type, keyword) -> None:
props = cls.get_bsdd_props()
props.properties.clear()
pprops = tool.Pset.get_pset_props(obj, obj_type)
dictionary_uris = (
[d.uri for d in props.dictionaries if d.is_active]
if pprops.pset_name == "BBIM_BSDD"
else [pprops.pset_name]
)
for dictionary_uri in dictionary_uris:
for bsdd_prop in cls.client.get_properties(dictionary_uri, keyword)["properties"]:
new = props.properties.add()
new.name = bsdd_prop["name"]
new.uri = bsdd_prop["uri"]
@classmethod
def import_selected_properties(cls) -> None:
props = cls.get_bsdd_props()
data_type_map = {
"String": "string",
"Real": "float",
"Boolean": "boolean",
}
imported_props = set()
for bsdd_prop in props.properties:
if not bsdd_prop.is_selected:
continue
if not (pset_name := bsdd_prop.pset):
prop_data = cls.get_bsdd_property(bsdd_prop.uri)
pset_name = prop_data.get("propertyClasses", [{}])[0].get("propertySet", "")
imported_props.add((pset_name, bsdd_prop.name))
if (
selected_property := props.selected_properties.get(bsdd_prop.name)
) and selected_property.metadata == pset_name:
continue
data = cls.bsdd_properties[bsdd_prop.uri]
predefined_value = data.get("predefinedValue")
if predefined_value:
possible_values = [predefined_value]
else:
possible_values = data.get("allowedValues", []) or []
possible_values = [v["value"] for v in possible_values]
new = props.selected_properties.add()
new.name = bsdd_prop.name
if possible_values:
new.enum_items = json.dumps(possible_values)
new.data_type = "enum"
else:
new.data_type = data_type_map.get(data["dataType"], "string")
new.description = data.get("description", "")
new.metadata = pset_name
to_remove = []
for i, selected_property in enumerate(props.selected_properties):
if (selected_property.metadata, selected_property.name) not in imported_props:
to_remove.append(i)
for i in to_remove[::-1]:
props.selected_properties.remove(i)
@classmethod
def get_applicable_psets(cls, element: ifcopenshell.entity_instance):
uris = set()
for reference in ifcopenshell.util.classification.get_references(element):
if (uri := reference.Location) and uri.startswith(cls.identifier_url):
uris.add(uri)
psets = set()
for uri in uris:
if not (bsdd_class := cls.bsdd_classes.get(uri, None)):
continue
for class_pset in bsdd_class.get("classProperties", []):
if not (pset_name := class_pset.get("propertySet", None)):
continue
psets.add((uri, bsdd_class["name"], pset_name))
return psets
@classmethod
def is_applicable(cls, pset_uri: str, element: ifcopenshell.entity_instance) -> bool:
uris = set()
for reference in ifcopenshell.util.classification.get_references(element):
if (uri := reference.Location) and uri.startswith(cls.identifier_url):
uris.add(uri)
class_uri, pset_name = pset_uri.rsplit("#", 1)
return class_uri in uris
+1 -1
View File
@@ -73,7 +73,7 @@ class Pset(bonsai.core.tool.Pset):
def get_pset_name(cls, obj: str, obj_type: tool.Ifc.OBJECT_TYPE, pset_type: PSET_TYPE = "PSET") -> str: def get_pset_name(cls, obj: str, obj_type: tool.Ifc.OBJECT_TYPE, pset_type: PSET_TYPE = "PSET") -> str:
props = cls.get_pset_props(obj, obj_type) props = cls.get_pset_props(obj, obj_type)
name = props.pset_name if pset_type == "PSET" else props.qto_name name = props.pset_name if pset_type == "PSET" else props.qto_name
if name == "BBIM_CUSTOM": if name in ("BBIM_CUSTOM", "BBIM_BSDD"):
return "" return ""
return name return name
+14 -2
View File
@@ -658,13 +658,25 @@ class Client:
return self.get(endpoint, params) return self.get(endpoint, params)
def get_properties( def get_properties(
self, dictionary_uri: str, offset: int = 0, limit: int = 100, language_code: str = "", version: int = 1 self,
dictionary_uri: str,
search_text: str = "",
offset: int = 0,
limit: int = 100,
language_code: str = "",
version: int = 1,
) -> DictionaryPropertiesResponseContractV1: ) -> DictionaryPropertiesResponseContractV1:
""" """
Get Dictionary with its properties Get Dictionary with its properties
""" """
endpoint = f"Dictionary/v{version}/Properties" endpoint = f"Dictionary/v{version}/Properties"
params = {"Uri": dictionary_uri, "languageCode": language_code, "offset": offset, "limit": limit} params = {
"Uri": dictionary_uri,
"SearchText": search_text,
"languageCode": language_code,
"offset": offset,
"limit": limit,
}
return self.get(endpoint, params) return self.get(endpoint, params)
def get_class( def get_class(
@@ -21,6 +21,12 @@ from typing import Optional
def get_references(element: ifcopenshell.entity_instance, should_inherit=True) -> set[ifcopenshell.entity_instance]: def get_references(element: ifcopenshell.entity_instance, should_inherit=True) -> set[ifcopenshell.entity_instance]:
"""Gets classification references associated with the element
:param should_inherit: If true, classification references are inherited
from the type. Classifications can be overriden per system.
:return: A set of IfcClassificationReference
"""
results = set() results = set()
if not element.is_a("IfcRoot"): if not element.is_a("IfcRoot"):
if (references := getattr(element, "HasExternalReferences", None)) is not None or ( if (references := getattr(element, "HasExternalReferences", None)) is not None or (
@@ -52,6 +58,11 @@ def get_references(element: ifcopenshell.entity_instance, should_inherit=True) -
def get_classification(reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: def get_classification(reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Get the IfcClassification that a classification reference belongs to
:param reference: An IfcClassificationReference
:return: IfcClassification
"""
if reference.is_a("IfcClassification"): if reference.is_a("IfcClassification"):
return reference return reference
return get_classification(reference.ReferencedSource) if reference.ReferencedSource is not None else None return get_classification(reference.ReferencedSource) if reference.ReferencedSource is not None else None