diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py
index bd7f6946ea..6c6dd30226 100644
--- a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py
@@ -17,20 +17,21 @@
# along with BlenderBIM Add-on. If not, see .
import bpy
-from . import ui, operator
+from . import ui, prop, operator
classes = (
operator.AssignObject,
operator.EnableEditingAggregate,
operator.DisableEditingAggregate,
operator.AddAggregate,
+ prop.BIMObjectAggregateProperties,
ui.BIM_PT_aggregate,
)
def register():
- pass
+ bpy.types.Object.BIMObjectAggregateProperties = bpy.props.PointerProperty(type=prop.BIMObjectAggregateProperties)
def unregister():
- pass
+ del bpy.types.Object.BIMObjectAggregateProperties
diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/data.py b/src/blenderbim/blenderbim/bim/module/aggregate/data.py
new file mode 100644
index 0000000000..4ae27e5ae4
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/aggregate/data.py
@@ -0,0 +1,27 @@
+import bpy
+import blenderbim.tool as tool
+import ifcopenshell.util.element
+
+
+def refresh():
+ AggregateData.is_loaded = False
+
+
+class AggregateData:
+ data = {}
+ is_loaded = False
+
+ @classmethod
+ def load(cls):
+ cls.data = {"has_aggregate": cls.has_aggregate(), "label": cls.get_label()}
+ cls.is_loaded = True
+
+ @classmethod
+ def has_aggregate(cls):
+ return ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object))
+
+ @classmethod
+ def get_label(cls):
+ aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(bpy.context.active_object))
+ if aggregate:
+ return f"{aggregate.is_a()}/{aggregate.Name or ''}"
diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py
index b9b830df3b..9a43886430 100644
--- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py
@@ -17,89 +17,52 @@
# along with BlenderBIM Add-on. If not, see .
import bpy
-import ifcopenshell.api
+import blenderbim.tool as tool
+import blenderbim.core.aggregate as core
+import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
-from ifcopenshell.api.aggregate.data import Data
-class AssignObject(bpy.types.Operator):
+class Operator:
+ def execute(self, context):
+ IfcStore.execute_ifc_operator(self, context)
+ blenderbim.bim.handler.refresh_ui_data()
+ return {"FINISHED"}
+
+
+class AssignObject(bpy.types.Operator, Operator):
bl_idname = "bim.assign_object"
bl_label = "Assign Object"
bl_options = {"REGISTER", "UNDO"}
- relating_object: bpy.props.StringProperty()
- related_object: bpy.props.StringProperty()
-
- def execute(self, context):
- return IfcStore.execute_ifc_operator(self, context)
+ relating_object: bpy.props.IntProperty()
+ related_object: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
- related_objects = (
- [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
+ core.assign_object(
+ tool.Ifc,
+ tool.Aggregator,
+ tool.Collector,
+ relating_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object)),
+ related_obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.related_object)),
)
- relating_object = bpy.data.objects.get(self.relating_object)
- if not relating_object or not relating_object.BIMObjectProperties.ifc_definition_id:
- return {"FINISHED"}
- for related_object in related_objects:
- oprops = related_object.BIMObjectProperties
- product = self.file.by_id(oprops.ifc_definition_id)
- ifcopenshell.api.run(
- "aggregate.assign_object",
- self.file,
- **{
- "product": product,
- "relating_object": self.file.by_id(relating_object.BIMObjectProperties.ifc_definition_id),
- },
- )
- bpy.ops.bim.edit_object_placement(obj=related_object.name)
- Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
- bpy.ops.bim.disable_editing_aggregate(obj=related_object.name)
-
- spatial_collection = bpy.data.collections.get(related_object.name)
- relating_collection = bpy.data.collections.get(relating_object.name)
- if spatial_collection:
- self.remove_collection(context.scene.collection, spatial_collection)
- for collection in bpy.data.collections:
- if collection == relating_collection:
- if not collection.children.get(spatial_collection.name):
- collection.children.link(spatial_collection)
- continue
- self.remove_collection(collection, spatial_collection)
- else:
- for collection in related_object.users_collection:
- if collection.name.startswith("Ifc"):
- collection.objects.unlink(related_object)
- relating_collection.objects.link(related_object)
- return {"FINISHED"}
-
- def remove_collection(self, parent, child):
- try:
- parent.children.unlink(child)
- except:
- pass
-class EnableEditingAggregate(bpy.types.Operator):
+class EnableEditingAggregate(bpy.types.Operator, Operator):
bl_idname = "bim.enable_editing_aggregate"
bl_label = "Enable Editing Aggregate"
bl_options = {"REGISTER", "UNDO"}
- def execute(self, context):
- context.active_object.BIMObjectProperties.relating_object = None
- context.active_object.BIMObjectProperties.is_editing_aggregate = True
- return {"FINISHED"}
+ def _execute(self, context):
+ core.enable_editing_aggregate(tool.Aggregator, obj=context.active_object)
-class DisableEditingAggregate(bpy.types.Operator):
+class DisableEditingAggregate(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_aggregate"
bl_label = "Disable Editing Aggregate"
- obj: bpy.props.StringProperty()
bl_options = {"REGISTER", "UNDO"}
- def execute(self, context):
- obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
- obj.BIMObjectProperties.is_editing_aggregate = False
- return {"FINISHED"}
+ def _execute(self, context):
+ core.disable_editing_aggregate(tool.Aggregator, obj=context.active_object)
class AddAggregate(bpy.types.Operator):
diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/prop.py b/src/blenderbim/blenderbim/bim/module/aggregate/prop.py
new file mode 100644
index 0000000000..8ecb77aeec
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/aggregate/prop.py
@@ -0,0 +1,19 @@
+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,
+)
+
+
+class BIMObjectAggregateProperties(PropertyGroup):
+ is_editing: BoolProperty(name="Is Editing")
+ relating_object: PointerProperty(name="Aggregate", type=bpy.types.Object)
diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py
index eaa201c8cf..e107784bcb 100644
--- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see .
from bpy.types import Panel
-from ifcopenshell.api.aggregate.data import Data
+from blenderbim.bim.module.aggregate.data import AggregateData
from blenderbim.bim.ifc import IfcStore
@@ -40,34 +40,27 @@ class BIM_PT_aggregate(Panel):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
- if props.ifc_definition_id not in Data.products:
- Data.load(IfcStore.get_file(), props.ifc_definition_id)
- if not Data.products[props.ifc_definition_id]:
- return False
return True
def draw(self, context):
- props = context.active_object.BIMObjectProperties
- if not props.ifc_definition_id:
- return
- if props.ifc_definition_id not in Data.products:
- Data.load(IfcStore.get_file(), props.ifc_definition_id)
+ if not AggregateData.is_loaded:
+ AggregateData.load()
- if props.is_editing_aggregate:
+ props = context.active_object.BIMObjectAggregateProperties
+
+ if props.is_editing:
row = self.layout.row(align=True)
row.prop(props, "relating_object", text="")
if props.relating_object:
- row.operator(
- "bim.assign_object", icon="CHECKMARK", text=""
- ).relating_object = props.relating_object.name
+ op = row.operator("bim.assign_object", icon="CHECKMARK", text="")
+ op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
+ op.related_object = context.active_object.BIMObjectProperties.ifc_definition_id
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
else:
row = self.layout.row(align=True)
- name = "{}/{}".format(
- Data.products[props.ifc_definition_id]["type"], Data.products[props.ifc_definition_id]["Name"]
- )
- if name == "None/None":
- name = "No Aggregate Found"
- row.label(text=name)
+ if AggregateData.data["has_aggregate"]:
+ row.label(text=AggregateData.data["label"])
+ else:
+ row.label(text="No Aggregate Found")
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py
index ac13623bc6..b2b5decfc2 100644
--- a/src/blenderbim/blenderbim/bim/module/project/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/project/operator.py
@@ -94,9 +94,16 @@ class CreateProject(bpy.types.Operator):
bpy.ops.bim.assign_class(obj=site.name, ifc_class="IfcSite")
bpy.ops.bim.assign_class(obj=building.name, ifc_class="IfcBuilding")
bpy.ops.bim.assign_class(obj=building_storey.name, ifc_class="IfcBuildingStorey")
- bpy.ops.bim.assign_object(related_object=site.name, relating_object=project.name)
- bpy.ops.bim.assign_object(related_object=building.name, relating_object=site.name)
- bpy.ops.bim.assign_object(related_object=building_storey.name, relating_object=building.name)
+
+ blenderbim.core.aggregate.assign_object(
+ tool.Ifc, tool.Aggregator, tool.Collector, relating_obj=project, related_obj=site
+ )
+ blenderbim.core.aggregate.assign_object(
+ tool.Ifc, tool.Aggregator, tool.Collector, relating_obj=site, related_obj=building
+ )
+ blenderbim.core.aggregate.assign_object(
+ tool.Ifc, tool.Aggregator, tool.Collector, relating_obj=building, related_obj=building_storey
+ )
context.view_layer.objects.active = active_object
return {"FINISHED"}
diff --git a/src/blenderbim/blenderbim/bim/module/spatial/data.py b/src/blenderbim/blenderbim/bim/module/spatial/data.py
index b9e099ce92..4eac3aefc0 100644
--- a/src/blenderbim/blenderbim/bim/module/spatial/data.py
+++ b/src/blenderbim/blenderbim/bim/module/spatial/data.py
@@ -32,10 +32,7 @@ class SpatialData:
@classmethod
def load(cls):
cls.data = {
- "is_editing": bpy.context.active_object.BIMObjectSpatialProperties.is_editing,
"parent_container_id": cls.get_parent_container_id(),
- "container_id": cls.get_container_id(),
- "container_list_settings": cls.get_container_list_settings(),
"is_contained": cls.is_contained(),
"is_directly_contained": cls.is_directly_contained(),
"label": cls.get_label(),
@@ -52,19 +49,6 @@ class SpatialData:
return
return container.Decomposes[0].RelatingObject.id()
- @classmethod
- def get_container_id(cls):
- props = bpy.context.scene.BIMSpatialProperties
- try:
- return props.containers[props.active_container_index].ifc_definition_id
- except:
- return 0
-
- @classmethod
- def get_container_list_settings(cls):
- props = bpy.context.scene.BIMSpatialProperties
- return ["", props, "containers", props, "active_container_id"]
-
@classmethod
def is_contained(cls):
return ifcopenshell.util.element.get_container(tool.Ifc.get_entity(bpy.context.active_object))
diff --git a/src/blenderbim/blenderbim/bim/module/spatial/prop.py b/src/blenderbim/blenderbim/bim/module/spatial/prop.py
index 4e15dff8ab..6dbecb6120 100644
--- a/src/blenderbim/blenderbim/bim/module/spatial/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/spatial/prop.py
@@ -32,10 +32,6 @@ from bpy.props import (
)
-def update_active_container_index(self, context):
- SpatialData.is_loaded = False
-
-
class SpatialElement(PropertyGroup):
name: StringProperty(name="Name")
long_name: StringProperty(name="Long Name")
@@ -46,7 +42,7 @@ class SpatialElement(PropertyGroup):
class BIMSpatialProperties(PropertyGroup):
containers: CollectionProperty(name="Containers", type=SpatialElement)
- active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index)
+ active_container_index: IntProperty(name="Active Container Index")
active_container_id: IntProperty(name="Active Container Id")
diff --git a/src/blenderbim/blenderbim/bim/module/spatial/ui.py b/src/blenderbim/blenderbim/bim/module/spatial/ui.py
index 9304528af6..94aea9bab3 100644
--- a/src/blenderbim/blenderbim/bim/module/spatial/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/spatial/ui.py
@@ -18,7 +18,6 @@
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
-from ifcopenshell.api.spatial.data import Data
from blenderbim.bim.module.spatial.data import SpatialData
@@ -38,26 +37,26 @@ class BIM_PT_spatial(Panel):
return False
if not IfcStore.get_element(oprops.ifc_definition_id):
return False
- if oprops.ifc_definition_id not in Data.products:
- Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
- if not Data.products[oprops.ifc_definition_id]:
- return False
return True
def draw(self, context):
if not SpatialData.is_loaded:
SpatialData.load()
- if SpatialData.data["is_editing"]:
+ props = context.scene.BIMSpatialProperties
+ osprops = context.active_object.BIMObjectSpatialProperties
+
+ if osprops.is_editing:
row = self.layout.row(align=True)
if SpatialData.data["parent_container_id"]:
op = row.operator("bim.change_spatial_level", text="", icon="FRAME_PREV")
op.parent = SpatialData.data["parent_container_id"]
- row.operator("bim.assign_container", icon="CHECKMARK").structure = SpatialData.data["container_id"]
+ op = row.operator("bim.assign_container", icon="CHECKMARK")
+ op.structure = props.containers[props.active_container_index].ifc_definition_id
row.operator("bim.copy_to_container", icon="COPYDOWN", text="")
row.operator("bim.disable_editing_container", icon="CANCEL", text="")
- self.layout.template_list("BIM_UL_containers", *SpatialData.data["container_list_settings"])
+ self.layout.template_list("BIM_UL_containers", "", props, "containers", props, "active_container_id")
else:
row = self.layout.row(align=True)
if SpatialData.data["is_contained"]:
diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py
index fd6ee6ec99..fd65f955c8 100644
--- a/src/blenderbim/blenderbim/bim/prop.py
+++ b/src/blenderbim/blenderbim/bim/prop.py
@@ -267,8 +267,6 @@ class BIMObjectProperties(PropertyGroup):
)
is_reassigning_class: BoolProperty(name="Is Reassigning Class")
global_ids: CollectionProperty(name="GlobalIds", type=GlobalId)
- relating_object: PointerProperty(name="Aggregate", type=bpy.types.Object)
- is_editing_aggregate: BoolProperty(name="Is Editing Aggregate")
psets: CollectionProperty(name="Psets", type=PsetQto)
qtos: CollectionProperty(name="Qtos", type=PsetQto)
diff --git a/src/blenderbim/blenderbim/core/aggregate.py b/src/blenderbim/blenderbim/core/aggregate.py
new file mode 100644
index 0000000000..3f1e817db1
--- /dev/null
+++ b/src/blenderbim/blenderbim/core/aggregate.py
@@ -0,0 +1,18 @@
+def enable_editing_aggregate(aggregator, obj=None):
+ aggregator.enable_editing(obj)
+
+
+def disable_editing_aggregate(aggregator, obj=None):
+ aggregator.disable_editing(obj)
+
+
+def assign_object(ifc, aggregator, collector, relating_obj=None, related_obj=None):
+ if not aggregator.can_aggregate(relating_obj, related_obj):
+ return
+ rel = ifc.run(
+ "aggregate.assign_object", product=ifc.get_entity(related_obj), relating_object=ifc.get_entity(relating_obj)
+ )
+ collector.assign(relating_obj)
+ collector.assign(related_obj)
+ aggregator.disable_editing(related_obj)
+ return rel
diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py
index 9c5182e648..a4ce6931c8 100644
--- a/src/blenderbim/blenderbim/core/tool.py
+++ b/src/blenderbim/blenderbim/core/tool.py
@@ -56,6 +56,23 @@ class AddressEditor(abc.ABC):
pass
+class Aggregator(abc.ABC):
+ @classmethod
+ @abc.abstractmethod
+ def enable_editing(cls, obj):
+ pass
+
+ @classmethod
+ @abc.abstractmethod
+ def disable_editing(cls, obj):
+ pass
+
+ @classmethod
+ @abc.abstractmethod
+ def can_aggregate(cls, relating_object, related_object):
+ pass
+
+
class Blender(abc.ABC):
pass
diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py
index 9d04e812d1..2b081b3416 100644
--- a/src/blenderbim/blenderbim/tool/__init__.py
+++ b/src/blenderbim/blenderbim/tool/__init__.py
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see .
from blenderbim.tool.address_editor import AddressEditor
+from blenderbim.tool.aggregator import Aggregator
from blenderbim.tool.blender import Blender
from blenderbim.tool.collector import Collector
from blenderbim.tool.container import Container
diff --git a/src/blenderbim/blenderbim/tool/aggregator.py b/src/blenderbim/blenderbim/tool/aggregator.py
new file mode 100644
index 0000000000..a114b33adb
--- /dev/null
+++ b/src/blenderbim/blenderbim/tool/aggregator.py
@@ -0,0 +1,34 @@
+import blenderbim.core.tool
+import blenderbim.tool as tool
+
+
+class Aggregator(blenderbim.core.tool.Aggregator):
+ @classmethod
+ def enable_editing(cls, obj):
+ obj.BIMObjectAggregateProperties.is_editing = True
+
+ @classmethod
+ def disable_editing(cls, obj):
+ obj.BIMObjectAggregateProperties.is_editing = False
+
+ @classmethod
+ def can_aggregate(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"):
+ if relating_obj.data:
+ return False
+ return True
+ if tool.Ifc.get_schema() == "IFC2X3":
+ if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
+ return True
+ if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
+ return True
+ else:
+ if relating_object.is_a("IfcSpatialElement") and related_object.is_a("IfcSpatialElement"):
+ return True
+ if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
+ return True
+ return False
diff --git a/src/blenderbim/blenderbim/tool/collector.py b/src/blenderbim/blenderbim/tool/collector.py
index cdbf6fdfd8..48cf7120c9 100644
--- a/src/blenderbim/blenderbim/tool/collector.py
+++ b/src/blenderbim/blenderbim/tool/collector.py
@@ -26,29 +26,76 @@ import blenderbim.tool as tool
class Collector(blenderbim.core.tool.Collector):
@classmethod
def assign(cls, obj):
- for collection in obj.users_collection:
- collection.objects.unlink(obj)
-
element = tool.Ifc.get_entity(obj)
+
+ object_collection = None
+ collection_collection = None
+
+ object_collection = cls._get_own_collection(element, obj)
+ if object_collection:
+ collection_collection = cls._get_collection(element, obj)
+ else:
+ object_collection = cls._get_collection(element, obj)
+
+ if obj.users_collection != (object_collection,):
+ for collection in obj.users_collection:
+ collection.objects.unlink(obj)
+ object_collection.objects.link(obj)
+
+ if collection_collection and collection_collection.children.find(object_collection.name) == -1:
+ for collection in bpy.data.collections:
+ if collection.children.find(object_collection.name) != -1:
+ collection.children.unlink(object_collection)
+ collection_collection.children.link(object_collection)
+
+ @classmethod
+ def _get_own_collection(cls, element, obj):
+ if element.is_a("IfcProject"):
+ collection = bpy.data.collections.get(obj.name)
+ if not collection:
+ collection = bpy.data.collections.new(obj.name)
+ return collection
+
+ if tool.Ifc.get_schema() == "IFC2X3":
+ if element.is_a("IfcSpatialStructureElement"):
+ collection = bpy.data.collections.get(obj.name)
+ if not collection:
+ collection = bpy.data.collections.new(obj.name)
+ return collection
+ else:
+ if element.is_a("IfcSpatialElement"):
+ collection = bpy.data.collections.get(obj.name)
+ if not collection:
+ collection = bpy.data.collections.new(obj.name)
+ return collection
+
+ if element.IsDecomposedBy:
+ collection = bpy.data.collections.get(obj.name)
+ if not collection:
+ collection = bpy.data.collections.new(obj.name)
+ return collection
+
+ @classmethod
+ def _get_collection(cls, element, obj):
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
aggregate_obj = tool.Ifc.get_object(aggregate)
collection = bpy.data.collections.get(aggregate_obj.name)
if collection:
- collection.objects.link(obj)
- return
+ return collection
container = ifcopenshell.util.element.get_container(element)
if container:
container_obj = tool.Ifc.get_object(container)
collection = bpy.data.collections.get(container_obj.name)
if collection:
- collection.objects.link(obj)
- return
+ return collection
+
+ if element.is_a("IfcProject"):
+ return bpy.context.scene.collection
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
if project_obj:
collection = bpy.data.collections.get(project_obj.name)
if collection:
- collection.objects.link(obj)
- return
+ return collection
diff --git a/src/blenderbim/test/bim/feature/aggregate.feature b/src/blenderbim/test/bim/feature/aggregate.feature
new file mode 100644
index 0000000000..8a0f3b9141
--- /dev/null
+++ b/src/blenderbim/test/bim/feature/aggregate.feature
@@ -0,0 +1,28 @@
+@aggregate
+Feature: Aggregate
+ Covers element and spatial aggregation
+
+Scenario: Enable editing aggregate
+ Given an empty IFC project
+ And the object "IfcSite/My Site" is selected
+ When I press "bim.enable_editing_aggregate"
+ Then nothing happens
+
+Scenario: Disable editing aggregate
+ Given an empty IFC project
+ And the object "IfcSite/My Site" is selected
+ And I press "bim.enable_editing_aggregate"
+ When I press "bim.disable_editing_aggregate"
+ Then nothing happens
+
+Scenario: Assign object
+ Given an empty IFC project
+ And the object "IfcSite/My Site" is selected
+ And I press "bim.enable_editing_aggregate"
+ And the variable "relating_object" is "tool.Ifc.get().by_type('IfcSite')[0].id()"
+ And the variable "related_object" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()"
+ When I press "bim.assign_object(relating_object={relating_object}, related_object={related_object})"
+ Then the object "IfcSite/My Site" is in the collection "IfcSite/My Site"
+ Then the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey"
+ Then the collection "IfcBuildingStorey/My Storey" is in the collection "IfcSite/My Site"
+
diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py
index dd6424646a..a7700a5df2 100644
--- a/src/blenderbim/test/bim/test_feature.py
+++ b/src/blenderbim/test/bim/test_feature.py
@@ -262,3 +262,8 @@ def prop_is_value(prop, value):
@then(parsers.parse('the object "{name}" is in the collection "{collection}"'))
def the_object_name_is_in_the_collection_collection(name, collection):
assert collection in [c.name for c in the_object_name_exists(name).users_collection]
+
+
+@then(parsers.parse('the collection "{name1}" is in the collection "{name2}"'))
+def the_collection_name1_is_in_the_collection_name2(name1, name2):
+ assert bpy.data.collections.get(name2).children.get(name1)
diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py
index 7b9a94d2b7..c8edfebabb 100644
--- a/src/blenderbim/test/core/bootstrap.py
+++ b/src/blenderbim/test/core/bootstrap.py
@@ -35,6 +35,13 @@ def blender():
prophet.verify()
+@pytest.fixture
+def aggregator():
+ prophet = Prophecy(blenderbim.core.tool.Aggregator)
+ yield prophet
+ prophet.verify()
+
+
@pytest.fixture
def person_editor():
prophet = Prophecy(blenderbim.core.tool.PersonEditor)
diff --git a/src/blenderbim/test/core/test_aggregate.py b/src/blenderbim/test/core/test_aggregate.py
new file mode 100644
index 0000000000..f7f82bc5cb
--- /dev/null
+++ b/src/blenderbim/test/core/test_aggregate.py
@@ -0,0 +1,31 @@
+import blenderbim.core.aggregate as subject
+from test.core.bootstrap import ifc, aggregator, collector
+
+
+class TestEnableEditingAggregate:
+ def test_run(self, aggregator):
+ aggregator.enable_editing("obj").should_be_called()
+ subject.enable_editing_aggregate(aggregator, obj="obj")
+
+
+class TestDisableEditingAggregate:
+ def test_run(self, aggregator):
+ aggregator.disable_editing("obj").should_be_called()
+ subject.disable_editing_aggregate(aggregator, obj="obj")
+
+
+class TestAssignObject:
+ def test_run(self, ifc, aggregator, collector):
+ aggregator.can_aggregate("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(
+ "aggregate.assign_object", product="related_object", relating_object="relating_object"
+ ).should_be_called().will_return("rel")
+ aggregator.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, aggregator, collector, relating_obj="relating_obj", related_obj="related_obj")
+ == "rel"
+ )
diff --git a/src/blenderbim/test/tool/test_aggregator.py b/src/blenderbim/test/tool/test_aggregator.py
new file mode 100644
index 0000000000..92ee92ebe0
--- /dev/null
+++ b/src/blenderbim/test/tool/test_aggregator.py
@@ -0,0 +1,101 @@
+import bpy
+import ifcopenshell
+import blenderbim.core.tool
+import blenderbim.tool as tool
+from test.bim.bootstrap import NewFile
+from blenderbim.tool.aggregator import Aggregator as subject
+
+
+class TestImplementsTool(NewFile):
+ def test_run(self):
+ assert isinstance(subject(), blenderbim.core.tool.Aggregator)
+
+
+class TestEnableEditing(NewFile):
+ def test_run(self):
+ obj = bpy.data.objects.new("Object", None)
+ subject.enable_editing(obj)
+ assert obj.BIMObjectAggregateProperties.is_editing is True
+
+
+class TestDisableEditing(NewFile):
+ def test_run(self):
+ obj = bpy.data.objects.new("Object", None)
+ subject.enable_editing(obj)
+ subject.disable_editing(obj)
+ assert obj.BIMObjectAggregateProperties.is_editing is False
+
+
+class TestCanAggregate(NewFile):
+ def test_elements_can_aggregate(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_aggregate(element_obj, subelement_obj) is True
+
+ def test_spatial_elements_can_aggregate(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ element = ifc.createIfcSite()
+ element_obj = bpy.data.objects.new("Object", None)
+ tool.Ifc.link(element, element_obj)
+ subelement = ifc.createIfcBuilding()
+ subelement_obj = bpy.data.objects.new("Object", None)
+ tool.Ifc.link(subelement, subelement_obj)
+ assert subject.can_aggregate(element_obj, subelement_obj) is True
+
+ def test_spatial_elements_can_aggregate_2x3(self):
+ ifc = ifcopenshell.file(schema="IFC2X3")
+ tool.Ifc.set(ifc)
+ element = ifc.createIfcSite()
+ element_obj = bpy.data.objects.new("Object", None)
+ tool.Ifc.link(element, element_obj)
+ subelement = ifc.createIfcBuilding()
+ subelement_obj = bpy.data.objects.new("Object", None)
+ tool.Ifc.link(subelement, subelement_obj)
+ assert subject.can_aggregate(element_obj, subelement_obj) is True
+
+ def test_spatial_elements_and_projects_can_aggregate(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ element = ifc.createIfcProject()
+ element_obj = bpy.data.objects.new("Object", None)
+ tool.Ifc.link(element, element_obj)
+ subelement = ifc.createIfcBuilding()
+ subelement_obj = bpy.data.objects.new("Object", None)
+ tool.Ifc.link(subelement, subelement_obj)
+ assert subject.can_aggregate(element_obj, subelement_obj) is True
+
+ def test_spatial_elements_and_projects_can_aggregate_2x3(self):
+ ifc = ifcopenshell.file(schema="IFC2X3")
+ tool.Ifc.set(ifc)
+ element = ifc.createIfcProject()
+ element_obj = bpy.data.objects.new("Object", None)
+ tool.Ifc.link(element, element_obj)
+ subelement = ifc.createIfcBuilding()
+ subelement_obj = bpy.data.objects.new("Object", None)
+ tool.Ifc.link(subelement, subelement_obj)
+ assert subject.can_aggregate(element_obj, subelement_obj) is True
+
+ def test_unlinked_elements_cannot_aggregate(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_aggregate(element_obj, subelement_obj) is False
+
+ def test_aggregates_with_meshes_are_invalid(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ element = ifc.createIfcElementAssembly()
+ element_obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
+ 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_aggregate(element_obj, subelement_obj) is False
diff --git a/src/blenderbim/test/tool/test_collector.py b/src/blenderbim/test/tool/test_collector.py
index cc565c0818..f94145c05e 100644
--- a/src/blenderbim/test/tool/test_collector.py
+++ b/src/blenderbim/test/tool/test_collector.py
@@ -55,3 +55,47 @@ class TestAssign(NewFile):
subject.assign(wall_obj)
assert len(wall_obj.users_collection) == 1
assert "IfcProject" in wall_obj.users_collection[0].name
+
+ def test_in_decomposition_mode_spatial_elements_are_placed_in_a_collection_of_the_same_name(self):
+ bpy.ops.bim.create_project()
+ space_obj = bpy.data.objects.new("IfcSpace/Name", None)
+ space_element = tool.Ifc.get().createIfcSpace()
+ tool.Ifc.link(space_element, space_obj)
+ bpy.context.scene.collection.objects.link(space_obj)
+ ifcopenshell.api.run(
+ "aggregate.assign_object",
+ tool.Ifc.get(),
+ relating_object=tool.Ifc.get().by_type("IfcSite")[0],
+ product=space_element,
+ )
+ subject.assign(space_obj)
+ assert len(space_obj.users_collection) == 1
+ assert space_obj.users_collection[0].name == space_obj.name
+
+ def test_in_decomposition_mode_aggregates_are_placed_in_a_collection_of_the_same_name(self):
+ bpy.ops.bim.create_project()
+ element_obj = bpy.data.objects.new("IfcElementAssembly/Name", None)
+ element = tool.Ifc.get().createIfcElementAssembly()
+ subelement_obj = bpy.data.objects.new("IfcBeam/Name", None)
+ subelement = tool.Ifc.get().createIfcBeam()
+ tool.Ifc.link(element, element_obj)
+ bpy.context.scene.collection.objects.link(element_obj)
+ ifcopenshell.api.run(
+ "aggregate.assign_object",
+ tool.Ifc.get(),
+ relating_object=element,
+ product=subelement,
+ )
+ subject.assign(element_obj)
+ assert len(element_obj.users_collection) == 1
+ assert element_obj.users_collection[0].name == element_obj.name
+
+ def test_in_decomposition_mode_projects_are_placed_in_a_collection_of_the_same_name(self):
+ tool.Ifc.set(ifcopenshell.file())
+ element_obj = bpy.data.objects.new("IfcProject/Name", None)
+ element = tool.Ifc.get().createIfcProject()
+ tool.Ifc.link(element, element_obj)
+ bpy.context.scene.collection.objects.link(element_obj)
+ subject.assign(element_obj)
+ assert len(element_obj.users_collection) == 1
+ assert element_obj.users_collection[0].name == element_obj.name