From f9221717ff557bdc4703e1f6e9c9adcdf73bc421 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 17 Nov 2021 13:27:07 +1100 Subject: [PATCH] You can now associate IFC elements with Brick elements and navigate to them --- .../blenderbim/bim/module/brick/__init__.py | 3 ++ .../blenderbim/bim/module/brick/data.py | 43 ++++++++++++++++ .../blenderbim/bim/module/brick/operator.py | 25 +++++++++ .../blenderbim/bim/module/brick/prop.py | 9 +++- .../blenderbim/bim/module/brick/ui.py | 51 ++++++++++++++++++- src/blenderbim/blenderbim/core/brick.py | 13 +++++ src/blenderbim/blenderbim/core/tool.py | 4 ++ src/blenderbim/blenderbim/tool/brick.py | 26 ++++++++++ src/blenderbim/test/core/test_brick.py | 30 ++++++++++- src/blenderbim/test/tool/test_brick.py | 33 ++++++++++++ .../ifcopenshell/api/library/add_reference.py | 6 +++ .../test/api/library/test_add_reference.py | 8 +++ 12 files changed, 248 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/__init__.py b/src/blenderbim/blenderbim/bim/module/brick/__init__.py index d9d6003110..a8224c20c7 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/brick/__init__.py @@ -20,7 +20,9 @@ import bpy from . import ui, prop, operator classes = ( + operator.AssignBrickReference, operator.CloseBrickProject, + operator.ConvertBrickProject, operator.LoadBrickProject, operator.RewindBrickClass, operator.ViewBrickClass, @@ -28,6 +30,7 @@ classes = ( prop.Brick, prop.BIMBrickProperties, ui.BIM_PT_brickschema, + ui.BIM_PT_ifc_brickschema_references, ui.BIM_UL_bricks, ) diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index bd186e0973..5f2e12c4d0 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -23,6 +23,7 @@ from blenderbim.tool.brick import BrickStore def refresh(): BrickschemaData.is_loaded = False + BrickschemaReferencesData.is_loaded = False class BrickschemaData: @@ -76,3 +77,45 @@ class BrickschemaData: } ) return results + + +class BrickschemaReferencesData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = {"is_loaded": cls.get_is_loaded(), "libraries": cls.libraries(), "references": cls.references()} + + @classmethod + def get_is_loaded(cls): + return BrickStore.graph is not None + + @classmethod + def libraries(cls): + results = [] + for library in tool.Ifc.get().by_type("IfcLibraryInformation"): + results.append((str(library.id()), library.Name or "Unnamed", "")) + return results + + @classmethod + def references(cls): + results = [] + for rel in getattr(tool.Ifc.get_entity(bpy.context.active_object), "HasAssociations", []): + if rel.is_a("IfcRelAssociatesLibrary"): + reference = rel.RelatingLibrary + if tool.Ifc.get_schema() == "IFC2X3" and "#" not in reference.ItemReference: + continue + if tool.Ifc.get_schema() != "IFC2X3" and "#" not in reference.Identification: + continue + results.append( + { + "id": reference.id(), + "identification": reference.ItemReference + if tool.Ifc.get_schema() == "IFC2X3" + else reference.Identification, + "name": reference.Name or "Unnamed", + } + ) + return results diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index bc11c4e296..bd70f169ca 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -82,3 +82,28 @@ class CloseBrickProject(bpy.types.Operator, Operator): def _execute(self, context): core.close_brick_project(tool.Brick) + + +class ConvertBrickProject(bpy.types.Operator, Operator): + bl_idname = "bim.convert_brick_project" + bl_label = "Convert Brick Project" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.convert_brick_project(tool.Ifc, tool.Brick) + + +class AssignBrickReference(bpy.types.Operator, Operator): + bl_idname = "bim.assign_brick_reference" + bl_label = "Assign Brick Reference" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + props = context.scene.BIMBrickProperties + core.assign_brick_reference( + tool.Ifc, + tool.Brick, + obj=context.active_object, + library=tool.Ifc.get().by_id(int(props.libraries)), + brick_uri=props.bricks[props.active_brick_index].uri, + ) diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index 8bd7f47d7e..8839786484 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -17,7 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from blenderbim.bim.module.brick.data import BrickschemaData +from blenderbim.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -36,6 +36,12 @@ def update_active_brick_index(self, context): BrickschemaData.is_loaded = False +def get_libraries(self, context): + if not BrickschemaReferencesData.is_loaded: + BrickschemaReferencesData.load() + return BrickschemaReferencesData.data["libraries"] + + class Brick(PropertyGroup): name: StringProperty(name="Name") uri: StringProperty(name="URI") @@ -47,3 +53,4 @@ class BIMBrickProperties(PropertyGroup): brick_breadcrumbs: CollectionProperty(name="Brick Breadcrumbs", type=StrProperty) bricks: CollectionProperty(name="Bricks", type=Brick) active_brick_index: IntProperty(name="Active Brick Index", update=update_active_brick_index) + libraries: EnumProperty(name="Libraries", items=get_libraries) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index 7854500ccb..9cfc2c5dfc 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -16,8 +16,9 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import blenderbim.tool as tool from bpy.types import Panel, UIList -from blenderbim.bim.module.brick.data import BrickschemaData +from blenderbim.bim.module.brick.data import BrickschemaData, BrickschemaReferencesData class BIM_PT_brickschema(Panel): @@ -55,6 +56,54 @@ class BIM_PT_brickschema(Panel): op.item = attribute["value_uri"] +class BIM_PT_ifc_brickschema_references(Panel): + bl_label = "IFC Brickschema References" + bl_idname = "BIM_PT_ifc_brickschema_references" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_brickschema" + + @classmethod + def poll(cls, context): + return tool.Ifc.get() + + def draw(self, context): + if not BrickschemaReferencesData.is_loaded: + BrickschemaReferencesData.load() + self.props = context.scene.BIMBrickProperties + + if not BrickschemaReferencesData.data["is_loaded"]: + row = self.layout.row() + row.label(text="No Brickschema Project Loaded") + return + + if not BrickschemaReferencesData.data["libraries"]: + row = self.layout.row(align=True) + row.label(text="No IFC Libraries") + row.operator("bim.convert_brick_project", text="", icon="ADD") + return + + row = self.layout.row(align=True) + row.prop(self.props, "libraries") + row.operator("bim.convert_brick_project", text="", icon="ADD") + + row = self.layout.row() + row.operator("bim.assign_brick_reference", icon="ADD") + + if not BrickschemaReferencesData.data["references"]: + row = self.layout.row() + row.label(text="No References") + + for reference in BrickschemaReferencesData.data["references"]: + row = self.layout.row(align=True) + row.label(text=reference["identification"], icon="ASSET_MANAGER") + row.label(text=reference["name"]) + row.operator("bim.unassign_library_reference", text="", icon="X").reference = reference["id"] + row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT").item = reference["identification"] + + class BIM_UL_bricks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index 6257ded455..81372f106a 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -47,3 +47,16 @@ def rewind_brick_class(brick): def close_brick_project(brick): brick.clear_project() + + +def convert_brick_project(ifc, brick): + library = ifc.run("library.add_library", name=brick.get_brick_path_name()) + ifc.run("library.edit_library", library=library, attributes={"Location": brick.get_brick_path()}) + + +def assign_brick_reference(ifc, brick, obj=None, library=None, brick_uri=None): + reference = brick.get_library_brick_reference(library, brick_uri) + if not reference: + reference = ifc.run("library.add_reference", library=library) + ifc.run("library.edit_reference", reference=reference, attributes=brick.export_brick_attributes(brick_uri)) + ifc.run("library.assign_reference", product=ifc.get_entity(obj), reference=reference) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 6fb5153944..8ef55cb76f 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -46,7 +46,11 @@ class Brick: def add_brick_breadcrumb(cls): pass def clear_brick_browser(cls): pass def clear_project(cls): pass + def export_brick_attributes(cls, brick_uri): pass + def get_brick_path(cls): pass + def get_brick_path_name(cls): pass def get_item_class(cls, item): pass + def get_library_brick_reference(cls, library, brick_uri): pass def import_brick_classes(cls, brick_class): pass def import_brick_items(cls, brick_class): pass def load_brick_file(cls, filepath): pass diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 2a970450d4..49533aa5c8 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -48,6 +48,18 @@ class Brick(blenderbim.core.tool.Brick): bpy.context.scene.BIMBrickProperties.active_brick_class == "" bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear() + @classmethod + def export_brick_attributes(cls, brick_uri): + return {"Identification": brick_uri, "Name": brick_uri.split("#")[-1]} + + @classmethod + def get_brick_path(cls): + return BrickStore.path + + @classmethod + def get_brick_path_name(cls): + return os.path.basename(BrickStore.path) + @classmethod def get_item_class(cls, item): query = BrickStore.graph.query( @@ -64,6 +76,17 @@ class Brick(blenderbim.core.tool.Brick): for row in query: return row.get("class").toPython().split("#")[-1] + @classmethod + def get_library_brick_reference(cls, library, brick_uri): + if tool.Ifc.get_schema() == "IFC2X3": + for reference in library.LibraryReference: + if reference.ItemReference == brick_uri: + return reference + else: + for reference in library.HasLibraryReferences: + if reference.Identification == brick_uri: + return reference + @classmethod def import_brick_classes(cls, brick_class): query = BrickStore.graph.query( @@ -115,6 +138,7 @@ class Brick(blenderbim.core.tool.Brick): schema_path = os.path.join(cwd, "..", "bim", "schema", "Brick.ttl") BrickStore.schema.load_file(schema_path) BrickStore.graph = brickschema.Graph().load_file(filepath) + BrickStore.schema + BrickStore.path = filepath @classmethod def pop_brick_breadcrumb(cls): @@ -138,8 +162,10 @@ class Brick(blenderbim.core.tool.Brick): class BrickStore: schema = None graph = None + path = None @staticmethod def purge(): BrickStore.schema = None BrickStore.graph = None + BrickStore.path = None diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py index 0f34837b4d..f5837d3106 100644 --- a/src/blenderbim/test/core/test_brick.py +++ b/src/blenderbim/test/core/test_brick.py @@ -17,7 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import blenderbim.core.brick as subject -from test.core.bootstrap import brick +from test.core.bootstrap import ifc, brick class TestLoadBrickProject: @@ -64,3 +64,31 @@ class TestCloseBrickProject: def test_run(self, brick): brick.clear_project().should_be_called() subject.close_brick_project(brick) + + +class TestConvertBrickProject: + def test_run(self, ifc, brick): + brick.get_brick_path_name().should_be_called().will_return("foo.ttl") + ifc.run("library.add_library", name="foo.ttl").should_be_called().will_return("library") + brick.get_brick_path().should_be_called().will_return("/path/to/foo.ttl") + ifc.run( + "library.edit_library", library="library", attributes={"Location": "/path/to/foo.ttl"} + ).should_be_called() + subject.convert_brick_project(ifc, brick) + + +class TestAssignBrickReference: + def test_assigning_to_a_new_reference(self, ifc, brick): + brick.get_library_brick_reference("library", "brick").should_be_called().will_return(None) + ifc.run("library.add_reference", library="library").should_be_called().will_return("reference") + brick.export_brick_attributes("brick").should_be_called().will_return("attributes") + ifc.run("library.edit_reference", reference="reference", attributes="attributes").should_be_called() + ifc.get_entity("obj").should_be_called().will_return("product") + ifc.run("library.assign_reference", product="product", reference="reference").should_be_called() + subject.assign_brick_reference(ifc, brick, obj="obj", library="library", brick_uri="brick") + + def test_assigning_to_an_existing_reference(self, ifc, brick): + brick.get_library_brick_reference("library", "brick").should_be_called().will_return("reference") + ifc.get_entity("obj").should_be_called().will_return("product") + ifc.run("library.assign_reference", product="product", reference="reference").should_be_called() + subject.assign_brick_reference(ifc, brick, obj="obj", library="library", brick_uri="brick") diff --git a/src/blenderbim/test/tool/test_brick.py b/src/blenderbim/test/tool/test_brick.py index 0a6c1b9274..f8988886e0 100644 --- a/src/blenderbim/test/tool/test_brick.py +++ b/src/blenderbim/test/tool/test_brick.py @@ -59,12 +59,45 @@ class TestClearProject(NewFile): assert len(bpy.context.scene.BIMBrickProperties.brick_breadcrumbs) == 0 +class TestExportBrickAttributes(NewFile): + def test_run(self): + assert subject.export_brick_attributes("ex:#floor") == {"Identification": "ex:#floor", "Name": "floor"} + + +class TestGetBrickPath(NewFile): + def test_run(self): + TestLoadBrickFile().test_run() + cwd = os.path.dirname(os.path.realpath(__file__)) + assert subject.get_brick_path() == os.path.join(cwd, "..", "files", "spaces.ttl") + + +class TestGetBrickPathName(NewFile): + def test_run(self): + TestLoadBrickFile().test_run() + assert subject.get_brick_path_name() == "spaces.ttl" + + class TestGetItemClass(NewFile): def test_run(self): TestLoadBrickFile().test_run() assert subject.get_item_class("ex:#floor") == "Floor" +class TestGetLibraryBrickReference(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + library = ifc.createIfcLibraryInformation() + reference = ifc.createIfcLibraryReference(Identification="ex:#floor", ReferencedLibrary=library) + assert subject.get_library_brick_reference(library, "ex:#floor") == reference + + def test_run_ifc2x3(self): + ifc = ifcopenshell.file(schema="IFC2X3") + tool.Ifc.set(ifc) + reference = ifc.createIfcLibraryReference(ItemReference="ex:#floor") + library = ifc.createIfcLibraryInformation(LibraryReference=[reference]) + assert subject.get_library_brick_reference(library, "ex:#floor") == reference + + class TestImportBrickClasses(NewFile): def test_run(self): TestLoadBrickFile().test_run() diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py index f7427155c6..70c92a39e8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py @@ -11,4 +11,10 @@ class Usecase: self.settings[key] = value def execute(self): + if self.file.schema == "IFC2X3": + reference = self.file.createIfcLibraryReference() + references = list(self.settings["library"].LibraryReference or []) + references.append(reference) + self.settings["library"].LibraryReference = references + return reference return self.file.createIfcLibraryReference(ReferencedLibrary=self.settings["library"]) diff --git a/src/ifcopenshell-python/test/api/library/test_add_reference.py b/src/ifcopenshell-python/test/api/library/test_add_reference.py index a33b6cfc23..edb7e532f3 100644 --- a/src/ifcopenshell-python/test/api/library/test_add_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_add_reference.py @@ -8,3 +8,11 @@ class TestAddReference(test.bootstrap.IFC4): reference = ifcopenshell.api.run("library.add_reference", self.file, library=library) assert reference.is_a("IfcLibraryReference") assert reference.ReferencedLibrary == library + + +class TestAddReferenceIFC2X3(test.bootstrap.IFC2X3): + def test_adding_a_reference(self): + library = ifcopenshell.api.run("library.add_library", self.file, name="Name") + reference = ifcopenshell.api.run("library.add_reference", self.file, library=library) + assert reference.is_a("IfcLibraryReference") + assert library.LibraryReference == (reference,)