diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index c1b31101cd..cf1c3395a1 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -44,6 +44,7 @@ modules = { "spatial": None, "void": None, "aggregate": None, + "nest": None, "geometry": None, "fm": None, "resource": None, diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py index b7b1d6d827..fd233658d9 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py @@ -20,14 +20,14 @@ import bpy from . import ui, prop, operator classes = ( - operator.BIM_OT_assign_object, - operator.BIM_OT_unassign_object, - operator.BIM_OT_enable_editing_aggregate, - operator.BIM_OT_disable_editing_aggregate, operator.BIM_OT_add_aggregate, - operator.BIM_OT_select_parts, - operator.BIM_OT_select_aggregate, operator.BIM_OT_add_part_to_object, + operator.BIM_OT_assign_object, + operator.BIM_OT_disable_editing_aggregate, + operator.BIM_OT_enable_editing_aggregate, + operator.BIM_OT_select_aggregate, + operator.BIM_OT_select_parts, + operator.BIM_OT_unassign_object, prop.BIMObjectAggregateProperties, ui.BIM_PT_aggregate, ) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/data.py b/src/blenderbim/blenderbim/bim/module/aggregate/data.py index b4e466355c..66c82d9cec 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/data.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/data.py @@ -35,7 +35,7 @@ class AggregateData: "has_relating_object": cls.has_relating_object(), "relating_object_label": cls.get_relating_object_label(), "has_related_objects": cls.has_related_objects(), - "related_objects_amount": cls.get_related_objects_amount(), + "total_parts": cls.total_parts(), "ifc_class": cls.ifc_class(), } cls.is_loaded = True @@ -59,7 +59,7 @@ class AggregateData: return ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(bpy.context.active_object)) @classmethod - def get_related_objects_amount(cls): + def total_parts(cls): parts = cls.get_related_objects() return len(parts) if parts else 0 diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index 6d5404c057..10d204dc13 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -72,8 +72,14 @@ class BIM_PT_aggregate(Panel): row.operator("bim.add_aggregate", icon="ADD", text="") row = layout.row(align=True) - parts = AggregateData.data["related_objects_amount"] - row.label(text=f"{parts or 'No'} Part{'s' if parts > 1 else ''}", icon="TRIA_DOWN") + total_parts = AggregateData.data["total_parts"] + if total_parts == 0: + row.label(text="No Parts", icon="TRIA_DOWN") + elif total_parts == 1: + row.label(text="1 Part", icon="TRIA_DOWN") + else: + row.label(text=f"{total_parts} Parts", icon="TRIA_DOWN") + if AggregateData.data["has_related_objects"]: op = row.operator("bim.select_parts", icon="RESTRICT_SELECT_OFF", text="") op.obj = context.active_object.name diff --git a/src/blenderbim/blenderbim/bim/module/nest/__init__.py b/src/blenderbim/blenderbim/bim/module/nest/__init__.py new file mode 100644 index 0000000000..f10a8e5cd6 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/nest/__init__.py @@ -0,0 +1,39 @@ +# 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.BIM_OT_assign_object, + operator.BIM_OT_disable_editing_nest, + operator.BIM_OT_enable_editing_nest, + operator.BIM_OT_select_components, + operator.BIM_OT_select_nest, + operator.BIM_OT_unassign_object, + prop.BIMObjectNestProperties, + ui.BIM_PT_nest, +) + + +def register(): + bpy.types.Object.BIMObjectNestProperties = bpy.props.PointerProperty(type=prop.BIMObjectNestProperties) + + +def unregister(): + del bpy.types.Object.BIMObjectNestProperties diff --git a/src/blenderbim/blenderbim/bim/module/nest/data.py b/src/blenderbim/blenderbim/bim/module/nest/data.py new file mode 100644 index 0000000000..9cbc655985 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/nest/data.py @@ -0,0 +1,67 @@ +# 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 +import blenderbim.tool as tool +import ifcopenshell.util.element + + +def refresh(): + NestData.is_loaded = False + + +class NestData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = { + "has_relating_object": cls.has_relating_object(), + "relating_object_label": cls.get_relating_object_label(), + "has_related_objects": cls.has_related_objects(), + "total_components": cls.total_components(), + } + cls.is_loaded = True + + @classmethod + def get_relating_object(cls): + return ifcopenshell.util.element.get_nest(tool.Ifc.get_entity(bpy.context.active_object)) + + @classmethod + def has_relating_object(cls) -> bool: + return cls.get_relating_object() is not None + + @classmethod + def get_relating_object_label(cls) -> str: + nest = cls.get_relating_object() + if nest: + return f"{nest.is_a()}/{nest.Name or ''}" + + @classmethod + def get_related_objects(cls): + return ifcopenshell.util.element.get_components(tool.Ifc.get_entity(bpy.context.active_object)) + + @classmethod + def total_components(cls): + components = cls.get_related_objects() + return len(components) if components else 0 + + @classmethod + def has_related_objects(cls) -> bool: + return bool(cls.get_related_objects()) diff --git a/src/blenderbim/blenderbim/bim/module/nest/operator.py b/src/blenderbim/blenderbim/bim/module/nest/operator.py new file mode 100644 index 0000000000..a500d0262c --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/nest/operator.py @@ -0,0 +1,151 @@ +# 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 +import ifcopenshell +import ifcopenshell.util.element +import blenderbim.tool as tool +import blenderbim.core.nest as core +import blenderbim.core.spatial +import blenderbim.bim.handler +from blenderbim.bim.ifc import IfcStore + + +class Operator: + def execute(self, context): + IfcStore.execute_ifc_operator(self, context) + blenderbim.bim.handler.refresh_ui_data() + return {"FINISHED"} + + +class BIM_OT_assign_object(bpy.types.Operator, Operator): + """Create nest relationship between two ifc elements""" + + bl_idname = "bim.assign_object" + bl_label = "Assign Object" + bl_options = {"REGISTER", "UNDO"} + relating_object: bpy.props.IntProperty() + + def _execute(self, context): + relating_obj = None + if self.relating_object: + relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object)) + elif context.active_object: + relating_obj = context.active_object + if not relating_obj: + return + + for obj in bpy.context.selected_objects: + if obj == relating_obj: + continue + element = tool.Ifc.get_entity(obj) + if not element: + continue + result = core.assign_object( + tool.Ifc, + tool.Nest, + tool.Collector, + relating_obj=relating_obj, + related_obj=obj, + ) + if not result: + self.report({"ERROR"}, f" Cannot nest {obj.name} to {relating_obj.name}") + + +class BIM_OT_unassign_object(bpy.types.Operator, Operator): + """Remove nest relationship between two ifc elements""" + + bl_idname = "bim.unassign_object" + bl_label = "Unassign Object" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + for obj in bpy.context.selected_objects: + element = tool.Ifc.get_entity(obj) + if not element: + continue + nest = ifcopenshell.util.element.get_nest(element) + if not nest: + continue + core.unassign_object( + tool.Ifc, + tool.Nest, + tool.Collector, + relating_obj=tool.Ifc.get_object(nest), + related_obj=tool.Ifc.get_object(element), + ) + + +class BIM_OT_enable_editing_nest(bpy.types.Operator, Operator): + """Enable editing nest relationship""" + + bl_idname = "bim.enable_editing_nest" + bl_label = "Enable Editing Nest" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.enable_editing_nest(tool.Nest, obj=context.active_object) + + +class BIM_OT_disable_editing_nest(bpy.types.Operator, Operator): + """Disable editing nest relationship""" + + bl_idname = "bim.disable_editing_nest" + bl_label = "Disable Editing Nest" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.disable_editing_nest(tool.Nest, obj=context.active_object) + + +class BIM_OT_select_components(bpy.types.Operator): + """Select Components""" + + bl_idname = "bim.select_components" + bl_label = "Select Components" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + obj = bpy.data.objects.get(self.obj) or context.active_object + components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(obj)) + component_objs = set(tool.Ifc.get_object(c) for c in components) + selectable_component_objs = set(context.selectable_objects).intersection(component_objs) + for selectable_component_obj in selectable_component_objs: + selectable_component_obj.select_set(True) + return {"FINISHED"} + + +class BIM_OT_select_nest(bpy.types.Operator): + """Select Nest""" + + bl_idname = "bim.select_nest" + bl_label = "Select Nest" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + obj = bpy.data.objects.get(self.obj) or context.active_object + nest = ifcopenshell.util.element.get_nest(tool.Ifc.get_entity(obj)) + nest_obj = tool.Ifc.get_object(nest) + if nest_obj in context.selectable_objects: + nest_obj.select_set(True) + bpy.context.view_layer.objects.active = nest_obj + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/nest/prop.py b/src/blenderbim/blenderbim/bim/module/nest/prop.py new file mode 100644 index 0000000000..043258a386 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/nest/prop.py @@ -0,0 +1,48 @@ +# 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 blenderbim.bim.prop import StrProperty, Attribute +from blenderbim.bim.module.spatial.data import SpatialData +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +def update_relating_object(self, context): + def message(self, context): + self.layout.label(text="Please select a valid Ifc Element") + + if self.relating_object is None: + return + if not self.relating_object.BIMObjectProperties.ifc_definition_id: + context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO") + self.relating_object = None + + +class BIMObjectNestProperties(PropertyGroup): + is_editing: BoolProperty(name="Is Editing") + relating_object: PointerProperty(name="Nest Host", type=bpy.types.Object, update=update_relating_object) diff --git a/src/blenderbim/blenderbim/bim/module/nest/ui.py b/src/blenderbim/blenderbim/bim/module/nest/ui.py new file mode 100644 index 0000000000..25f5ed7edd --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/nest/ui.py @@ -0,0 +1,83 @@ +# 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 . + +from bpy.types import Panel +from blenderbim.bim.module.nest.data import NestData +from blenderbim.bim.ifc import IfcStore + + +class BIM_PT_nest(Panel): + bl_label = "Nest" + bl_idname = "BIM_PT_nest" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_parent_id = "BIM_PT_tab_object_metadata" + + @classmethod + def poll(cls, context): + if not context.active_object: + return False + props = context.active_object.BIMObjectProperties + if not props.ifc_definition_id: + return False + if not IfcStore.get_element(props.ifc_definition_id): + return False + if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): + return False + return True + + def draw(self, context): + layout = self.layout + if not NestData.is_loaded: + NestData.load() + + props = context.active_object.BIMObjectNestProperties + + if props.is_editing: + row = layout.row(align=True) + row.prop(props, "relating_object", text="") + if props.relating_object: + op = row.operator("bim.assign_object", icon="CHECKMARK", text="") + op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id + row.operator("bim.disable_editing_nest", icon="CANCEL", text="") + else: + row = layout.row(align=True) + if NestData.data["has_relating_object"]: + row.label(text=NestData.data["relating_object_label"], icon="TRIA_UP") + op = row.operator("bim.select_nest", icon="RESTRICT_SELECT_OFF", text="") + op.obj = context.active_object.name + row.operator("bim.enable_editing_nest", icon="GREASEPENCIL", text="") + op = row.operator("bim.unassign_object", icon="X", text="") + else: + row.label(text="No Host", icon="TRIA_UP") + row.operator("bim.enable_editing_nest", icon="GREASEPENCIL", text="") + + row = layout.row(align=True) + total_components = NestData.data["total_components"] + if total_components == 0: + row.label(text="No Components", icon="TRIA_DOWN") + elif total_components == 1: + row.label(text="1 Component", icon="TRIA_DOWN") + else: + row.label(text=f"{total_components} Components", icon="TRIA_DOWN") + + if NestData.data["has_related_objects"]: + op = row.operator("bim.select_components", icon="RESTRICT_SELECT_OFF", text="") + op.obj = context.active_object.name diff --git a/src/blenderbim/blenderbim/core/nest.py b/src/blenderbim/blenderbim/core/nest.py new file mode 100644 index 0000000000..087a1aa5f6 --- /dev/null +++ b/src/blenderbim/blenderbim/core/nest.py @@ -0,0 +1,58 @@ +# 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 . + + +def enable_editing_nest(nest, obj=None): + nest.enable_editing(obj) + + +def disable_editing_nest(nest, obj=None): + nest.disable_editing(obj) + + +def assign_object(ifc, nest, collector, relating_obj=None, related_obj=None): + if not nest.can_nest(relating_obj, related_obj): + return + rel = ifc.run( + "nest.assign_object", related_object=ifc.get_entity(related_obj), relating_object=ifc.get_entity(relating_obj) + ) + collector.assign(relating_obj) + collector.assign(related_obj) + nest.disable_editing(related_obj) + return rel + + +def unassign_object(ifc, nest, collector, relating_obj=None, related_obj=None): + related_element = ifc.get_entity(related_obj) + container = nest.get_container(related_element) + if not relating_obj: + relating_element = nest.get_relating_object(related_element) + if related_element: + relating_obj = ifc.get_object(relating_element) + if relating_obj: + ifc.run("nest.unassign_object", related_object=related_element) + if container: + ifc.run("spatial.assign_container", product=related_element, relating_structure=container) + collector.assign(relating_obj) + collector.assign(related_obj) + + +def add_part_to_object(ifc, nest, collector, blender, obj, part_class, part_name=None): + part_obj = blender.create_ifc_object(ifc_class=part_class, name=part_name) + assign_object(ifc, nest, collector, relating_obj=obj, related_obj=part_obj) + blender.set_active_object(obj) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 6384c16c93..0a12e0b2ca 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -507,6 +507,14 @@ class Model: def replace_object_ifc_representation(cls, ifc_file, ifc_context, obj, new_representation): pass +@interface +class Nest: + def can_nest(cls, relating_object, related_object): pass + def disable_editing(cls, obj): pass + def enable_editing(cls, obj): pass + def get_container(cls, element): pass + + @interface class Patch: def run_migrate_patch(cls, infile, outfile, schema): pass diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index 5b63b278d7..3cc7507457 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -37,6 +37,7 @@ from blenderbim.tool.loader import Loader from blenderbim.tool.material import Material from blenderbim.tool.misc import Misc from blenderbim.tool.model import Model +from blenderbim.tool.nest import Nest from blenderbim.tool.owner import Owner from blenderbim.tool.patch import Patch from blenderbim.tool.project import Project diff --git a/src/blenderbim/blenderbim/tool/collector.py b/src/blenderbim/blenderbim/tool/collector.py index 4d9a4b2705..9857f19651 100644 --- a/src/blenderbim/blenderbim/tool/collector.py +++ b/src/blenderbim/blenderbim/tool/collector.py @@ -107,11 +107,12 @@ class Collector(blenderbim.core.tool.Collector): collection.children.unlink(object_collection) collection_collection.children.link(object_collection) + # If an aggregate or nested host loses all its children, it no longer needs its own collection + if obj.BIMObjectProperties.collection and obj.BIMObjectProperties.collection != object_collection: + bpy.data.collections.remove(obj.BIMObjectProperties.collection) + @classmethod def _get_own_collection(cls, element, obj): - if obj.BIMObjectProperties.collection: - return obj.BIMObjectProperties.collection - if element.is_a("IfcProject"): return cls._create_own_collection(obj) @@ -155,6 +156,9 @@ class Collector(blenderbim.core.tool.Collector): if getattr(element, "IsDecomposedBy", None): return cls._create_own_collection(obj) + if getattr(element, "IsNestedBy", None): + return cls._create_own_collection(obj) + @classmethod def _get_collection(cls, element, obj): if element.is_a("IfcTypeObject"): @@ -235,6 +239,8 @@ class Collector(blenderbim.core.tool.Collector): @classmethod def _create_own_collection(cls, obj): + if obj.BIMObjectProperties.collection: + return obj.BIMObjectProperties.collection collection = bpy.data.collections.new(obj.name) obj.BIMObjectProperties.collection = collection collection.BIMCollectionProperties.obj = obj diff --git a/src/blenderbim/blenderbim/tool/nest.py b/src/blenderbim/blenderbim/tool/nest.py new file mode 100644 index 0000000000..cf2912902e --- /dev/null +++ b/src/blenderbim/blenderbim/tool/nest.py @@ -0,0 +1,49 @@ +# 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.core.tool +import blenderbim.tool as tool +import ifcopenshell.util.element + + +class Nest(blenderbim.core.tool.Nest): + @classmethod + def can_nest(cls, relating_obj, related_obj): + relating_object = tool.Ifc.get_entity(relating_obj) + related_object = tool.Ifc.get_entity(related_obj) + if not relating_object or not related_object: + return False + if relating_object.is_a("IfcElement") and related_object.is_a("IfcElement"): + return True + return False + + @classmethod + def disable_editing(cls, obj): + obj.BIMObjectNestProperties.is_editing = False + + @classmethod + def enable_editing(cls, obj): + obj.BIMObjectNestProperties.is_editing = True + + @classmethod + def get_container(cls, element): + return ifcopenshell.util.element.get_container(element) + + @classmethod + def get_relating_object(cls, related_element): + return ifcopenshell.util.element.get_nest(related_element) diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py index 2f71c5bd37..3b48e66c80 100644 --- a/src/blenderbim/test/core/bootstrap.py +++ b/src/blenderbim/test/core/bootstrap.py @@ -126,6 +126,13 @@ def misc(): prophet.verify() +@pytest.fixture +def nest(): + prophet = Prophecy(blenderbim.core.tool.Nest) + yield prophet + prophet.verify() + + @pytest.fixture def owner(): prophet = Prophecy(blenderbim.core.tool.Owner) diff --git a/src/blenderbim/test/core/test_nest.py b/src/blenderbim/test/core/test_nest.py new file mode 100644 index 0000000000..56f73cacbb --- /dev/null +++ b/src/blenderbim/test/core/test_nest.py @@ -0,0 +1,59 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 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.nest as subject +from test.core.bootstrap import ifc, nest, collector + + +class TestEnableEditingNeset: + def test_run(self, nest): + nest.enable_editing("obj").should_be_called() + subject.enable_editing_nest(nest, obj="obj") + + +class TestDisableEditingNest: + def test_run(self, nest): + nest.disable_editing("obj").should_be_called() + subject.disable_editing_nest(nest, obj="obj") + + +class TestAssignObject: + def test_run(self, ifc, nest, collector): + nest.can_nest("relating_obj", "related_obj").should_be_called().will_return(True) + ifc.get_entity("relating_obj").should_be_called().will_return("relating_object") + ifc.get_entity("related_obj").should_be_called().will_return("related_object") + ifc.run( + "nest.assign_object", related_object="related_object", relating_object="relating_object" + ).should_be_called().will_return("rel") + nest.disable_editing("related_obj").should_be_called() + collector.assign("relating_obj").should_be_called() + collector.assign("related_obj").should_be_called() + assert ( + subject.assign_object(ifc, nest, collector, relating_obj="relating_obj", related_obj="related_obj") == "rel" + ) + + +class TestUnassignObject: + def test_run(self, ifc, nest, collector): + ifc.get_entity("related_obj").should_be_called().will_return("element") + nest.get_container("element").should_be_called().will_return("container") + ifc.run("spatial.assign_container", product="element", relating_structure="container").should_be_called() + ifc.run("nest.unassign_object", related_object="element").should_be_called().will_return("rel") + collector.assign("relating_obj").should_be_called() + collector.assign("related_obj").should_be_called() + subject.unassign_object(ifc, nest, collector, relating_obj="relating_obj", related_obj="related_obj") diff --git a/src/blenderbim/test/tool/test_aggregate.py b/src/blenderbim/test/tool/test_aggregate.py index cb01270711..21a1b7002c 100644 --- a/src/blenderbim/test/tool/test_aggregate.py +++ b/src/blenderbim/test/tool/test_aggregate.py @@ -29,10 +29,6 @@ class TestImplementsTool(NewFile): assert isinstance(subject(), blenderbim.core.tool.Aggregate) - - - - class TestCanAggregate(NewFile): def test_elements_can_aggregate(self): ifc = ifcopenshell.file() @@ -107,6 +103,7 @@ class TestCanAggregate(NewFile): tool.Ifc.link(subelement, subelement_obj) assert subject.can_aggregate(element_obj, subelement_obj) is False + class TestDisableEditing(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) diff --git a/src/blenderbim/test/tool/test_nest.py b/src/blenderbim/test/tool/test_nest.py new file mode 100644 index 0000000000..fcf244b59a --- /dev/null +++ b/src/blenderbim/test/tool/test_nest.py @@ -0,0 +1,74 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 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.nest import Nest as subject + + +class TestImplementsTool(NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.Nest) + + +class TestCanNest(NewFile): + def test_elements_can_nest(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + element = ifc.createIfcElementAssembly() + element_obj = bpy.data.objects.new("Object", None) + tool.Ifc.link(element, element_obj) + subelement = ifc.createIfcBeam() + subelement_obj = bpy.data.objects.new("Object", None) + tool.Ifc.link(subelement, subelement_obj) + assert subject.can_nest(element_obj, subelement_obj) is True + + def test_unlinked_elements_cannot_nest(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + element_obj = bpy.data.objects.new("Object", None) + subelement_obj = bpy.data.objects.new("Object", None) + assert subject.can_nest(element_obj, subelement_obj) is False + + +class TestDisableEditing(NewFile): + def test_run(self): + obj = bpy.data.objects.new("Object", None) + subject.enable_editing(obj) + subject.disable_editing(obj) + assert obj.BIMObjectNestProperties.is_editing is False + + +class TestEnableEditing(NewFile): + def test_run(self): + obj = bpy.data.objects.new("Object", None) + subject.enable_editing(obj) + assert obj.BIMObjectNestProperties.is_editing is True + + +class TestGetContainer(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + element = ifc.createIfcWall() + container = ifc.createIfcBuildingStorey() + ifcopenshell.api.run("spatial.assign_container", ifc, product=element, relating_structure=container) + assert subject.get_container(element) == container diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index ca9ee2665d..d3547b8cfe 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -805,7 +805,8 @@ def get_aggregate(element): aggregate = ifcopenshell.util.element.get_aggregate(element) """ if hasattr(element, "Decomposes") and element.Decomposes: - return element.Decomposes[0].RelatingObject + if element.Decomposes[0].is_a("IfcRelAggregates"): # IFC2X3 + return element.Decomposes[0].RelatingObject def get_nest(element): @@ -821,13 +822,17 @@ def get_nest(element): element = file.by_type("IfcBeam")[0] aggregate = ifcopenshell.util.element.get_nest(element) """ - if hasattr(element, "Nests") and element.Nests: - return element.Nests[0].RelatingObject + if hasattr(element, "Nests"): + if element.Nests: + return element.Nests[0].RelatingObject + elif hasattr(element, "Decomposes") and element.Decomposes: # IFC2X3 + if element.Decomposes[0].is_a("IfcRelNests"): + return element.Decomposes[0].RelatingObject def get_parts(element): """ - Retrieves the parts of an element. + Retrieves the parts of an element that have an aggregation relationship. :param element: The IFC element :return: The parts of the element @@ -840,7 +845,30 @@ def get_parts(element): """ if hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy: - return element.IsDecomposedBy[0].RelatedObjects + if element.IsDecomposedBy[0].is_a("IfcRelAggregates"): + return element.IsDecomposedBy[0].RelatedObjects + + +def get_components(element): + """ + Retrieves the components of an element that have an nest relationship. + + :param element: The IFC element + :return: The components of the element + + Example: + + .. code:: python + element = file.by_type("IfcElementAssembly")[0] + components = ifcopenshell.util.element.get_components(element) + + """ + if hasattr(element, "IsNestedBy"): + if element.IsNestedBy: + return element.IsNestedBy[0].RelatedObjects + elif hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy: + if element.IsDecomposedBy[0].is_a("IfcRelNests"): + return element.IsDecomposedBy[0].RelatedObjects def replace_attribute(element, old, new):