diff --git a/src/blenderbim/blenderbim/bim/module/spatial/data.py b/src/blenderbim/blenderbim/bim/module/spatial/data.py index a9db36f7dd..32a98f5421 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/data.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/data.py @@ -33,9 +33,9 @@ class SpatialData: def load(cls): cls.data = { "parent_container_id": cls.get_parent_container_id(), - "is_contained": cls.is_contained(), "is_directly_contained": cls.is_directly_contained(), - "label": cls.get_label(), + "label": cls.label(), + "references": cls.references(), } cls.is_loaded = True @@ -50,11 +50,7 @@ class SpatialData: return container.Decomposes[0].RelatingObject.id() @classmethod - def is_contained(cls): - return ifcopenshell.util.element.get_container(tool.Ifc.get_entity(bpy.context.active_object)) - - @classmethod - def get_label(cls): + def label(cls): container = ifcopenshell.util.element.get_container(tool.Ifc.get_entity(bpy.context.active_object)) if container: label = f"{container.is_a()}/{container.Name or ''}" @@ -62,6 +58,11 @@ class SpatialData: label += "*" return label + @classmethod + def references(cls): + results = ifcopenshell.util.element.get_referenced_structures(tool.Ifc.get_entity(bpy.context.active_object)) + return sorted([f"{r.is_a()}/{r.Name or ''}" for r in results]) + @classmethod def is_directly_contained(cls): return bool(getattr(tool.Ifc.get_entity(bpy.context.active_object), "ContainedInStructure", False)) diff --git a/src/blenderbim/blenderbim/bim/module/spatial/operator.py b/src/blenderbim/blenderbim/bim/module/spatial/operator.py index 185a46b6ca..f8e7bb7f0c 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/operator.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/operator.py @@ -27,14 +27,41 @@ from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.spatial.data import SpatialData -class Operator: - def execute(self, context): - IfcStore.execute_ifc_operator(self, context) - blenderbim.bim.handler.refresh_ui_data() - return {"FINISHED"} +class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.reference_structure" + bl_label = "Reference Structure" + bl_options = {"REGISTER", "UNDO"} + structure: bpy.props.IntProperty() + + def _execute(self, context): + sprops = context.scene.BIMSpatialProperties + containers = [tool.Ifc.get().by_id(c.ifc_definition_id) for c in sprops.containers if c.is_selected] + for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if not element: + continue + for container in containers: + core.reference_structure(tool.Ifc, tool.Spatial, structure=container, element=element) -class AssignContainer(bpy.types.Operator, Operator): +class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.dereference_structure" + bl_label = "Dereference Structure" + bl_options = {"REGISTER", "UNDO"} + structure: bpy.props.IntProperty() + + def _execute(self, context): + sprops = context.scene.BIMSpatialProperties + containers = [tool.Ifc.get().by_id(c.ifc_definition_id) for c in sprops.containers if c.is_selected] + for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if not element: + continue + for container in containers: + core.dereference_structure(tool.Ifc, tool.Spatial, structure=container, element=element) + + +class AssignContainer(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_container" bl_label = "Assign Container" bl_options = {"REGISTER", "UNDO"} @@ -48,7 +75,7 @@ class AssignContainer(bpy.types.Operator, Operator): ) -class EnableEditingContainer(bpy.types.Operator, Operator): +class EnableEditingContainer(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.enable_editing_container" bl_label = "Enable Editing Container" bl_options = {"REGISTER", "UNDO"} @@ -57,7 +84,7 @@ class EnableEditingContainer(bpy.types.Operator, Operator): core.enable_editing_container(tool.Spatial, obj=context.active_object) -class ChangeSpatialLevel(bpy.types.Operator, Operator): +class ChangeSpatialLevel(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.change_spatial_level" bl_label = "Change Spatial Level" bl_options = {"REGISTER", "UNDO"} @@ -67,7 +94,7 @@ class ChangeSpatialLevel(bpy.types.Operator, Operator): core.change_spatial_level(tool.Spatial, parent=tool.Ifc.get().by_id(self.parent)) -class DisableEditingContainer(bpy.types.Operator, Operator): +class DisableEditingContainer(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.disable_editing_container" bl_label = "Disable Editing Container" bl_options = {"REGISTER", "UNDO"} @@ -76,7 +103,7 @@ class DisableEditingContainer(bpy.types.Operator, Operator): core.disable_editing_container(tool.Spatial, obj=context.active_object) -class RemoveContainer(bpy.types.Operator, Operator): +class RemoveContainer(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_container" bl_label = "Remove Container" bl_options = {"REGISTER", "UNDO"} @@ -86,11 +113,16 @@ class RemoveContainer(bpy.types.Operator, Operator): core.remove_container(tool.Ifc, tool.Collector, obj=obj) -class CopyToContainer(bpy.types.Operator, Operator): +class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator): """ - Copies selected objects to selected containers - Check the mark next to a container in the container list to select it - Several containers can be selected at a time + Copies selected 3D elements in the viewport to checkmarked spatial containers + + Example: bulk copy a wall to multiple storeys + + 1. Select one or more 3D elements in the viewport + 2. Enable the checkmark next to one or more containers in the container list below to select it + 3. Press this button + 4. The copied elements will have a new position relative to the destination containers """ bl_idname = "bim.copy_to_container" @@ -105,7 +137,7 @@ class CopyToContainer(bpy.types.Operator, Operator): blenderbim.bim.handler.purge_module_data() -class SelectContainer(bpy.types.Operator, Operator): +class SelectContainer(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.select_container" bl_label = "Select Container" bl_options = {"REGISTER", "UNDO"} @@ -114,7 +146,7 @@ class SelectContainer(bpy.types.Operator, Operator): core.select_container(tool.Ifc, tool.Spatial, obj=context.active_object) -class SelectSimilarContainer(bpy.types.Operator, Operator): +class SelectSimilarContainer(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.select_similar_container" bl_label = "Select Similar Container" bl_options = {"REGISTER", "UNDO"} diff --git a/src/blenderbim/blenderbim/bim/module/spatial/ui.py b/src/blenderbim/blenderbim/bim/module/spatial/ui.py index 8f0e78ecf4..347c355b1a 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/ui.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/ui.py @@ -55,13 +55,15 @@ class BIM_PT_spatial(Panel): if props.containers and props.active_container_index < len(props.containers): op = row.operator("bim.assign_container", icon="CHECKMARK") op.structure = props.containers[props.active_container_index].ifc_definition_id + row.operator("bim.reference_structure", icon="LINKED", text="") + row.operator("bim.dereference_structure", icon="UNLINKED", text="") 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", "", props, "containers", props, "active_container_index") else: row = self.layout.row(align=True) - if SpatialData.data["is_contained"]: + if SpatialData.data["label"]: row.label(text=SpatialData.data["label"]) row.operator("bim.select_container", icon="TRACKER", text="") row.operator("bim.select_similar_container", icon="RESTRICT_SELECT_OFF", text="") @@ -71,6 +73,9 @@ class BIM_PT_spatial(Panel): else: row.label(text="This object is not spatially contained") row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="") + for reference in SpatialData.data["references"]: + row = self.layout.row() + row.label(text=reference, icon="LINKED") class BIM_UL_containers(UIList): diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index e256eb894f..e4090dce4e 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -19,6 +19,16 @@ import blenderbim.core +def reference_structure(ifc, spatial, structure=None, element=None): + if spatial.can_reference(structure, element): + return ifc.run("spatial.reference_structure", product=element, relating_structure=structure) + + +def dereference_structure(ifc, spatial, structure=None, element=None): + if spatial.can_reference(structure, element): + return ifc.run("spatial.dereference_structure", product=element, relating_structure=structure) + + def assign_container(ifc, collector, spatial, structure_obj=None, element_obj=None): if not spatial.can_contain(structure_obj, element_obj): return diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 99d570dc85..4e7d74d0bb 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -435,6 +435,7 @@ class Sequence: @interface class Spatial: def can_contain(cls, structure_obj, element_obj): pass + def can_reference(cls, structure, element): pass def disable_editing(cls, obj): pass def duplicate_object_and_data(cls, obj): pass def enable_editing(cls, obj): pass diff --git a/src/blenderbim/blenderbim/tool/spatial.py b/src/blenderbim/blenderbim/tool/spatial.py index 18e41ec01e..4470901759 100644 --- a/src/blenderbim/blenderbim/tool/spatial.py +++ b/src/blenderbim/blenderbim/tool/spatial.py @@ -41,6 +41,20 @@ class Spatial(blenderbim.core.tool.Spatial): return False return True + @classmethod + def can_reference(cls, structure, element): + if not structure or not element: + return False + if tool.Ifc.get_schema() == "IFC2X3": + if not structure.is_a("IfcSpatialStructureElement"): + return False + else: + if not structure.is_a("IfcSpatialElement"): + return False + if not hasattr(element, "ReferencedInStructures"): + return False + return True + @classmethod def disable_editing(cls, obj): obj.BIMObjectSpatialProperties.is_editing = False diff --git a/src/blenderbim/test/bim/feature/spatial.feature b/src/blenderbim/test/bim/feature/spatial.feature index 20d51badfd..acdf6ee292 100644 --- a/src/blenderbim/test/bim/feature/spatial.feature +++ b/src/blenderbim/test/bim/feature/spatial.feature @@ -41,6 +41,31 @@ Scenario: Copy to container And I press "bim.copy_to_container" Then the object "IfcWall/Cube.001" is in the collection "IfcSite/My Site" +Scenario: Reference structure + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And the object "IfcWall/Cube" is selected + And I press "bim.enable_editing_container" + When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" + And I press "bim.reference_structure" + Then nothing happens + +Scenario: Dereference structure + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And the object "IfcWall/Cube" is selected + And I press "bim.enable_editing_container" + When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" + And I press "bim.reference_structure" + And I press "bim.dereference_structure" + Then nothing happens + Scenario: Select container Given an empty IFC project And I add a cube diff --git a/src/blenderbim/test/core/test_spatial.py b/src/blenderbim/test/core/test_spatial.py index 5bec7131a7..0c8e37e571 100644 --- a/src/blenderbim/test/core/test_spatial.py +++ b/src/blenderbim/test/core/test_spatial.py @@ -20,6 +20,20 @@ import blenderbim.core.spatial as subject from test.core.bootstrap import ifc, collector, spatial +class TestReferenceStructure: + def test_run(self, ifc, spatial): + spatial.can_reference("structure", "element").should_be_called().will_return(True) + ifc.run("spatial.reference_structure", product="element", relating_structure="structure").should_be_called() + subject.reference_structure(ifc, spatial, structure="structure", element="element") + + +class TestDereferenceStructure: + def test_run(self, ifc, spatial): + spatial.can_reference("structure", "element").should_be_called().will_return(True) + ifc.run("spatial.dereference_structure", product="element", relating_structure="structure").should_be_called() + subject.dereference_structure(ifc, spatial, structure="structure", element="element") + + class TestAssignContainer: def test_run(self, ifc, collector, spatial): spatial.can_contain("structure_obj", "element_obj").should_be_called().will_return(True) diff --git a/src/blenderbim/test/tool/test_spatial.py b/src/blenderbim/test/tool/test_spatial.py index d3f097fcb7..adf7fa26bc 100644 --- a/src/blenderbim/test/tool/test_spatial.py +++ b/src/blenderbim/test/tool/test_spatial.py @@ -92,6 +92,25 @@ class TestCanContain(NewFile): assert subject.can_contain(structure_obj, element_obj) is True +class TestCanReference(NewFile): + def test_an_element_can_reference_a_spatial_element(self): + ifc = ifcopenshell.file() + assert subject.can_reference(ifc.createIfcSite(), ifc.createIfcWall()) is True + + def test_an_element_can_reference_a_spatial_element_ifc2x3(self): + ifc = ifcopenshell.file(schema="IFC2X3") + tool.Ifc.set(ifc) + assert subject.can_reference(ifc.createIfcSite(), ifc.createIfcWall()) is True + + def test_a_non_spatial_element_cannot_reference_anything(self): + ifc = ifcopenshell.file() + assert subject.can_reference(ifc.createIfcWall(), ifc.createIfcWall()) is False + + def test_a_non_element_cannot_reference_anything(self): + ifc = ifcopenshell.file() + assert subject.can_reference(ifc.createIfcSite(), ifc.createIfcTask()) is False + + class TestDisableEditing(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None)