From 67952ed159280ce7834a2271550ac89c06a2a092 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 20 Jan 2022 18:21:57 +1100 Subject: [PATCH] Fix #1576. Managing IfcSystem subtypes is now possible. --- .../blenderbim/bim/module/system/__init__.py | 13 +- .../blenderbim/bim/module/system/data.py | 82 +++++++++ .../blenderbim/bim/module/system/operator.py | 174 +++++------------- .../blenderbim/bim/module/system/prop.py | 9 + .../blenderbim/bim/module/system/ui.py | 66 ++++--- src/blenderbim/blenderbim/core/system.py | 66 +++++++ src/blenderbim/blenderbim/core/tool.py | 12 ++ src/blenderbim/blenderbim/tool/__init__.py | 1 + src/blenderbim/blenderbim/tool/system.py | 67 +++++++ src/blenderbim/pytest.ini | 1 + .../test/bim/feature/system.feature | 97 ++++++++++ src/blenderbim/test/core/bootstrap.py | 7 + src/blenderbim/test/core/test_system.py | 90 +++++++++ src/blenderbim/test/tool/test_system.py | 152 +++++++++++++++ src/ifcopenshell-python/Makefile | 4 +- .../ifcopenshell/api/system/add_system.py | 4 +- .../ifcopenshell/util/system.py | 36 ++++ .../test/api/system/test_add_system.py | 28 +++ .../test/api/system/test_remove_system.py | 27 +++ .../test/util/test_system.py | 47 +++++ 20 files changed, 817 insertions(+), 166 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/system/data.py create mode 100644 src/blenderbim/blenderbim/core/system.py create mode 100644 src/blenderbim/blenderbim/tool/system.py create mode 100644 src/blenderbim/test/bim/feature/system.feature create mode 100644 src/blenderbim/test/core/test_system.py create mode 100644 src/blenderbim/test/tool/test_system.py create mode 100644 src/ifcopenshell-python/ifcopenshell/util/system.py create mode 100644 src/ifcopenshell-python/test/api/system/test_add_system.py create mode 100644 src/ifcopenshell-python/test/api/system/test_remove_system.py create mode 100644 src/ifcopenshell-python/test/util/test_system.py diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py index 115d400737..7610039899 100644 --- a/src/blenderbim/blenderbim/bim/module/system/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py @@ -20,17 +20,16 @@ import bpy from . import ui, prop, operator classes = ( - operator.LoadSystems, - operator.DisableSystemEditingUI, operator.AddSystem, - operator.EditSystem, - operator.RemoveSystem, - operator.ToggleAssigningSystem, operator.AssignSystem, - operator.UnassignSystem, - operator.EnableEditingSystem, operator.DisableEditingSystem, + operator.DisableSystemEditingUI, + operator.EditSystem, + operator.EnableEditingSystem, + operator.LoadSystems, + operator.RemoveSystem, operator.SelectSystemProducts, + operator.UnassignSystem, prop.System, prop.BIMSystemProperties, ui.BIM_PT_systems, diff --git a/src/blenderbim/blenderbim/bim/module/system/data.py b/src/blenderbim/blenderbim/bim/module/system/data.py new file mode 100644 index 0000000000..f2af809ec0 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/system/data.py @@ -0,0 +1,82 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 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 +import ifcopenshell +import ifcopenshell.util.schema +import blenderbim.tool as tool + + +def refresh(): + SystemData.is_loaded = False + ObjectSystemData.is_loaded = False + + +class SystemData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = { + "system_class": cls.system_class(), + "total_systems": cls.total_systems(), + } + cls.is_loaded = True + + @classmethod + def system_class(cls): + declaration = tool.Ifc.schema().declaration_by_name("IfcSystem") + declarations = ifcopenshell.util.schema.get_subtypes(declaration) + # We're only interested in systems for services. Not sure why IFC groups these together. + return [ + (c, c, "") + for c in sorted([d.name() for d in declarations]) + if c not in ("IfcZone", "IfcStructuralAnalysisModel") + ] + + @classmethod + def total_systems(cls): + return len(tool.Ifc.get().by_type("IfcSystem")) + + +class ObjectSystemData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = { + "systems": cls.systems(), + "total_systems": cls.total_systems(), + } + cls.is_loaded = True + + @classmethod + def systems(cls): + results = [] + element = tool.Ifc.get_entity(bpy.context.active_object) + if not element: + return results + for system in ifcopenshell.util.system.get_element_systems(element): + results.append({"id": system.id(), "name": system.Name or "Unnamed", "ifc_class": system.is_a()}) + return results + + @classmethod + def total_systems(cls): + return len(tool.Ifc.get().by_type("IfcSystem")) diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py index bbfa9c40eb..aaf3a623b6 100644 --- a/src/blenderbim/blenderbim/bim/module/system/operator.py +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -17,208 +17,118 @@ # along with BlenderBIM Add-on. If not, see . import bpy -import ifcopenshell.util.attribute import ifcopenshell.api -import blenderbim.bim.helper +import blenderbim.tool as tool +import blenderbim.core.system as core +import blenderbim.bim.handler from blenderbim.bim.ifc import IfcStore -from ifcopenshell.api.system.data import Data -class LoadSystems(bpy.types.Operator): +class Operator: + def execute(self, context): + IfcStore.execute_ifc_operator(self, context) + blenderbim.bim.handler.refresh_ui_data() + return {"FINISHED"} + + +class LoadSystems(bpy.types.Operator, Operator): bl_idname = "bim.load_systems" bl_label = "Load Systems" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - props = context.scene.BIMSystemProperties - props.systems.clear() - for ifc_definition_id, system in Data.systems.items(): - new = props.systems.add() - new.ifc_definition_id = ifc_definition_id - new.name = system["Name"] - props.is_editing = True - bpy.ops.bim.disable_editing_system() - return {"FINISHED"} + def _execute(self, context): + core.load_systems(tool.System) -class DisableSystemEditingUI(bpy.types.Operator): +class DisableSystemEditingUI(bpy.types.Operator, Operator): bl_idname = "bim.disable_system_editing_ui" bl_label = "Disable System Editing UI" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - context.scene.BIMSystemProperties.is_editing = False - context.scene.BIMSystemProperties.active_system_id = 0 - return {"FINISHED"} + def _execute(self, context): + core.disable_system_editing_ui(tool.System) -class AddSystem(bpy.types.Operator): +class AddSystem(bpy.types.Operator, Operator): bl_idname = "bim.add_system" bl_label = "Add System" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - result = ifcopenshell.api.run("system.add_system", IfcStore.get_file()) - Data.load(IfcStore.get_file()) - bpy.ops.bim.load_systems() - bpy.ops.bim.enable_editing_system(system=result.id()) - return {"FINISHED"} + core.add_system(tool.Ifc, tool.System, ifc_class=context.scene.BIMSystemProperties.system_class) -class EditSystem(bpy.types.Operator): +class EditSystem(bpy.types.Operator, Operator): bl_idname = "bim.edit_system" bl_label = "Edit System" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - props = context.scene.BIMSystemProperties - attributes = {} - for attribute in props.system_attributes: - if attribute.is_null: - attributes[attribute.name] = None - else: - attributes[attribute.name] = attribute.string_value - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "system.edit_system", - self.file, - **{"system": self.file.by_id(props.active_system_id), "attributes": attributes} + core.edit_system( + tool.Ifc, tool.System, system=tool.Ifc.get().by_id(context.scene.BIMSystemProperties.active_system_id) ) - Data.load(IfcStore.get_file()) - bpy.ops.bim.load_systems() - return {"FINISHED"} -class RemoveSystem(bpy.types.Operator): +class RemoveSystem(bpy.types.Operator, Operator): bl_idname = "bim.remove_system" bl_label = "Remove System" bl_options = {"REGISTER", "UNDO"} system: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - props = context.scene.BIMSystemProperties - self.file = IfcStore.get_file() - ifcopenshell.api.run("system.remove_system", self.file, **{"system": self.file.by_id(self.system)}) - Data.load(IfcStore.get_file()) - bpy.ops.bim.load_systems() - return {"FINISHED"} + core.remove_system(tool.Ifc, tool.System, system=tool.Ifc.get().by_id(self.system)) -class EnableEditingSystem(bpy.types.Operator): +class EnableEditingSystem(bpy.types.Operator, Operator): bl_idname = "bim.enable_editing_system" bl_label = "Enable Editing System" bl_options = {"REGISTER", "UNDO"} system: bpy.props.IntProperty() - def execute(self, context): - props = context.scene.BIMSystemProperties - props.system_attributes.clear() - - blenderbim.bim.helper.import_attributes("IfcSystem", props.system_attributes, Data.systems[self.system]) - props.active_system_id = self.system - return {"FINISHED"} + def _execute(self, context): + core.enable_editing_system(tool.System, system=tool.Ifc.get().by_id(self.system)) -class DisableEditingSystem(bpy.types.Operator): +class DisableEditingSystem(bpy.types.Operator, Operator): bl_idname = "bim.disable_editing_system" bl_label = "Disable Editing System" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - context.scene.BIMSystemProperties.active_system_id = 0 - return {"FINISHED"} + def _execute(self, context): + core.disable_editing_system(tool.System) -class ToggleAssigningSystem(bpy.types.Operator): - bl_idname = "bim.toggle_assigning_system" - bl_label = "Toggle Assigning System" - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - context.scene.BIMSystemProperties.is_adding = not context.scene.BIMSystemProperties.is_adding - return {"FINISHED"} - - -class AssignSystem(bpy.types.Operator): +class AssignSystem(bpy.types.Operator, Operator): bl_idname = "bim.assign_system" bl_label = "Assign System" bl_options = {"REGISTER", "UNDO"} - product: bpy.props.StringProperty() system: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - self.file = IfcStore.get_file() - products = [bpy.data.objects.get(self.product)] if self.product else context.selected_objects - for product in products: - if not product.BIMObjectProperties.ifc_definition_id: - continue - ifcopenshell.api.run( - "system.assign_system", - self.file, - **{ - "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id), - "system": self.file.by_id(self.system), - } - ) - Data.load(self.file) - return {"FINISHED"} + for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if element: + core.assign_system(tool.Ifc, system=tool.Ifc.get().by_id(self.system), product=element) -class UnassignSystem(bpy.types.Operator): +class UnassignSystem(bpy.types.Operator, Operator): bl_idname = "bim.unassign_system" bl_label = "Unassign System" bl_options = {"REGISTER", "UNDO"} - product: bpy.props.StringProperty() system: bpy.props.IntProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - self.file = IfcStore.get_file() - products = [bpy.data.objects.get(self.product)] if self.product else context.selected_objects - for product in products: - props = product.BIMObjectProperties - if not props.ifc_definition_id: - continue - if not (props.ifc_definition_id in Data.products and self.system in Data.products[props.ifc_definition_id]): - continue - ifcopenshell.api.run( - "system.unassign_system", - self.file, - **{ - "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id), - "system": self.file.by_id(self.system), - } - ) - Data.load(self.file) - return {"FINISHED"} + for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if element: + core.unassign_system(tool.Ifc, system=tool.Ifc.get().by_id(self.system), product=element) -class SelectSystemProducts(bpy.types.Operator): +class SelectSystemProducts(bpy.types.Operator, Operator): bl_idname = "bim.select_system_products" bl_label = "Select System Products" bl_options = {"REGISTER", "UNDO"} system: bpy.props.IntProperty() - def execute(self, context): - for obj in context.visible_objects: - obj.select_set(False) - if not obj.BIMObjectProperties.ifc_definition_id: - continue - product_systems = Data.products.get(obj.BIMObjectProperties.ifc_definition_id, []) - if self.system in product_systems: - obj.select_set(True) - return {"FINISHED"} + def _execute(self, context): + core.select_system_products(tool.System, system=tool.Ifc.get().by_id(self.system)) diff --git a/src/blenderbim/blenderbim/bim/module/system/prop.py b/src/blenderbim/blenderbim/bim/module/system/prop.py index d38c867e25..3d31414ffd 100644 --- a/src/blenderbim/blenderbim/bim/module/system/prop.py +++ b/src/blenderbim/blenderbim/bim/module/system/prop.py @@ -17,6 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy +from blenderbim.bim.module.system.data import SystemData from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -31,8 +32,15 @@ from bpy.props import ( ) +def get_system_class(self, context): + if not SystemData.is_loaded: + SystemData.load() + return SystemData.data["system_class"] + + class System(PropertyGroup): name: StringProperty(name="Name") + ifc_class: StringProperty(name="IFC Class") ifc_definition_id: IntProperty(name="IFC Definition ID") @@ -43,3 +51,4 @@ class BIMSystemProperties(PropertyGroup): systems: CollectionProperty(name="Systems", type=System) active_system_index: IntProperty(name="Active System Index") active_system_id: IntProperty(name="Active System Id") + system_class: EnumProperty(items=get_system_class, name="Class") diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index 5c72194381..594d479e99 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -18,7 +18,7 @@ from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore -from ifcopenshell.api.system.data import Data +from blenderbim.bim.module.system.data import SystemData, ObjectSystemData class BIM_PT_systems(Panel): @@ -35,15 +35,18 @@ class BIM_PT_systems(Panel): return IfcStore.get_file() def draw(self, context): - if not Data.is_loaded: - Data.load(IfcStore.get_file()) + if not SystemData.is_loaded: + SystemData.load() self.props = context.scene.BIMSystemProperties row = self.layout.row(align=True) - row.label(text="{} Systems Found".format(len(Data.systems)), icon="OUTLINER") + row.label(text="{} Systems Found".format(SystemData.data["total_systems"]), icon="OUTLINER") if self.props.is_editing: - row.operator("bim.add_system", text="", icon="ADD") row.operator("bim.disable_system_editing_ui", text="", icon="CANCEL") + + row = self.layout.row(align=True) + row.prop(self.props, "system_class", text="") + row.operator("bim.add_system", text="", icon="ADD") else: row.operator("bim.load_systems", text="", icon="GREASEPENCIL") @@ -84,13 +87,13 @@ class BIM_PT_object_systems(Panel): return IfcStore.get_file() and context.active_object.BIMObjectProperties.ifc_definition_id def draw(self, context): - if not Data.is_loaded: - Data.load(IfcStore.get_file()) + if not ObjectSystemData.is_loaded: + ObjectSystemData.load() self.props = context.scene.BIMSystemProperties - row = self.layout.row(align=True) - if self.props.is_adding: - row.label(text="Adding Systems", icon="OUTLINER") - row.operator("bim.toggle_assigning_system", text="", icon="CANCEL") + if self.props.is_editing: + row = self.layout.row() + row.alignment = "RIGHT" + row.operator("bim.disable_system_editing_ui", text="", icon="CANCEL") self.layout.template_list( "BIM_UL_object_systems", "", @@ -100,17 +103,23 @@ class BIM_PT_object_systems(Panel): "active_system_index", ) else: - row.label(text=f"{len(Data.systems)} Systems in IFC Project", icon="OUTLINER") - row.operator("bim.toggle_assigning_system", text="", icon="ADD") - - systems_object = Data.products.get(context.active_object.BIMObjectProperties.ifc_definition_id, []) - for system_id in systems_object: row = self.layout.row(align=True) - row.label(text=Data.systems[system_id].get("Name", "Unnamed")) + row.label(text=f"{ObjectSystemData.data['total_systems']} Systems in IFC Project", icon="OUTLINER") + row.operator("bim.load_systems", text="", icon="GREASEPENCIL") + + system_icons = { + "IfcSystem": "EXTERNAL_DRIVE", + "IfcDistributionSystem": "NETWORK_DRIVE", + "IfcDistributionCircuit": "DRIVER", + "IfcBuildingSystem": "MOD_BUILD", + } + for system in ObjectSystemData.data["systems"]: + row = self.layout.row(align=True) + row.label(text=system["name"], icon=system_icons[system["ifc_class"]]) op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") - op.system = system_id + op.system = system["id"] op = row.operator("bim.unassign_system", text="", icon="X") - op.system = system_id + op.system = system["id"] if not systems_object: self.layout.label(text="No System associated with Active Object") @@ -118,9 +127,15 @@ class BIM_PT_object_systems(Panel): class BIM_UL_systems(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + system_icons = { + "IfcSystem": "EXTERNAL_DRIVE", + "IfcDistributionSystem": "NETWORK_DRIVE", + "IfcDistributionCircuit": "DRIVER", + "IfcBuildingSystem": "MOD_BUILD", + } if item: row = layout.row(align=True) - row.label(text=item.name) + row.label(text=item.name, icon=system_icons[item.ifc_class]) system_id = item.ifc_definition_id if context.scene.BIMSystemProperties.active_system_id == system_id: op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") @@ -143,8 +158,13 @@ class BIM_UL_systems(UIList): class BIM_UL_object_systems(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + system_icons = { + "IfcSystem": "EXTERNAL_DRIVE", + "IfcDistributionSystem": "NETWORK_DRIVE", + "IfcDistributionCircuit": "DRIVER", + "IfcBuildingSystem": "MOD_BUILD", + } if item: row = layout.row(align=True) - row.label(text=item.name) - op = row.operator("bim.assign_system", text="", icon="ADD") - op.system = item.ifc_definition_id + row.label(text=item.name, icon=system_icons[item.ifc_class]) + row.operator("bim.assign_system", text="", icon="ADD").system = item.ifc_definition_id diff --git a/src/blenderbim/blenderbim/core/system.py b/src/blenderbim/blenderbim/core/system.py new file mode 100644 index 0000000000..ae58433f72 --- /dev/null +++ b/src/blenderbim/blenderbim/core/system.py @@ -0,0 +1,66 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 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 . + + +def load_systems(system): + system.import_systems() + system.enable_system_editing_ui() + system.disable_editing_system() + + +def disable_system_editing_ui(system): + system.disable_editing_system() + system.disable_system_editing_ui() + + +def add_system(ifc, system, ifc_class=None): + ifc.run("system.add_system", ifc_class=ifc_class) + system.import_systems() + + +def edit_system(ifc, system_tool, system=None): + attributes = system_tool.export_system_attributes() + ifc.run("system.edit_system", system=system, attributes=attributes) + system_tool.disable_editing_system() + system_tool.import_systems() + + +def remove_system(ifc, system_tool, system=None): + ifc.run("system.remove_system", system=system) + system_tool.import_systems() + + +def enable_editing_system(system_tool, system=None): + system_tool.import_system_attributes(system) + system_tool.set_active_system(system) + + +def disable_editing_system(system): + system.disable_editing_system() + + +def assign_system(ifc, system=None, product=None): + ifc.run("system.assign_system", product=product, system=system) + + +def unassign_system(ifc, system=None, product=None): + ifc.run("system.unassign_system", product=product, system=system) + + +def select_system_products(system_tool, system=None): + system_tool.select_system_products(system) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 1d620d2abf..cdb4c9a651 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -315,6 +315,18 @@ class Surveyor: def get_absolute_matrix(cls, obj): pass +@interface +class System: + def disable_editing_system(cls): pass + def disable_system_editing_ui(cls): pass + def enable_system_editing_ui(cls): pass + def export_system_attributes(cls): pass + def import_system_attributes(cls, system): pass + def import_systems(cls): pass + def select_system_products(cls, system): pass + def set_active_system(cls, system): pass + + @interface class Type: def change_object_data(cls, obj, data, is_global=False): pass diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index bdd34ab26b..9d0172432e 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -37,5 +37,6 @@ from blenderbim.tool.spatial import Spatial from blenderbim.tool.structural import Structural from blenderbim.tool.style import Style from blenderbim.tool.surveyor import Surveyor +from blenderbim.tool.system import System from blenderbim.tool.type import Type from blenderbim.tool.unit import Unit diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py new file mode 100644 index 0000000000..906510f89b --- /dev/null +++ b/src/blenderbim/blenderbim/tool/system.py @@ -0,0 +1,67 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 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 +import ifcopenshell.util.system +import blenderbim.core.tool +import blenderbim.tool as tool + + +class System(blenderbim.core.tool.System): + @classmethod + def disable_editing_system(cls): + bpy.context.scene.BIMSystemProperties.active_system_id = 0 + + @classmethod + def disable_system_editing_ui(cls): + bpy.context.scene.BIMSystemProperties.is_editing = False + + @classmethod + def enable_system_editing_ui(cls): + bpy.context.scene.BIMSystemProperties.is_editing = True + + @classmethod + def export_system_attributes(cls): + return blenderbim.bim.helper.export_attributes(bpy.context.scene.BIMSystemProperties.system_attributes) + + @classmethod + def import_system_attributes(cls, system): + blenderbim.bim.helper.import_attributes2(system, bpy.context.scene.BIMSystemProperties.system_attributes) + + @classmethod + def import_systems(cls): + props = bpy.context.scene.BIMSystemProperties + props.systems.clear() + for system in tool.Ifc.get().by_type("IfcSystem"): + if system.is_a() in ["IfcZone", "IfcStructuralAnalysisModel"]: + continue + new = props.systems.add() + new.ifc_definition_id = system.id() + new.name = system.Name or "Unnamed" + new.ifc_class = system.is_a() + + @classmethod + def select_system_products(cls, system): + for element in ifcopenshell.util.system.get_system_elements(system): + obj = tool.Ifc.get_object(element) + if obj: + obj.select_set(True) + + @classmethod + def set_active_system(cls, system): + bpy.context.scene.BIMSystemProperties.active_system_id = system.id() diff --git a/src/blenderbim/pytest.ini b/src/blenderbim/pytest.ini index 8e1e234f3f..71d660e766 100644 --- a/src/blenderbim/pytest.ini +++ b/src/blenderbim/pytest.ini @@ -23,6 +23,7 @@ markers = spatial structural style + system type unit void diff --git a/src/blenderbim/test/bim/feature/system.feature b/src/blenderbim/test/bim/feature/system.feature new file mode 100644 index 0000000000..1b7143fe63 --- /dev/null +++ b/src/blenderbim/test/bim/feature/system.feature @@ -0,0 +1,97 @@ +@system +Feature: System + +Scenario: Load systems + Given an empty IFC project + When I press "bim.load_systems" + Then nothing happens + +Scenario: Disable system editing UI + Given an empty IFC project + When I press "bim.load_systems" + And I press "bim.disable_system_editing_ui" + Then nothing happens + +Scenario: Add system + Given an empty IFC project + And I press "bim.load_systems" + When I press "bim.add_system" + Then nothing happens + +Scenario: Edit system + Given an empty IFC project + And I press "bim.load_systems" + And I press "bim.add_system" + And the variable "system" is "{ifc}.by_type('IfcSystem')[0].id()" + And I press "bim.enable_editing_system(system={system})" + When I press "bim.edit_system" + Then nothing happens + +Scenario: Edit system + Given an empty IFC project + And I press "bim.load_systems" + And I press "bim.add_system" + And the variable "system" is "{ifc}.by_type('IfcSystem')[0].id()" + When I press "bim.remove_system(system={system})" + Then nothing happens + +Scenario: Enable editing system + Given an empty IFC project + And I press "bim.load_systems" + And I press "bim.add_system" + And the variable "system" is "{ifc}.by_type('IfcSystem')[0].id()" + When I press "bim.enable_editing_system(system={system})" + Then nothing happens + +Scenario: Disable editing system + Given an empty IFC project + And I press "bim.load_systems" + And I press "bim.add_system" + And the variable "system" is "{ifc}.by_type('IfcSystem')[0].id()" + And I press "bim.enable_editing_system(system={system})" + When I press "bim.disable_editing_system" + Then nothing happens + +Scenario: Assign system + Given an empty IFC project + And I press "bim.load_systems" + And I press "bim.add_system" + And the variable "system" is "{ifc}.by_type('IfcSystem')[0].id()" + And I press "bim.enable_editing_system(system={system})" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcPump" + And I press "bim.assign_class" + And the object "IfcPump/Cube" is selected + When I press "bim.assign_system(system={system})" + Then nothing happens + +Scenario: Unassign system + Given an empty IFC project + And I press "bim.load_systems" + And I press "bim.add_system" + And the variable "system" is "{ifc}.by_type('IfcSystem')[0].id()" + And I press "bim.enable_editing_system(system={system})" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcPump" + And I press "bim.assign_class" + And the object "IfcPump/Cube" is selected + And I press "bim.assign_system(system={system})" + When I press "bim.unassign_system(system={system})" + Then nothing happens + +Scenario: Select system products + Given an empty IFC project + And I press "bim.load_systems" + And I press "bim.add_system" + And the variable "system" is "{ifc}.by_type('IfcSystem')[0].id()" + And I press "bim.enable_editing_system(system={system})" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcPump" + And I press "bim.assign_class" + And the object "IfcPump/Cube" is selected + And I press "bim.assign_system(system={system})" + When I press "bim.select_system_products(system={system})" + Then nothing happens diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py index b27dd5e05e..42dab7d0e4 100644 --- a/src/blenderbim/test/core/bootstrap.py +++ b/src/blenderbim/test/core/bootstrap.py @@ -168,6 +168,13 @@ def surveyor(): prophet.verify() +@pytest.fixture +def system(): + prophet = Prophecy(blenderbim.core.tool.System) + yield prophet + prophet.verify() + + @pytest.fixture def type(): prophet = Prophecy(blenderbim.core.tool.Type) diff --git a/src/blenderbim/test/core/test_system.py b/src/blenderbim/test/core/test_system.py new file mode 100644 index 0000000000..02fe512cd2 --- /dev/null +++ b/src/blenderbim/test/core/test_system.py @@ -0,0 +1,90 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 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.core.system as subject +from test.core.bootstrap import ifc, system + + +class TestLoadSystems: + def test_run(self, system): + system.import_systems().should_be_called() + system.enable_system_editing_ui().should_be_called() + system.disable_editing_system().should_be_called() + subject.load_systems(system) + + +class TestDisableSystemEditingUI: + def test_run(self, system): + system.disable_editing_system().should_be_called() + system.disable_system_editing_ui().should_be_called() + subject.disable_system_editing_ui(system) + + +class TestAddSystem: + def test_run(self, ifc, system): + ifc.run("system.add_system", ifc_class="ifc_class").should_be_called() + system.import_systems().should_be_called() + subject.add_system(ifc, system, ifc_class="ifc_class") + + +class TestEditSystem: + def test_run(self, ifc, system): + system.export_system_attributes().should_be_called().will_return("attributes") + ifc.run("system.edit_system", system="system", attributes="attributes").should_be_called() + system.disable_editing_system().should_be_called() + system.import_systems().should_be_called() + subject.edit_system(ifc, system, system="system") + + +class TestRemoveSystem: + def test_run(self, ifc, system): + ifc.run("system.remove_system", system="system").should_be_called() + system.import_systems().should_be_called() + subject.remove_system(ifc, system, system="system") + + +class TestEnableEditingSystem: + def test_run(self, system): + system.import_system_attributes("system").should_be_called() + system.set_active_system("system").should_be_called() + subject.enable_editing_system(system, system="system") + + +class TestDisableEditingSystem: + def test_run(self, system): + system.disable_editing_system().should_be_called() + subject.disable_editing_system(system) + + +class TestAssignSystem: + def test_run(self, ifc): + ifc.run("system.assign_system", product="product", system="system").should_be_called() + subject.assign_system(ifc, system="system", product="product") + + +class TestUnassignSystem: + def test_run(self, ifc): + ifc.run("system.unassign_system", product="product", system="system").should_be_called() + subject.unassign_system(ifc, system="system", product="product") + + +class TestSelectSystemProducts: + def test_run(self, system): + system.select_system_products("system").should_be_called() + subject.select_system_products(system, system="system") diff --git a/src/blenderbim/test/tool/test_system.py b/src/blenderbim/test/tool/test_system.py new file mode 100644 index 0000000000..c94a32c76b --- /dev/null +++ b/src/blenderbim/test/tool/test_system.py @@ -0,0 +1,152 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 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 +import ifcopenshell +import blenderbim.core.tool +import blenderbim.tool as tool +from test.bim.bootstrap import NewFile +from blenderbim.tool.system import System as subject + + +class TestImplementsTool(NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.System) + + +class TestDisableEditingSystem(NewFile): + def test_run(self): + bpy.context.scene.BIMSystemProperties.active_system_id = 10 + subject.disable_editing_system() + assert bpy.context.scene.BIMSystemProperties.active_system_id == 0 + + +class TestDisableSystemEditingUI(NewFile): + def test_run(self): + subject.enable_system_editing_ui() + subject.disable_system_editing_ui() + assert bpy.context.scene.BIMSystemProperties.is_editing is False + + +class TestEnableSystemEditingUI(NewFile): + def test_run(self): + subject.enable_system_editing_ui() + assert bpy.context.scene.BIMSystemProperties.is_editing is True + + +class TestExportSystemAttributes(NewFile): + def test_run(self): + TestImportSystemAttributes().test_importing_a_system() + assert subject.export_system_attributes() == { + "GlobalId": "GlobalId", + "Name": "Name", + "Description": "Description", + "ObjectType": "ObjectType", + } + + +class TestImportSystemAttributes(NewFile): + def test_importing_a_system(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + system = ifc.createIfcSystem() + system.GlobalId = "GlobalId" + system.Name = "Name" + system.Description = "Description" + system.ObjectType = "ObjectType" + subject().import_system_attributes(system) + props = bpy.context.scene.BIMSystemProperties + assert props.system_attributes.get("GlobalId").string_value == "GlobalId" + assert props.system_attributes.get("Name").string_value == "Name" + assert props.system_attributes.get("Description").string_value == "Description" + assert props.system_attributes.get("ObjectType").string_value == "ObjectType" + + def test_importing_a_building_system(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + system = ifc.createIfcBuildingSystem() + system.GlobalId = "GlobalId" + system.Name = "Name" + system.Description = "Description" + system.ObjectType = "ObjectType" + system.PredefinedType = "SHADING" + system.LongName = "LongName" + subject().import_system_attributes(system) + props = bpy.context.scene.BIMSystemProperties + assert props.system_attributes.get("GlobalId").string_value == "GlobalId" + assert props.system_attributes.get("Name").string_value == "Name" + assert props.system_attributes.get("Description").string_value == "Description" + assert props.system_attributes.get("ObjectType").string_value == "ObjectType" + assert props.system_attributes.get("PredefinedType").enum_value == "SHADING" + assert props.system_attributes.get("LongName").string_value == "LongName" + + def test_importing_a_distribution_system(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + system = ifc.createIfcDistributionSystem() + system.GlobalId = "GlobalId" + system.Name = "Name" + system.Description = "Description" + system.ObjectType = "ObjectType" + system.PredefinedType = "ELECTRICAL" + system.LongName = "LongName" + subject().import_system_attributes(system) + props = bpy.context.scene.BIMSystemProperties + assert props.system_attributes.get("GlobalId").string_value == "GlobalId" + assert props.system_attributes.get("Name").string_value == "Name" + assert props.system_attributes.get("Description").string_value == "Description" + assert props.system_attributes.get("ObjectType").string_value == "ObjectType" + assert props.system_attributes.get("PredefinedType").enum_value == "ELECTRICAL" + assert props.system_attributes.get("LongName").string_value == "LongName" + + +class TestImportSystems(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + system = ifc.createIfcDistributionSystem() + zone = ifc.createIfcZone() + subject.import_systems() + props = bpy.context.scene.BIMSystemProperties + assert len(props.systems) == 1 + assert props.systems[0].ifc_definition_id == system.id() + assert props.systems[0].name == "Unnamed" + assert props.systems[0].ifc_class == "IfcDistributionSystem" + + +class TestSelectSystemProducts(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcPump") + system = ifcopenshell.api.run("system.add_system", ifc, ifc_class="IfcSystem") + ifcopenshell.api.run("system.assign_system", ifc, product=element, system=system) + obj = bpy.data.objects.new("Object", None) + bpy.context.scene.collection.objects.link(obj) + tool.Ifc.link(element, obj) + subject.select_system_products(system) + assert obj in bpy.context.selected_objects + + +class TestSetActiveSystem(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + system = ifcopenshell.api.run("system.add_system", ifc, ifc_class="IfcSystem") + subject.set_active_system(system) + assert bpy.context.scene.BIMSystemProperties.active_system_id == system.id() diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 6c550a001e..d91e13a251 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -9,8 +9,8 @@ qa: .PHONY: license license: - #copyright-header --license LGPL3 --copyright-holder "Thomas Krijnen " --copyright-year "2021" --copyright-software "IfcOpenShell" --copyright-software-description "IFC toolkit and geometry engine" -a ./ -o ./ - copyright-header --license LGPL3 --copyright-holder "Dion Moult " --copyright-year "2021" --copyright-software "IfcOpenShell" --copyright-software-description "IFC toolkit and geometry engine" -a ./ -o ./ + #copyright-header --license LGPL3 --copyright-holder "Thomas Krijnen " --copyright-year "2022" --copyright-software "IfcOpenShell" --copyright-software-description "IFC toolkit and geometry engine" -a ./ -o ./ + copyright-header --license LGPL3 --copyright-holder "Dion Moult " --copyright-year "2022" --copyright-software "IfcOpenShell" --copyright-software-description "IFC toolkit and geometry engine" -a ./ -o ./ .PHONY: coverage coverage: diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py index 397eb7cde2..41131aa8b3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py @@ -23,13 +23,13 @@ import ifcopenshell.api class Usecase: def __init__(self, file, **settings): self.file = file - self.settings = {} + self.settings = {"ifc_class": "IfcSystem"} for key, value in settings.items(): self.settings[key] = value def execute(self): return self.file.create_entity( - "IfcSystem", + self.settings["ifc_class"], **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), diff --git a/src/ifcopenshell-python/ifcopenshell/util/system.py b/src/ifcopenshell-python/ifcopenshell/util/system.py new file mode 100644 index 0000000000..e1517be6b8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/util/system.py @@ -0,0 +1,36 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + + +def get_system_elements(system): + results = [] + for rel in system.IsGroupedBy: + results.extend(rel.RelatedObjects) + return results + + +def get_element_systems(element): + results = [] + for rel in element.HasAssignments: + if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a() in [ + "IfcSystem", + "IfcDistributionSystem", + "IfcBuildingSystem", + ]: + results.append(rel.RelatingGroup) + return results diff --git a/src/ifcopenshell-python/test/api/system/test_add_system.py b/src/ifcopenshell-python/test/api/system/test_add_system.py new file mode 100644 index 0000000000..07c304cbba --- /dev/null +++ b/src/ifcopenshell-python/test/api/system/test_add_system.py @@ -0,0 +1,28 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api + + +class TestAddSystem(test.bootstrap.IFC4): + def test_adding_a_system(self): + system = ifcopenshell.api.run("system.add_system", self.file, ifc_class="IfcSystem") + system2 = ifcopenshell.api.run("system.add_system", self.file, ifc_class="IfcDistributionSystem") + assert system.is_a("IfcSystem") + assert system2.is_a("IfcDistributionSystem") diff --git a/src/ifcopenshell-python/test/api/system/test_remove_system.py b/src/ifcopenshell-python/test/api/system/test_remove_system.py new file mode 100644 index 0000000000..34d99189af --- /dev/null +++ b/src/ifcopenshell-python/test/api/system/test_remove_system.py @@ -0,0 +1,27 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api + + +class TestRemoveSystem(test.bootstrap.IFC4): + def test_removing_a_system(self): + system = ifcopenshell.api.run("system.add_system", self.file, ifc_class="IfcSystem") + ifcopenshell.api.run("system.remove_system", self.file, system=system) + assert len(self.file.by_type("IfcSystem")) == 0 diff --git a/src/ifcopenshell-python/test/util/test_system.py b/src/ifcopenshell-python/test/util/test_system.py new file mode 100644 index 0000000000..5d998cd904 --- /dev/null +++ b/src/ifcopenshell-python/test/util/test_system.py @@ -0,0 +1,47 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.system as subject + + +class TestGetSystemElements(test.bootstrap.IFC4): + def test_run(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + system = ifcopenshell.api.run("system.add_system", self.file, ifc_class="IfcSystem") + ifcopenshell.api.run("system.assign_system", self.file, product=element, system=system) + assert subject.get_system_elements(system) == [element] + + +class TestGetElementSystems(test.bootstrap.IFC4): + def test_run(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + system = ifcopenshell.api.run("system.add_system", self.file, ifc_class="IfcSystem") + ifcopenshell.api.run("system.assign_system", self.file, product=element, system=system) + assert subject.get_element_systems(element) == [system] + + def test_do_not_get_non_services_groups(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcPump") + ifcopenshell.api.run("system.assign_system", self.file, product=element, system=self.file.createIfcGroup()) + ifcopenshell.api.run("system.assign_system", self.file, product=element, system=self.file.createIfcZone()) + ifcopenshell.api.run( + "system.assign_system", self.file, product=element, system=self.file.createIfcStructuralAnalysisModel() + ) + assert not subject.get_element_systems(element)