From d156927e136a80ee361ac598d6eaff3f419c20fd Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 9 Dec 2021 17:48:47 +1100 Subject: [PATCH] From an IFC element, you can now add a new Brick element in a Brick model --- .../blenderbim/bim/module/brick/__init__.py | 1 + .../blenderbim/bim/module/brick/data.py | 34 ++++++++++++++- .../blenderbim/bim/module/brick/operator.py | 20 +++++++++ .../blenderbim/bim/module/brick/prop.py | 14 ++++++ .../blenderbim/bim/module/brick/ui.py | 7 +++ src/blenderbim/blenderbim/core/brick.py | 7 +++ src/blenderbim/blenderbim/core/tool.py | 2 + src/blenderbim/blenderbim/tool/brick.py | 15 ++++++- src/blenderbim/test/bim/feature/brick.feature | 25 +++++++++++ src/blenderbim/test/core/test_brick.py | 13 ++++++ src/blenderbim/test/files/spaces.ttl | 26 +++++------ src/blenderbim/test/tool/test_brick.py | 43 +++++++++++++++---- 12 files changed, 183 insertions(+), 24 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/__init__.py b/src/blenderbim/blenderbim/bim/module/brick/__init__.py index a8224c20c7..26eaf4fb8d 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/brick/__init__.py @@ -20,6 +20,7 @@ import bpy from . import ui, prop, operator classes = ( + operator.AddBrick, operator.AssignBrickReference, operator.CloseBrickProject, operator.ConvertBrickProject, diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index 364d969c6e..2fc5d38377 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -38,8 +38,13 @@ class BrickschemaData: @classmethod def load(cls): - cls.data = {"is_loaded": cls.get_is_loaded(), "attributes": cls.attributes()} cls.is_loaded = True + cls.data = { + "is_loaded": cls.get_is_loaded(), + "attributes": cls.attributes(), + "namespaces": cls.namespaces(), + "brick_equipment_classes": cls.brick_equipment_classes(), + } @classmethod def get_is_loaded(cls): @@ -99,6 +104,33 @@ class BrickschemaData: ) return results + @classmethod + def namespaces(cls): + if BrickStore.graph is None: + return [] + results = [] + for alias, uri in BrickStore.graph.namespaces(): + results.append((uri, f"{alias}: {uri}", "")) + return results + + @classmethod + def brick_equipment_classes(cls): + if BrickStore.graph is None: + return [] + results = [] + query = BrickStore.graph.query( + """ + PREFIX brick: + PREFIX rdfs: + SELECT ?class WHERE { + ?class rdfs:subClassOf* brick:Equipment . + } + """ + ) + for uri in sorted([x[0].toPython() for x in query]): + results.append((uri, uri.split("#")[-1], "")) + return results + class BrickschemaReferencesData: data = {} diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index bd70f169ca..6cee0af3ad 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -107,3 +107,23 @@ class AssignBrickReference(bpy.types.Operator, Operator): library=tool.Ifc.get().by_id(int(props.libraries)), brick_uri=props.bricks[props.active_brick_index].uri, ) + + +class AddBrick(bpy.types.Operator, Operator): + bl_idname = "bim.add_brick" + bl_label = "Add Brick" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + props = context.scene.BIMBrickProperties + library = None + if props.libraries: + library=tool.Ifc.get().by_id(int(props.libraries)) + core.add_brick( + tool.Ifc, + tool.Brick, + obj=context.active_object, + namespace=props.namespace, + brick_class=props.brick_equipment_class, + library=library, + ) diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index 8839786484..3792d385e9 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -42,6 +42,18 @@ def get_libraries(self, context): return BrickschemaReferencesData.data["libraries"] +def get_namespaces(self, context): + if not BrickschemaData.is_loaded: + BrickschemaData.load() + return BrickschemaData.data["namespaces"] + + +def get_brick_equipment_classes(self, context): + if not BrickschemaData.is_loaded: + BrickschemaData.load() + return BrickschemaData.data["brick_equipment_classes"] + + class Brick(PropertyGroup): name: StringProperty(name="Name") uri: StringProperty(name="URI") @@ -54,3 +66,5 @@ class BIMBrickProperties(PropertyGroup): 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) + namespace: EnumProperty(name="Namespace", items=get_namespaces) + brick_equipment_class: EnumProperty(name="Brick Equipment Class", items=get_brick_equipment_classes) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index 9cfc2c5dfc..1ebebdf9d1 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -45,6 +45,10 @@ class BIM_PT_brickschema(Panel): row.label(text=self.props.active_brick_class) row.operator("bim.close_brick_project", text="", icon="CANCEL") + row = self.layout.row(align=True) + row.prop(self.props, "namespace", text="") + row.prop(self.props, "brick_equipment_class", text="") + row.operator("bim.add_brick", text="", icon="ADD") self.layout.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index") for attribute in BrickschemaData.data["attributes"]: @@ -54,6 +58,9 @@ class BIM_PT_brickschema(Panel): if attribute["is_uri"]: op = row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT") op.item = attribute["value_uri"] + if attribute["is_globalid"]: + op = row.operator("bim.select_global_id", icon="RESTRICT_SELECT_OFF", text="") + op.global_id = attribute["value"] class BIM_PT_ifc_brickschema_references(Panel): diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index a71091a29d..fc03bb3724 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -66,3 +66,10 @@ def assign_brick_reference(ifc, brick, obj=None, library=None, brick_uri=None): if not project: project = brick.add_brickifc_project(brick.get_namespace(brick_uri)) brick.add_brickifc_reference(brick_uri, product, project) + + +def add_brick(ifc, brick, obj=None, namespace=None, brick_class=None, library=None): + product = ifc.get_entity(obj) + brick_uri = brick.add_brick(product, namespace, brick_class) + if library: + brick.run_assign_brick_reference(obj=obj, library=library, brick_uri=brick_uri) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 000ac8fb1f..719314af71 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -43,6 +43,7 @@ class Blender: pass @interface class Brick: + def add_brick(cls, element, namespace, brick_class): pass def add_brick_breadcrumb(cls): pass def add_brickifc_project(cls, namespace): pass def add_brickifc_reference(cls, brick, element, project): pass @@ -59,6 +60,7 @@ class Brick: def import_brick_items(cls, brick_class): pass def load_brick_file(cls, filepath): pass def pop_brick_breadcrumb(cls): pass + def run_assign_brick_reference(cls, obj=None, library=None, brick_uri=None): pass def select_browser_item(cls, item): pass def set_active_brick_class(cls, brick_class): pass diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 3be475e7be..ee5c014d6c 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -33,8 +33,13 @@ except: class Brick(blenderbim.core.tool.Brick): @classmethod - def set_user(cls, user): - bpy.context.scene.BIMOwnerProperties.active_user_id = user.id() + def add_brick(cls, element, namespace, brick_class): + ns = Namespace(namespace) + brick = ns[element.GlobalId] + BrickStore.graph.add((brick, RDF.type, URIRef(brick_class))) + if element.Name: + BrickStore.graph.add((brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal(element.Name))) + return str(brick) @classmethod def add_brick_breadcrumb(cls): @@ -210,6 +215,12 @@ class Brick(blenderbim.core.tool.Brick): bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.remove(last_index) return name + @classmethod + def run_assign_brick_reference(cls, obj=None, library=None, brick_uri=None): + return blenderbim.core.brick.assign_brick_reference( + tool.Ifc, tool.Brick, obj=obj, library=library, brick_uri=brick_uri + ) + @classmethod def select_browser_item(cls, item): name = item.split("#")[-1] diff --git a/src/blenderbim/test/bim/feature/brick.feature b/src/blenderbim/test/bim/feature/brick.feature index 608b50d204..582bc4ca3d 100644 --- a/src/blenderbim/test/bim/feature/brick.feature +++ b/src/blenderbim/test/bim/feature/brick.feature @@ -49,3 +49,28 @@ Scenario: Assign brick reference And I press "bim.convert_brick_project" When I press "bim.assign_brick_reference" Then nothing happens + +Scenario: Add brick - without a brick IFC library + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" + And I press "bim.assign_class" + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And the object "IfcChiller/Cube" is selected + And I set "scene.BIMBrickProperties.namespace" to "http://example.org/digitaltwin#" + When I press "bim.add_brick" + Then nothing happens + +Scenario: Add brick - with a brick IFC library + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcChiller" + And I press "bim.assign_class" + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I press "bim.convert_brick_project" + And the object "IfcChiller/Cube" is selected + And I set "scene.BIMBrickProperties.namespace" to "http://example.org/digitaltwin#" + When I press "bim.add_brick" + Then nothing happens diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py index 2fab16424b..72935640bb 100644 --- a/src/blenderbim/test/core/test_brick.py +++ b/src/blenderbim/test/core/test_brick.py @@ -113,3 +113,16 @@ class TestAssignBrickReference: brick.add_brickifc_project("namespace").should_be_called().will_return("project") brick.add_brickifc_reference("brick", "product", "project").should_be_called() subject.assign_brick_reference(ifc, brick, obj="obj", library="library", brick_uri="brick") + + +class TestAddBrick: + def test_adding_a_brick(self, ifc, brick): + ifc.get_entity("obj").should_be_called().will_return("product") + brick.add_brick("product", "namespace", "brick_class").should_be_called().will_return("brick_uri") + subject.add_brick(ifc, brick, obj="obj", namespace="namespace", brick_class="brick_class", library=None) + + def test_adding_a_brick_an_auto_assigning_it_to_the_ifc_element(self, ifc, brick): + ifc.get_entity("obj").should_be_called().will_return("product") + brick.add_brick("product", "namespace", "brick_class").should_be_called().will_return("brick_uri") + brick.run_assign_brick_reference(obj="obj", library="library", brick_uri="brick_uri").should_be_called() + subject.add_brick(ifc, brick, obj="obj", namespace="namespace", brick_class="brick_class", library="library") diff --git a/src/blenderbim/test/files/spaces.ttl b/src/blenderbim/test/files/spaces.ttl index a4f293add2..dda7dc9c5d 100644 --- a/src/blenderbim/test/files/spaces.ttl +++ b/src/blenderbim/test/files/spaces.ttl @@ -1,24 +1,24 @@ -@prefix : . +@prefix digitaltwin: . @prefix brick: . @prefix unit: . -:site a brick:Site ; +digitaltwin:site a brick:Site ; brick:buildingPrimaryFunction [ brick:value "Library" ] ; - brick:hasPart :bldg . + brick:hasPart digitaltwin:bldg . -:bldg a brick:Building ; - brick:hasPart :floor . +digitaltwin:bldg a brick:Building ; + brick:hasPart digitaltwin:floor . -:floor a brick:Floor ; +digitaltwin:floor a brick:Floor ; brick:area [ brick:value 5000 ; brick:hasUnit unit:M_2 ] ; - brick:hasPart :space, :hvac_zone, :lighting_zone_1, :lighting_zone_2 . + brick:hasPart digitaltwin:space, digitaltwin:hvac_zone, digitaltwin:lighting_zone_1, digitaltwin:lighting_zone_2 . -:space a brick:Space ; +digitaltwin:space a brick:Space ; brick:netArea [ brick:value 500 ; brick:hasUnit unit:M_2 ] ; brick:grossArea [ brick:value 650 ; brick:hasUnit unit:M_2 ] ; - brick:isPartOf :hvac_zone ; - brick:hasPart :lighting_zone_1, :lighting_zone_2 . + brick:isPartOf digitaltwin:hvac_zone ; + brick:hasPart digitaltwin:lighting_zone_1, digitaltwin:lighting_zone_2 . -:hvac_zone a brick:HVAC_Zone . -:lighting_zone_1 a brick:Lighting_Zone . -:lighting_zone_2 a brick:Lighting_Zone . +digitaltwin:hvac_zone a brick:HVAC_Zone . +digitaltwin:lighting_zone_1 a brick:Lighting_Zone . +digitaltwin:lighting_zone_2 a brick:Lighting_Zone . diff --git a/src/blenderbim/test/tool/test_brick.py b/src/blenderbim/test/tool/test_brick.py index 54a35c3fae..b16d61b830 100644 --- a/src/blenderbim/test/tool/test_brick.py +++ b/src/blenderbim/test/tool/test_brick.py @@ -34,6 +34,28 @@ class TestImplementsTool(NewFile): assert isinstance(subject(), blenderbim.core.tool.Brick) +class TestAddBrick(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + element = ifc.createIfcChiller() + element.Name = "Chiller" + element.GlobalId = ifcopenshell.guid.new() + BrickStore.graph = brickschema.Graph() + result = subject.add_brick( + element, "http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment" + ) + uri = f"http://example.org/digitaltwin#{element.GlobalId}" + assert result == uri + assert list( + BrickStore.graph.triples((URIRef(uri), RDF.type, URIRef("https://brickschema.org/schema/Brick#Equipment"))) + ) + assert list( + BrickStore.graph.triples( + (URIRef(uri), URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("Chiller")) + ) + ) + + class TestAddBrickBreadcrumb(NewFile): def test_run(self): subject.set_active_brick_class("brick_class") @@ -119,11 +141,11 @@ class TestClearProject(NewFile): class TestExportBrickAttributes(NewFile): def test_run(self): - assert subject.export_brick_attributes("ex:#floor") == {"Identification": "ex:#floor", "Name": "floor"} + assert subject.export_brick_attributes("http://example.org/digitaltwin#floor") == {"Identification": "http://example.org/digitaltwin#floor", "Name": "floor"} def test_run_ifc2x3(self): tool.Ifc.set(ifcopenshell.file(schema="IFC2X3")) - assert subject.export_brick_attributes("ex:#floor") == {"ItemReference": "ex:#floor", "Name": "floor"} + assert subject.export_brick_attributes("http://example.org/digitaltwin#floor") == {"ItemReference": "http://example.org/digitaltwin#floor", "Name": "floor"} class TestGetBrickPath(NewFile): @@ -151,22 +173,22 @@ class TestGetBrickifcProject(NewFile): class TestGetItemClass(NewFile): def test_run(self): TestLoadBrickFile().test_run() - assert subject.get_item_class("ex:#floor") == "Floor" + assert subject.get_item_class("http://example.org/digitaltwin#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 + reference = ifc.createIfcLibraryReference(Identification="http://example.org/digitaltwin#floor", ReferencedLibrary=library) + assert subject.get_library_brick_reference(library, "http://example.org/digitaltwin#floor") == reference def test_run_ifc2x3(self): ifc = ifcopenshell.file(schema="IFC2X3") tool.Ifc.set(ifc) - reference = ifc.createIfcLibraryReference(ItemReference="ex:#floor") + reference = ifc.createIfcLibraryReference(ItemReference="http://example.org/digitaltwin#floor") library = ifc.createIfcLibraryInformation(LibraryReference=[reference]) - assert subject.get_library_brick_reference(library, "ex:#floor") == reference + assert subject.get_library_brick_reference(library, "http://example.org/digitaltwin#floor") == reference class TestGetNamespace(NewFile): @@ -196,7 +218,7 @@ class TestImportBrickItems(NewFile): assert len(bpy.context.scene.BIMBrickProperties.bricks) == 1 brick = bpy.context.scene.BIMBrickProperties.bricks[0] assert brick.name == "bldg" - assert brick.uri == "ex:#bldg" + assert brick.uri == "http://example.org/digitaltwin#bldg" assert brick.total_items == 0 @@ -224,6 +246,11 @@ class TestPopBrickBreadcrumb(NewFile): assert bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[0].name == "foo" +class TestRunAssignBrickReference(NewFile): + def test_nothing(self): + pass + + class TestSelectBrowserItem(NewFile): def test_run(self): subject.set_active_brick_class("brick_class")