diff --git a/src/blenderbim/blenderbim/bim/module/owner/__init__.py b/src/blenderbim/blenderbim/bim/module/owner/__init__.py index 9b676a586d..3c9ba9de47 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/owner/__init__.py @@ -28,6 +28,7 @@ classes = ( operator.AddPersonAndOrganisation, operator.AddPersonAttribute, operator.AddRole, + operator.AssignActor, operator.ClearUser, operator.DisableEditingActor, operator.DisableEditingAddress, @@ -53,11 +54,13 @@ classes = ( operator.RemovePersonAttribute, operator.RemoveRole, operator.SetUser, + operator.UnassignActor, prop.BIMOwnerProperties, ui.BIM_PT_people, ui.BIM_PT_organisations, ui.BIM_PT_owner, ui.BIM_PT_actor, + ui.BIM_PT_object_actor, ) diff --git a/src/blenderbim/blenderbim/bim/module/owner/data.py b/src/blenderbim/blenderbim/bim/module/owner/data.py index 972da087da..be1cae0192 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/data.py +++ b/src/blenderbim/blenderbim/bim/module/owner/data.py @@ -25,6 +25,7 @@ def refresh(): OrganisationsData.is_loaded = False OwnerData.is_loaded = False ActorData.is_loaded = False + ObjectActorData.is_loaded = False class RolesAddressesData: @@ -243,3 +244,46 @@ class ActorData: {"id": actor.id(), "name": actor.Name or "Unnamed", "the_actor": the_actor, "is_editing": is_editing} ) return actors + + +class ObjectActorData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = { + "actor": cls.actor(), + "actors": cls.actors() + } + + @classmethod + def actor(cls): + return [(str(p.id()), p.Name or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcActor")] + + @classmethod + def actors(cls): + results = [] + element = tool.Ifc.get_entity(bpy.context.active_object) + if not element: + return results + for rel in getattr(element, "HasAssignments", []): + if rel.is_a("IfcRelAssignsToActor"): + actor = rel.RelatingActor + if actor.TheActor.is_a("IfcPerson"): + roles = cls.get_roles(actor.TheActor) + elif actor.TheActor.is_a("IfcOrganization"): + roles = cls.get_roles(actor.TheActor) + elif actor.TheActor.is_a("IfcPersonAndOrganization"): + roles = cls.get_roles(actor.TheActor.ThePerson) + roles.extend(cls.get_roles(actor.TheActor.TheOrganization)) + role = ", ".join(roles) + results.append({ + "id": actor.id(), "name": actor.Name or "Unnamed", "role": role, "ifc_class": actor.is_a() + }) + return results + + @classmethod + def get_roles(cls, parent): + return [r.UserDefinedRole or r.Role for r in parent.Roles or []] diff --git a/src/blenderbim/blenderbim/bim/module/owner/operator.py b/src/blenderbim/blenderbim/bim/module/owner/operator.py index 48935f91d3..38709184b5 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/operator.py +++ b/src/blenderbim/blenderbim/bim/module/owner/operator.py @@ -355,3 +355,27 @@ class RemoveActor(bpy.types.Operator, Operator): def _execute(self, context): core.remove_actor(tool.Ifc, actor=tool.Ifc.get().by_id(self.actor)) + + +class AssignActor(bpy.types.Operator, Operator): + bl_idname = "bim.assign_actor" + bl_label = "Assign Actor" + bl_options = {"REGISTER", "UNDO"} + actor: bpy.props.IntProperty() + + def _execute(self, context): + core.assign_actor( + tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=tool.Ifc.get_entity(context.active_object) + ) + + +class UnassignActor(bpy.types.Operator, Operator): + bl_idname = "bim.unassign_actor" + bl_label = "Unassign Actor" + bl_options = {"REGISTER", "UNDO"} + actor: bpy.props.IntProperty() + + def _execute(self, context): + core.unassign_actor( + tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=tool.Ifc.get_entity(context.active_object) + ) diff --git a/src/blenderbim/blenderbim/bim/module/owner/prop.py b/src/blenderbim/blenderbim/bim/module/owner/prop.py index 57e1095c8b..c1a2e65915 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/prop.py +++ b/src/blenderbim/blenderbim/bim/module/owner/prop.py @@ -18,7 +18,7 @@ import bpy from blenderbim.bim.prop import StrProperty, Attribute -from blenderbim.bim.module.owner.data import OwnerData, ActorData +from blenderbim.bim.module.owner.data import OwnerData, ActorData, ObjectActorData from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -50,6 +50,12 @@ def get_the_actor(self, context): return ActorData.data["the_actor"] +def get_actor(self, context): + if not ObjectActorData.is_loaded: + ObjectActorData.load() + return ObjectActorData.data["actor"] + + def update_actor_type(self, context): ActorData.data["the_actor"] = ActorData.the_actor() @@ -95,3 +101,4 @@ class BIMOwnerProperties(PropertyGroup): update=update_actor_type, ) the_actor: EnumProperty(items=get_the_actor, name="Actor") + actor: EnumProperty(items=get_actor, name="Actor") diff --git a/src/blenderbim/blenderbim/bim/module/owner/ui.py b/src/blenderbim/blenderbim/bim/module/owner/ui.py index 09865648b0..2cf26457c0 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/ui.py +++ b/src/blenderbim/blenderbim/bim/module/owner/ui.py @@ -19,7 +19,7 @@ import bpy import blenderbim.bim.helper import blenderbim.tool as tool -from blenderbim.bim.module.owner.data import PeopleData, OrganisationsData, OwnerData, ActorData +from blenderbim.bim.module.owner.data import PeopleData, OrganisationsData, OwnerData, ActorData, ObjectActorData def draw_roles(box, parent): @@ -281,3 +281,38 @@ class BIM_PT_actor(bpy.types.Panel): row.label(text=actor["the_actor"]) row.operator("bim.enable_editing_actor", icon="GREASEPENCIL", text="").actor = actor["id"] row.operator("bim.remove_actor", icon="X", text="").actor = actor["id"] + + +class BIM_PT_object_actor(bpy.types.Panel): + bl_label = "IFC Actor" + bl_idname = "BIM_PT_object_actor" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_parent_id = "BIM_PT_misc_object" + + @classmethod + def poll(cls, context): + return tool.Ifc.get() + + def draw(self, context): + if not ObjectActorData.is_loaded: + ObjectActorData.load() + + self.props = context.scene.BIMOwnerProperties + + if not ObjectActorData.data["actor"]: + row = self.layout.row(align=True) + row.label(text="No Actors Found", icon="USER") + return + + row = self.layout.row(align=True) + row.prop(self.props, "actor", text="") + row.operator("bim.assign_actor", icon="ADD", text="").actor = int(self.props.actor) + + for actor in ObjectActorData.data["actors"]: + row = self.layout.row(align=True) + row.label(text=actor["name"], icon="USER") + row.label(text=actor["role"]) + row.operator("bim.unassign_actor", icon="X", text="").actor = actor["id"] diff --git a/src/blenderbim/blenderbim/core/owner.py b/src/blenderbim/blenderbim/core/owner.py index 65b28cac39..dfeec08970 100644 --- a/src/blenderbim/blenderbim/core/owner.py +++ b/src/blenderbim/blenderbim/core/owner.py @@ -165,3 +165,11 @@ def disable_editing_actor(owner): def edit_actor(ifc, owner): ifc.run("owner.edit_actor", actor=owner.get_actor(), attributes=owner.export_actor_attributes()) disable_editing_actor(owner) + + +def assign_actor(ifc, actor=None, element=None): + ifc.run("owner.assign_actor", relating_actor=actor, related_object=element) + + +def unassign_actor(ifc, actor=None, element=None): + ifc.run("owner.unassign_actor", relating_actor=actor, related_object=element) diff --git a/src/blenderbim/test/bim/feature/owner.feature b/src/blenderbim/test/bim/feature/owner.feature index 3e8f13513f..7852e2d70e 100644 --- a/src/blenderbim/test/bim/feature/owner.feature +++ b/src/blenderbim/test/bim/feature/owner.feature @@ -277,3 +277,30 @@ Scenario: Edit actor And I press "bim.enable_editing_actor(actor={actor})" When I press "bim.edit_actor" Then "scene.BIMOwnerProperties.active_actor_id" is "0" + +Scenario: Assign actor + Given an empty IFC project + And I press "bim.add_person" + And I press "bim.add_actor" + 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 variable "actor" is "{ifc}.by_type('IfcActor')[0].id()" + And the object "IfcWall/Cube" is selected + When I press "bim.assign_actor(actor={actor})" + Then nothing happens + +Scenario: Unassign actor + Given an empty IFC project + And I press "bim.add_person" + And I press "bim.add_actor" + 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 variable "actor" is "{ifc}.by_type('IfcActor')[0].id()" + And the object "IfcWall/Cube" is selected + And I press "bim.assign_actor(actor={actor})" + When I press "bim.unassign_actor(actor={actor})" + Then nothing happens diff --git a/src/blenderbim/test/core/test_owner.py b/src/blenderbim/test/core/test_owner.py index 91e23281c6..0587e3b8cd 100644 --- a/src/blenderbim/test/core/test_owner.py +++ b/src/blenderbim/test/core/test_owner.py @@ -259,3 +259,15 @@ class TestEditActor: ifc.run("owner.edit_actor", actor="actor", attributes="attributes").should_be_called() owner.clear_actor().should_be_called() subject.edit_actor(ifc, owner) + + +class TestAssignActor: + def test_run(self, ifc, owner): + ifc.run("owner.assign_actor", relating_actor="actor", related_object="element").should_be_called() + subject.assign_actor(ifc, actor="actor", element="element") + + +class TestUnassignActor: + def test_run(self, ifc, owner): + ifc.run("owner.unassign_actor", relating_actor="actor", related_object="element").should_be_called() + subject.unassign_actor(ifc, actor="actor", element="element") diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/assign_actor.py b/src/ifcopenshell-python/ifcopenshell/api/owner/assign_actor.py new file mode 100644 index 0000000000..eefe3d5142 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/assign_actor.py @@ -0,0 +1,59 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "relating_actor": None, + "related_object": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + if self.settings["related_object"].HasAssignments: + for rel in self.settings["related_object"].HasAssignments: + if rel.is_a("IfcRelAssignsToActor") and rel.RelatingActor == self.settings["relating_actor"]: + return + + rel = None + + if self.settings["relating_actor"].IsActingUpon: + rel = self.settings["relating_actor"].IsActingUpon[0] + + if rel: + related_objects = list(rel.RelatedObjects) + related_objects.append(self.settings["related_object"]) + rel.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + rel = self.file.create_entity( + "IfcRelAssignsToActor", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), + "RelatedObjects": [self.settings["related_object"]], + "RelatingActor": self.settings["relating_actor"], + } + ) + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/unassign_actor.py b/src/ifcopenshell-python/ifcopenshell/api/owner/unassign_actor.py new file mode 100644 index 0000000000..9bd8b2f834 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/unassign_actor.py @@ -0,0 +1,43 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = { + "relating_actor": None, + "related_object": None, + } + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for rel in self.settings["related_object"].HasAssignments or []: + if not rel.is_a("IfcRelAssignsToActor") or rel.RelatingActor != self.settings["relating_actor"]: + continue + if len(rel.RelatedObjects) == 1: + return self.file.remove(rel) + related_objects = list(rel.RelatedObjects) + related_objects.remove(self.settings["related_object"]) + rel.RelatedObjects = related_objects + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + return rel diff --git a/src/ifcopenshell-python/test/api/owner/test_assign_actor.py b/src/ifcopenshell-python/test/api/owner/test_assign_actor.py new file mode 100644 index 0000000000..720cf1492b --- /dev/null +++ b/src/ifcopenshell-python/test/api/owner/test_assign_actor.py @@ -0,0 +1,38 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api + + +class TestAssignProduct(test.bootstrap.IFC4): + def test_assigning_an_actor(self): + wall = self.file.createIfcWall() + wall2 = self.file.createIfcWall() + actor = self.file.createIfcActor() + ifcopenshell.api.run("owner.assign_actor", self.file, relating_actor=actor, related_object=wall) + assert actor.IsActingUpon[0].RelatedObjects == (wall,) + ifcopenshell.api.run("owner.assign_actor", self.file, relating_actor=actor, related_object=wall2) + assert actor.IsActingUpon[0].RelatedObjects == (wall, wall2) + + def test_not_assigning_twice(self): + wall = self.file.createIfcWall() + actor = self.file.createIfcActor() + ifcopenshell.api.run("owner.assign_actor", self.file, relating_actor=actor, related_object=wall) + ifcopenshell.api.run("owner.assign_actor", self.file, relating_actor=actor, related_object=wall) + assert actor.IsActingUpon[0].RelatedObjects == (wall,) diff --git a/src/ifcopenshell-python/test/api/owner/test_unassign_actor.py b/src/ifcopenshell-python/test/api/owner/test_unassign_actor.py new file mode 100644 index 0000000000..c7e4e7c220 --- /dev/null +++ b/src/ifcopenshell-python/test/api/owner/test_unassign_actor.py @@ -0,0 +1,29 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api + + +class TestUnassignActor(test.bootstrap.IFC4): + def test_unassigning_an_actor(self): + wall = self.file.createIfcWall() + actor = self.file.createIfcActor() + ifcopenshell.api.run("owner.assign_actor", self.file, relating_actor=actor, related_object=wall) + ifcopenshell.api.run("owner.unassign_actor", self.file, relating_actor=actor, related_object=wall) + assert len(self.file.by_type("IfcRelAssignsToActor")) == 0