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