diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index 5f2e12c4d0..364d969c6e 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -20,6 +20,12 @@ import bpy import blenderbim.tool as tool from blenderbim.tool.brick import BrickStore +try: + from rdflib import URIRef, BNode +except: + # See #1860 + print("Warning: brickschema not available.") + def refresh(): BrickschemaData.is_loaded = False @@ -67,15 +73,30 @@ class BrickschemaData: "{uri}", uri ) ) + for row in query: + name = row.get("name").toPython().split("#")[-1] + value = row.get("value") results.append( { - "name": row.get("name").toPython().split("#")[-1], - "value": row.get("value").toPython().split("#")[-1], - "is_uri": "#" in row.get("value").toPython(), - "value_uri": row.get("value").toPython(), + "name": name, + "value": value.toPython().split("#")[-1], + "is_uri": isinstance(value, URIRef), + "value_uri": value.toPython(), + "is_globalid": name == "globalID", } ) + if isinstance(row.get("value"), BNode): + for s, p, o in BrickStore.graph.triples((value, None, None)): + results.append( + { + "name": name + ":" + p.toPython().split("#")[-1], + "value": o.toPython().split("#")[-1], + "is_uri": isinstance(o, URIRef), + "value_uri": o.toPython(), + "is_globalid": p.toPython().split("#")[-1] == "globalID", + } + ) return results @@ -96,7 +117,10 @@ class BrickschemaReferencesData: def libraries(cls): results = [] for library in tool.Ifc.get().by_type("IfcLibraryInformation"): - results.append((str(library.id()), library.Name or "Unnamed", "")) + if tool.Ifc.get_schema() == "IFC2X3": + results.append((str(library.id()), library.Name or "Unnamed", "")) + elif ".ttl" in library.Location: + results.append((str(library.id()), library.Name or "Unnamed", "")) return results @classmethod diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index 4b3ac716a1..a71091a29d 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -60,4 +60,9 @@ def assign_brick_reference(ifc, brick, obj=None, library=None, brick_uri=None): 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) + product = ifc.get_entity(obj) + ifc.run("library.assign_reference", product=product, reference=reference) + project = brick.get_brickifc_project() + if not project: + project = brick.add_brickifc_project(brick.get_namespace(brick_uri)) + brick.add_brickifc_reference(brick_uri, product, project) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index f5ffa6179f..000ac8fb1f 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -44,13 +44,17 @@ class Blender: pass @interface class Brick: def add_brick_breadcrumb(cls): pass + def add_brickifc_project(cls, namespace): pass + def add_brickifc_reference(cls, brick, element, project): 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_brickifc_project(cls): pass def get_item_class(cls, item): pass def get_library_brick_reference(cls, library, brick_uri): pass + def get_namespace(cls, 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 0511a458f9..3be475e7be 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -23,6 +23,9 @@ import blenderbim.tool as tool try: import brickschema + import urllib.parse + from rdflib import Literal, URIRef, Namespace + from rdflib.namespace import RDF except: # See #1860 print("Warning: brickschema not available.") @@ -38,6 +41,39 @@ class Brick(blenderbim.core.tool.Brick): new = bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.add() new.name = bpy.context.scene.BIMBrickProperties.active_brick_class + @classmethod + def add_brickifc_project(cls, namespace): + project = tool.Ifc.get().by_type("IfcProject")[0] + ns = Namespace(namespace) + ns_brickifc = Namespace("https://brickschema.org/extension/ifc#") + BrickStore.graph.bind("brickifc", ns_brickifc) + brick_project = ns[project.GlobalId] + BrickStore.graph.add((brick_project, RDF.type, ns_brickifc["Project"])) + if project.Name: + BrickStore.graph.add( + (brick_project, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal(project.Name)) + ) + BrickStore.graph.add((brick_project, ns_brickifc["projectID"], Literal(project.GlobalId))) + BrickStore.graph.add( + (brick_project, ns_brickifc["fileLocation"], Literal(bpy.context.scene.BIMProperties.ifc_file)) + ) + return str(brick_project) + + @classmethod + def add_brickifc_reference(cls, brick, element, project): + ns_brickifc = Namespace("https://brickschema.org/extension/ifc#") + BrickStore.graph.bind("brickifc", ns_brickifc) + BrickStore.graph.add( + ( + URIRef(brick), + ns_brickifc["hasIFCReference"], + [ + (ns_brickifc["hasProjectReference"], URIRef(project)), + (ns_brickifc["globalID"], Literal(element.GlobalId)), + ], + ) + ) + @classmethod def clear_brick_browser(cls): bpy.context.scene.BIMBrickProperties.bricks.clear() @@ -63,6 +99,25 @@ class Brick(blenderbim.core.tool.Brick): def get_brick_path_name(cls): return os.path.basename(BrickStore.path) + @classmethod + def get_brickifc_project(cls): + project = tool.Ifc.get().by_type("IfcProject")[0] + query = BrickStore.graph.query( + """ + PREFIX brickifc: + SELECT ?proj WHERE { + ?proj a brickifc:Project . + ?proj brickifc:projectID "{project_globalid}" + } + LIMIT 1 + """.replace( + "{project_globalid}", project.GlobalId + ) + ) + results = list(query) + if results: + return results[0][0].toPython() + @classmethod def get_item_class(cls, item): query = BrickStore.graph.query( @@ -90,6 +145,10 @@ class Brick(blenderbim.core.tool.Brick): if reference.Identification == brick_uri: return reference + @classmethod + def get_namespace(cls, uri): + return uri.split("#")[0] + "#" + @classmethod def import_brick_classes(cls, brick_class): query = BrickStore.graph.query( diff --git a/src/blenderbim/test/bim/feature/brick.feature b/src/blenderbim/test/bim/feature/brick.feature index 8f95013404..608b50d204 100644 --- a/src/blenderbim/test/bim/feature/brick.feature +++ b/src/blenderbim/test/bim/feature/brick.feature @@ -30,3 +30,22 @@ Scenario: Close Brick project And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" When I press "bim.close_brick_project" Then nothing happens + +Scenario: Convert brick project + Given an empty IFC project + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + When I press "bim.convert_brick_project" + Then nothing happens + +Scenario: Assign brick reference + Given an empty IFC project + And I press "bim.load_brick_project(filepath='{cwd}/test/files/spaces.ttl')" + And I press "bim.view_brick_item(item='https://brickschema.org/schema/Brick#Chiller')" + 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 the object "IfcChiller/Cube" is selected + And I press "bim.convert_brick_project" + When I press "bim.assign_brick_reference" + Then nothing happens diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py index ee675a7f82..2fab16424b 100644 --- a/src/blenderbim/test/core/test_brick.py +++ b/src/blenderbim/test/core/test_brick.py @@ -92,10 +92,24 @@ class TestAssignBrickReference: 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() + brick.get_brickifc_project().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") 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() + brick.get_brickifc_project().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") + + def test_adding_a_brickifc_project_if_it_doesnt_exist(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() + brick.get_brickifc_project().should_be_called().will_return(None) + brick.get_namespace("brick").should_be_called().will_return("namespace") + 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") diff --git a/src/blenderbim/test/tool/test_brick.py b/src/blenderbim/test/tool/test_brick.py index 7d5540db7f..54a35c3fae 100644 --- a/src/blenderbim/test/tool/test_brick.py +++ b/src/blenderbim/test/tool/test_brick.py @@ -22,6 +22,8 @@ import brickschema import ifcopenshell import blenderbim.core.tool import blenderbim.tool as tool +from rdflib.namespace import RDF +from rdflib import Literal, URIRef, Namespace from test.bim.bootstrap import NewFile from blenderbim.tool.brick import Brick as subject from blenderbim.tool.brick import BrickStore @@ -41,6 +43,62 @@ class TestAddBrickBreadcrumb(NewFile): assert bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[1].name == "brick_class" +class TestAddBrickifcProject(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + project = ifc.createIfcProject(ifcopenshell.guid.new()) + project.Name = "My Project" + BrickStore.graph = brickschema.Graph() + result = subject.add_brickifc_project("http://example.org/digitaltwin#") + assert result == f"http://example.org/digitaltwin#{project.GlobalId}" + brick = URIRef(result) + assert list( + BrickStore.graph.triples((brick, RDF.type, URIRef("https://brickschema.org/extension/ifc#Project"))) + ) + assert list( + BrickStore.graph.triples( + (brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("My Project")) + ) + ) + assert list( + BrickStore.graph.triples( + (brick, URIRef("https://brickschema.org/extension/ifc#projectID"), Literal(project.GlobalId)) + ) + ) + assert list( + BrickStore.graph.triples( + ( + brick, + URIRef("https://brickschema.org/extension/ifc#fileLocation"), + Literal(bpy.context.scene.BIMProperties.ifc_file), + ) + ) + ) + + +class TestAddBrickifcReference(NewFile): + def test_run(self): + TestAddBrickifcProject().test_run() + element = tool.Ifc.get().createIfcChiller(ifcopenshell.guid.new()) + project = URIRef(f"http://example.org/digitaltwin#{tool.Ifc.get().by_type('IfcProject')[0].GlobalId}") + subject.add_brickifc_reference("http://example.org/digitaltwin#foo", element, project) + brick = URIRef("http://example.org/digitaltwin#foo") + bnode = list( + BrickStore.graph.triples((brick, URIRef("https://brickschema.org/extension/ifc#hasIFCReference"), None)) + )[0][2] + assert list( + BrickStore.graph.triples( + (bnode, URIRef("https://brickschema.org/extension/ifc#hasProjectReference"), project) + ) + ) + assert list( + BrickStore.graph.triples( + (bnode, URIRef("https://brickschema.org/extension/ifc#globalID"), Literal(element.GlobalId)) + ) + ) + + class TestClearBrickBrowser(NewFile): def test_run(self): bpy.context.scene.BIMBrickProperties.bricks.add() @@ -81,6 +139,15 @@ class TestGetBrickPathName(NewFile): assert subject.get_brick_path_name() == "spaces.ttl" +class TestGetBrickifcProject(NewFile): + def test_run(self): + TestAddBrickifcProject().test_run() + assert ( + subject.get_brickifc_project() + == f"http://example.org/digitaltwin#{tool.Ifc.get().by_type('IfcProject')[0].GlobalId}" + ) + + class TestGetItemClass(NewFile): def test_run(self): TestLoadBrickFile().test_run() @@ -102,6 +169,11 @@ class TestGetLibraryBrickReference(NewFile): assert subject.get_library_brick_reference(library, "ex:#floor") == reference +class TestGetNamespace(NewFile): + def test_run(self): + assert subject.get_namespace("http://example.org/digitaltwin#globalid") == "http://example.org/digitaltwin#" + + class TestImportBrickClasses(NewFile): def test_run(self): TestLoadBrickFile().test_run()