diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 5d3ceeeee3..94f9df787e 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -203,6 +203,8 @@ endif cp -r dist/working/IfcOpenShell-0.7.0/src/ifcfm/ifcfm dist/blenderbim/libs/site/packages/ # Provides IFCCOBie functionality cp -r dist/working/IfcOpenShell-0.7.0/src/ifccobie/* dist/blenderbim/libs/site/packages/ + # Provides bSDD functionality + cp -r dist/working/IfcOpenShell-0.7.0/src/bsdd/* dist/blenderbim/libs/site/packages/ # Provides IFCDiff functionality cp -r dist/working/IfcOpenShell-0.7.0/src/ifcdiff/* dist/blenderbim/libs/site/packages/ # Provides IFCCSV functionality diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 5f2e0fe87e..c21240c12b 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -30,6 +30,7 @@ modules = { "project": None, "search": None, "bcf": None, + "bsdd": None, "root": None, "unit": None, "model": None, diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/__init__.py b/src/blenderbim/blenderbim/bim/module/bsdd/__init__.py new file mode 100644 index 0000000000..d56f214697 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/bsdd/__init__.py @@ -0,0 +1,40 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on 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. +# +# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see . + +import bpy +from . import ui, prop, operator + +classes = ( + operator.LoadBSDDDomains, + operator.SearchBSDDClassifications, + operator.SetActiveBSDDDomain, + prop.BSDDDomain, + prop.BSDDClassification, + prop.BIMBSDDProperties, + ui.BIM_UL_bsdd_domains, + ui.BIM_UL_bsdd_classifications, + ui.BIM_PT_bsdd, +) + + +def register(): + bpy.types.Scene.BIMBSDDProperties = bpy.props.PointerProperty(type=prop.BIMBSDDProperties) + + +def unregister(): + del bpy.types.Scene.BIMBSDDProperties diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/operator.py b/src/blenderbim/blenderbim/bim/module/bsdd/operator.py new file mode 100644 index 0000000000..f7cb918e59 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/bsdd/operator.py @@ -0,0 +1,85 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on 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. +# +# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see . + +import os +import bpy +import bsdd +import ifcopenshell +import blenderbim.tool as tool + + +class LoadBSDDDomains(bpy.types.Operator): + bl_idname = "bim.load_bsdd_domains" + bl_label = "Load bSDD Domains" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMBSDDProperties + props.domains.clear() + client = bsdd.Client() + for domain in sorted(client.Domain(), key=lambda x: x["name"]): + new = props.domains.add() + new.name = domain["name"] + new.namespace_uri = domain["namespaceUri"] + new.default_language_code = domain["defaultLanguageCode"] + new.organization_name_owner = domain["organizationNameOwner"] + new.status = domain["status"] + new.version = domain["version"] + return {"FINISHED"} + + +class SetActiveBSDDDomain(bpy.types.Operator): + bl_idname = "bim.set_active_bsdd_domain" + bl_label = "Load bSDD Domains" + bl_options = {"REGISTER", "UNDO"} + name: bpy.props.StringProperty() + uri: bpy.props.StringProperty() + + def execute(self, context): + props = context.scene.BIMBSDDProperties + props.active_domain = self.name + props.active_uri = self.uri + return {"FINISHED"} + + +class SearchBSDDClassifications(bpy.types.Operator): + bl_idname = "bim.search_bsdd_classifications" + bl_label = "Search bSDD Classifications" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMBSDDProperties + props.classifications.clear() + client = bsdd.Client() + related_ifc_entities = [] + if len(props.keyword) < 3: + return {"FINISHED"} + if props.should_filter_ifc_class and context.active_object: + element = tool.Ifc.get_entity(context.active_object) + if element: + related_ifc_entities = [element.is_a()] + results = client.ClassificationSearchOpen(props.keyword, DomainNamespaceUris=[props.active_uri], RelatedIfcEntities=related_ifc_entities) + for result in sorted(results["classifications"], key=lambda x: x["referenceCode"]): + new = props.classifications.add() + new.name = result["name"] + new.reference_code = result["referenceCode"] + new.description = result["description"] + new.namespace_uri = result["namespaceUri"] + new.domain_name = result["domainName"] + new.domain_namespace_uri = result["domainNamespaceUri"] + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/prop.py b/src/blenderbim/blenderbim/bim/module/bsdd/prop.py new file mode 100644 index 0000000000..1a12b32809 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/bsdd/prop.py @@ -0,0 +1,59 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on 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. +# +# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see . + +import bpy +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +class BSDDDomain(PropertyGroup): + name: StringProperty(name="Name") + namespace_uri: StringProperty(name="URI") + default_language_code: StringProperty(name="Language") + organization_name_owner: StringProperty(name="Organization") + status: StringProperty(name="Status") + version: StringProperty(name="Version") + + +class BSDDClassification(PropertyGroup): + name: StringProperty(name="Name") + reference_code: StringProperty(name="Reference Code") + description: StringProperty(name="Description") + namespace_uri: StringProperty(name="Namespace URI") + domain_name: StringProperty(name="Domain Name") + domain_namespace_uri: StringProperty(name="Domain Namespace URI") + + +class BIMBSDDProperties(PropertyGroup): + active_domain: StringProperty(name="Active Domain") + active_uri: StringProperty(name="Active URI") + domains: CollectionProperty(name="Domains", type=BSDDDomain) + active_domain_index: IntProperty(name="Active Domain Index") + classifications: CollectionProperty(name="Classifications", type=BSDDClassification) + active_classification_index: IntProperty(name="Active Classification Index") + keyword: StringProperty(name="Keyword") + should_filter_ifc_class: BoolProperty(name="Filter Active IFC Class", default=True) diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/ui.py b/src/blenderbim/blenderbim/bim/module/bsdd/ui.py new file mode 100644 index 0000000000..8577c3242a --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/bsdd/ui.py @@ -0,0 +1,71 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on 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. +# +# BlenderBIM Add-on 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 BlenderBIM Add-on. If not, see . + +import blenderbim.tool as tool +from bpy.types import Panel, UIList +from blenderbim.bim.ifc import IfcStore + + +class BIM_PT_bsdd(Panel): + bl_label = "buildingSMART Data Dictionary" + bl_idname = "BIM_PT_bsdd" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_project_setup" + + def draw(self, context): + props = context.scene.BIMBSDDProperties + if props.active_domain: + 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") + + if len(props.domains): + self.layout.template_list( + "BIM_UL_bsdd_domains", + "", + props, + "domains", + props, + "active_domain_index", + ) + else: + row = self.layout.row() + row.operator("bim.load_bsdd_domains") + + +class BIM_UL_bsdd_domains(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=f"{item.name} ({item.organization_name_owner})") + op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF") + op.name = item.name + op.uri = item.namespace_uri + + +class BIM_UL_bsdd_classifications(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.reference_code) + row.label(text=item.name) diff --git a/src/blenderbim/blenderbim/bim/module/classification/__init__.py b/src/blenderbim/blenderbim/bim/module/classification/__init__.py index f5fdcaaaed..d59b2656b3 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/classification/__init__.py @@ -21,7 +21,9 @@ from . import ui, prop, operator classes = ( operator.AddClassification, + operator.AddClassificationFromBSDD, operator.AddClassificationReference, + operator.AddClassificationReferenceFromBSDD, operator.ChangeClassificationLevel, operator.DisableEditingClassification, operator.DisableEditingClassificationReference, diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index d9b1b56845..9af38e5058 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -54,6 +54,25 @@ class AddClassification(bpy.types.Operator, tool.Ifc.Operator): ) +class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_classification_from_bsdd" + bl_label = "Add Classification From bSDD" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + props = context.scene.BIMBSDDProperties + domain = [d for d in props.domains if d.name == props.active_domain][0] + for element in tool.Ifc.get().by_type("IfcClassification"): + if element.Name == props.active_domain or element.Location == domain.namespace_uri: + return + classification = ifcopenshell.api.run( + "classification.add_classification", tool.Ifc.get(), classification=props.active_domain + ) + classification.Source = domain.organization_name_owner + classification.Location = domain.namespace_uri + classification.Edition = domain.version + + class EnableEditingClassification(bpy.types.Operator): bl_idname = "bim.enable_editing_classification" bl_label = "Enable Editing Classification" @@ -254,6 +273,56 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator): ) +class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_classification_reference_from_bsdd" + bl_label = "Add Classification Reference From bSDD" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + obj_type: bpy.props.StringProperty() + + def _execute(self, context): + if self.obj_type == "Object": + if context.selected_objects: + objects = [o.name for o in context.selected_objects] + else: + objects = [context.active_object.name] + else: + objects = [self.obj] + props = context.scene.BIMClassificationProperties + bprops = context.scene.BIMBSDDProperties + + bsdd_classification = bprops.classifications[bprops.active_classification_index] + + classification = None + for element in tool.Ifc.get().by_type("IfcClassification"): + if ( + element.Name == bsdd_classification.domain_name + or element.Location == bsdd_classification.domain_namespace_uri + ): + classification = element + break + + if not classification: + classification = ifcopenshell.api.run( + "classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name + ) + classification.Location = bsdd_classification.domain_namespace_uri + + for obj in objects: + ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) + if not ifc_definition_id: + continue + reference = ifcopenshell.api.run( + "classification.add_reference", + tool.Ifc.get(), + product=tool.Ifc.get().by_id(ifc_definition_id), + classification=classification, + identification=bsdd_classification.reference_code, + name=bsdd_classification.name, + ) + reference.Location = bsdd_classification.namespace_uri + + class ChangeClassificationLevel(bpy.types.Operator): bl_idname = "bim.change_classification_level" bl_label = "Change Classification Level" diff --git a/src/blenderbim/blenderbim/bim/module/classification/prop.py b/src/blenderbim/blenderbim/bim/module/classification/prop.py index eee7b14952..22c4e6af5c 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/prop.py +++ b/src/blenderbim/blenderbim/bim/module/classification/prop.py @@ -48,6 +48,15 @@ class ClassificationReference(PropertyGroup): class BIMClassificationProperties(PropertyGroup): + classification_source: EnumProperty( + 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") classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute) active_classification_id: IntProperty(name="Active Classification Id") diff --git a/src/blenderbim/blenderbim/bim/module/classification/ui.py b/src/blenderbim/blenderbim/bim/module/classification/ui.py index 5a4de6bf65..965b215643 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/ui.py +++ b/src/blenderbim/blenderbim/bim/module/classification/ui.py @@ -48,6 +48,42 @@ class BIM_PT_classifications(Panel): self.props = context.scene.BIMClassificationProperties + row = self.layout.row(align=True) + row.label(text="Source", icon="OUTLINER") + row.prop(self.props, "classification_source", text="") + + if self.props.classification_source == "FILE": + self.draw_add_file_ui(context) + elif self.props.classification_source == "BSDD": + self.draw_add_bsdd_ui(context) + elif self.props.classification_source == "MANUAL": + self.draw_add_manual_ui(context) + + for classification in ClassificationsData.data["classifications"]: + if self.props.active_classification_id == classification["id"]: + self.draw_editable_ui() + else: + self.draw_ui(classification) + + def draw_add_manual_ui(self, context): + row = self.layout.row() + row.label(text="TODO", icon="ERROR") + + 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 = self.layout.row() + row.operator("bim.add_classification_from_bsdd", icon="ADD") + + def draw_add_file_ui(self, context): if ClassificationsData.data["has_classification_file"]: row = self.layout.row(align=True) row.prop(self.props, "available_classifications", text="") @@ -58,12 +94,6 @@ class BIM_PT_classifications(Panel): row.label(text="No Active Classification Library") row.operator("bim.load_classification_library", text="", icon="IMPORT") - for classification in ClassificationsData.data["classifications"]: - if self.props.active_classification_id == classification["id"]: - self.draw_editable_ui() - else: - self.draw_ui(classification) - def draw_editable_ui(self): row = self.layout.row(align=True) row.operator("bim.edit_classification", text="Save changes", icon="CHECKMARK") @@ -84,6 +114,7 @@ class ReferenceUI: obj = context.active_object self.oprops = obj.BIMObjectProperties self.sprops = context.scene.BIMClassificationProperties + self.bprops = context.scene.BIMBSDDProperties self.props = obj.BIMClassificationReferenceProperties self.file = IfcStore.get_file() @@ -100,14 +131,68 @@ class ReferenceUI: self.draw_reference_ui(reference) def draw_add_ui(self, context): + row = self.layout.row(align=True) + row.label(text="Source", icon="OUTLINER") + row.prop(self.sprops, "classification_source", text="") + + if self.sprops.classification_source == "FILE": + self.draw_add_file_ui(context) + elif self.sprops.classification_source == "BSDD": + self.draw_add_bsdd_ui(context) + elif self.sprops.classification_source == "MANUAL": + self.draw_add_manual_ui(context) + + def draw_add_manual_ui(self, context): + row = self.layout.row() + row.label(text="TODO", icon="ERROR") + + 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 = self.layout.row(align=True) + row.prop(self.bprops, "keyword", text="") + row.operator("bim.search_bsdd_classifications", text="", icon="VIEWZOOM") + + row = self.layout.row() + row.prop(self.bprops, "should_filter_ifc_class") + + if len(self.bprops.classifications): + self.layout.template_list( + "BIM_UL_bsdd_classifications", + "", + self.bprops, + "classifications", + self.bprops, + "active_classification_index", + ) + else: + row = self.layout.row() + row.label(text="No Search Results") + + if self.bprops.active_classification_index < len(self.bprops.classifications): + row = self.layout.row() + op = row.operator( + "bim.add_classification_reference_from_bsdd", text="Add Classification Reference", icon="ADD" + ) + op.obj = self.obj + op.obj_type = self.obj_type + + def draw_add_file_ui(self, context): if not self.data.data["active_classification_library"]: row = self.layout.row(align=True) - row.label(text="No Active Classification Library") + row.label(text="No Active Classification Library", icon="ERROR") row.operator("bim.load_classification_library", text="", icon="IMPORT") return + row = self.layout.row(align=True) row.label(text=f"Active Classification Library: {self.data.data['active_classification_library']}") - #row.prop(self.sprops, "available_classifications", text="") + # row.prop(self.sprops, "available_classifications", text="") if not self.sprops.available_library_references: op = row.operator("bim.change_classification_level", text="", icon="GREASEPENCIL") op.parent_id = int(self.sprops.available_classifications)