From 7c06c65df307eb161e227a0b9d3066ae659c84dd Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Wed, 9 Aug 2023 22:02:03 -0700 Subject: [PATCH 01/32] Move Brick "set root" button and change roots on update instead of operator --- .../blenderbim/bim/module/brick/__init__.py | 1 - .../blenderbim/bim/module/brick/operator.py | 14 -------------- .../blenderbim/bim/module/brick/prop.py | 15 +++++++++++++-- src/blenderbim/blenderbim/bim/module/brick/ui.py | 6 +----- src/blenderbim/blenderbim/core/brick.py | 4 +--- 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/__init__.py b/src/blenderbim/blenderbim/bim/module/brick/__init__.py index ca3e084128..ac53d3eb41 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/brick/__init__.py @@ -35,7 +35,6 @@ classes = ( operator.ViewBrickItem, operator.SerializeBrick, operator.AddBrickNamespace, - operator.SetBrickListRoot, operator.RemoveBrickRelation, prop.Brick, prop.BIMBrickProperties, diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 5ca949ffc4..7778fd6eef 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -270,20 +270,6 @@ class AddBrickNamespace(bpy.types.Operator, Operator): core.add_namespace(tool.Brick, alias=alias, uri=uri) -class SetBrickListRoot(bpy.types.Operator, Operator): - bl_idname = "bim.set_brick_list_root" - bl_label = "Set Brick View Type" - bl_options = {"REGISTER", "UNDO"} - split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) - - def _execute(self, context): - if self.split_screen: - root = context.scene.BIMBrickProperties.split_screen_brick_list_root - else: - root = context.scene.BIMBrickProperties.brick_list_root - core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=self.split_screen) - - class RemoveBrickRelation(bpy.types.Operator, Operator): bl_idname = "bim.remove_brick_relation" bl_label = "Remove Relation" diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index 2396b65a59..fed515b044 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -30,6 +30,8 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +import blenderbim.core.brick as core +import blenderbim.tool.brick as tool from blenderbim.tool.brick import BrickStore def update_active_brick_index(self, context): @@ -65,6 +67,15 @@ def get_brick_relations(self, context): return BrickStore.relationships +def update_view(self, context): + root = context.scene.BIMBrickProperties.brick_list_root + core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False) + +def split_screen_update_view(self, context): + root = context.scene.BIMBrickProperties.split_screen_brick_list_root + core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True) + + class Brick(PropertyGroup): name: StringProperty(name="Name") label: StringProperty(name="Label") @@ -79,7 +90,7 @@ class BIMBrickProperties(PropertyGroup): active_brick_index: IntProperty(name="Active Brick Index", update=update_active_brick_index) libraries: EnumProperty(name="Libraries", items=get_libraries) set_list_root_toggled: BoolProperty(name="Set List Root Toggled", default=False) - brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots) + brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots, update=update_view) # namespace manager namespace: EnumProperty(name="Namespace", items=get_namespaces) brick_settings_toggled: BoolProperty(name="Brick Settings Toggled", default=False) @@ -102,4 +113,4 @@ class BIMBrickProperties(PropertyGroup): split_screen_active_brick_index: IntProperty(name="Split Screen Active Brick Index", update=update_active_brick_index) split_screen_active_brick_class: StringProperty(name="Split Screen Active Brick Class") split_screen_brick_breadcrumbs: CollectionProperty(name="Split Screen Brick Breadcrumbs", type=StrProperty) - split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots) \ No newline at end of file + split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index b19e7152d8..20a46e8d45 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -91,6 +91,7 @@ class BIM_PT_brickschema(Panel): row = self.layout.row(align=True) col = row.column() col.alignment = "RIGHT" + row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER") row.prop(data=self.props, property="split_screen_toggled", text="", icon="WINDOW") grid = self.layout.grid_flow(even_columns=True) @@ -99,13 +100,10 @@ class BIM_PT_brickschema(Panel): if len(self.props.brick_breadcrumbs): op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV") op.split_screen = False - row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER") row.label(text=self.props.active_brick_class) if self.props.set_list_root_toggled: row = grid1.row(align=True) - op = row.operator("bim.set_brick_list_root", text="Set View") - op.split_screen = False row.prop(data=self.props, property="brick_list_root", text="") row = grid1.row() @@ -122,8 +120,6 @@ class BIM_PT_brickschema(Panel): if self.props.set_list_root_toggled: row = grid2.row(align=True) - op = row.operator("bim.set_brick_list_root", text="Set View") - op.split_screen = True row.prop(data=self.props, property="split_screen_brick_list_root", text="") row = grid2.row() diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index dace67569d..5fea5abe1a 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -132,9 +132,7 @@ def add_namespace(brick, alias=None, uri=None): def set_brick_list_root(brick, brick_root=None, split_screen=False): - brick.clear_brick_browser(split_screen=split_screen) - brick.import_brick_classes(brick_root, split_screen=split_screen) - brick.set_active_brick_class(brick_root, split_screen=split_screen) + brick.run_view_brick_class(brick_class=brick_root, split_screen=split_screen) brick.clear_breadcrumbs(split_screen=split_screen) From 1982069b698b2b9c02deda9aa0c5cdc3305b059f Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 12:29:12 -0700 Subject: [PATCH 02/32] Add description to Brick operators --- .../blenderbim/bim/module/brick/operator.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 7778fd6eef..64358b12a6 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -37,6 +37,7 @@ class LoadBrickProject(bpy.types.Operator, Operator): bl_idname = "bim.load_brick_project" bl_label = "Load Brickschema Project" bl_options = {"REGISTER", "UNDO"} + bl_description = "Load in a Brick project from a file" filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"}) @@ -56,6 +57,7 @@ class ViewBrickClass(bpy.types.Operator, Operator): bl_idname = "bim.view_brick_class" bl_label = "View Brick Class" bl_options = {"REGISTER", "UNDO"} + bl_description = "Inspect the subclasses of this class" brick_class: bpy.props.StringProperty(name="Brick Class") split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) @@ -67,6 +69,7 @@ class ViewBrickItem(bpy.types.Operator, Operator): bl_idname = "bim.view_brick_item" bl_label = "View Brick Item" bl_options = {"REGISTER", "UNDO"} + bl_description = "Inspect this entity in the viewer" item: bpy.props.StringProperty(name="Brick Item") split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) @@ -78,6 +81,7 @@ class RewindBrickClass(bpy.types.Operator, Operator): bl_idname = "bim.rewind_brick_class" bl_label = "Rewind Brick Class" bl_options = {"REGISTER", "UNDO"} + bl_description = "Go back to the previous list view" split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) def _execute(self, context): @@ -88,6 +92,7 @@ class CloseBrickProject(bpy.types.Operator, Operator): bl_idname = "bim.close_brick_project" bl_label = "Close Brick Project" bl_options = {"REGISTER", "UNDO"} + bl_description = "Close the Brick project" def _execute(self, context): core.close_brick_project(tool.Brick) @@ -122,6 +127,7 @@ class AddBrick(bpy.types.Operator, Operator): bl_idname = "bim.add_brick" bl_label = "Add Brick" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create the Brick entity" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -143,6 +149,7 @@ class AddBrickRelation(bpy.types.Operator, Operator): bl_idname = "bim.add_brick_relation" bl_label = "Add Brick Relation" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create the Brick relationship" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -178,6 +185,7 @@ class NewBrickFile(bpy.types.Operator): bl_idname = "bim.new_brick_file" bl_label = "New Brick File" bl_options = {"REGISTER", "UNDO"} + bl_description = "Create a Brick project from scratch" def execute(self, context): IfcStore.begin_transaction(self) @@ -210,6 +218,7 @@ class RefreshBrickViewer(bpy.types.Operator, Operator): bl_idname = "bim.refresh_brick_viewer" bl_label = "Refresh Brick Viewer" bl_options = {"REGISTER", "UNDO"} + bl_description = "Refresh the list view" split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) def _execute(self, context): @@ -220,6 +229,7 @@ class RemoveBrick(bpy.types.Operator, Operator): bl_idname = "bim.remove_brick" bl_label = "Remove Brick" bl_options = {"REGISTER", "UNDO"} + bl_description = "Delete this entity" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -262,6 +272,7 @@ class SerializeBrick(bpy.types.Operator): class AddBrickNamespace(bpy.types.Operator, Operator): bl_idname = "bim.add_brick_namespace" bl_label = "Add Brick Namespace" + bl_description = "Bind a new namespace to the Brick project" def _execute(self, context): props = context.scene.BIMBrickProperties @@ -274,6 +285,7 @@ class RemoveBrickRelation(bpy.types.Operator, Operator): bl_idname = "bim.remove_brick_relation" bl_label = "Remove Relation" bl_options = {"REGISTER", "UNDO"} + bl_description = "Delete this relationship" predicate: bpy.props.StringProperty(name="Relation") object: bpy.props.StringProperty(name="Object") From 08a5d444d4406271025c57a979c701ac4119a318 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 12:32:32 -0700 Subject: [PATCH 03/32] Remove another needless BrickStore.graph.triples check As before, this returns a generator type which always evaluates true, even if it is empty. --- src/blenderbim/blenderbim/tool/brick.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 8f308d1590..2f4efd84dc 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -340,10 +340,9 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def remove_brick(cls, brick_uri): - if BrickStore.graph.triples((URIRef(brick_uri), None, None)): - with BrickStore.new_changeset() as cs: - for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)): - cs.remove(triple) + with BrickStore.new_changeset() as cs: + for triple in BrickStore.graph.triples((URIRef(brick_uri), None, None)): + cs.remove(triple) @classmethod def run_assign_brick_reference(cls, element=None, library=None, brick_uri=None): From 60a74335735f59455a37bb9947ebc59ae2518462 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 12:44:25 -0700 Subject: [PATCH 04/32] Clear Brick breadcrumbs on close project --- src/blenderbim/blenderbim/core/brick.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index 5fea5abe1a..821348a3b8 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -51,6 +51,8 @@ def close_brick_project(brick): brick.clear_project() brick.clear_brick_browser() brick.clear_brick_browser(split_screen=True) + brick.clear_breadcrumbs() + brick.clear_breadcrumbs(split_screen=True) def convert_brick_project(ifc, brick): From 429da7c6f3143cff2a9f528b463d26b64b5aae9d Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 12:51:46 -0700 Subject: [PATCH 05/32] Filter more (all) Brick ontology namespaces I'm not sure if this method of filtering out the entire domain will cause problems. This could use investigation. --- src/blenderbim/blenderbim/tool/brick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 2f4efd84dc..c0b8638471 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -426,7 +426,7 @@ class BrickStore: @classmethod def load_namespaces(cls): BrickStore.namespaces = [] - keyword_filter = ["brickschema.org", "schema.org", "w3.org", "purl.org", "rdfs.org", "qudt.org", "ashrae.org"] + keyword_filter = ["brickschema.org", "schema.org", "w3.org", "purl.org", "rdfs.org", "qudt.org", "ashrae.org", "usefulinc.com", "xmlns.com", "opengis.net"] for alias, uri in BrickStore.graph.namespaces(): ignore_namespace = False for keyword in keyword_filter: From d58af0bfa576475bed0254c0864b259a8dfb22b5 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 13:00:02 -0700 Subject: [PATCH 06/32] Filter Brick class if deprecated --- src/blenderbim/blenderbim/tool/brick.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index c0b8638471..8758cc49d1 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -443,8 +443,12 @@ class BrickStore: """ PREFIX brick: PREFIX rdfs: + PREFIX owl: SELECT ?class WHERE { ?class rdfs:subClassOf* brick:{root_class} . + FILTER NOT EXISTS { + ?class owl:deprecated true . + } } """.replace( "{root_class}", root_class From 926970b256189589bd5d68afaef86993655e5915 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 13:32:45 -0700 Subject: [PATCH 07/32] Polish Brick UI and add "last saved" label --- .../blenderbim/bim/module/brick/ui.py | 32 +++++++++++-------- src/blenderbim/blenderbim/tool/brick.py | 11 +++++++ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index 20a46e8d45..a32ec17bf9 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -42,9 +42,17 @@ class BIM_PT_brickschema(Panel): row.operator("bim.load_brick_project", text="Load Project") return + row = self.layout.row(align=True) if BrickStore.path: - row = self.layout.row(align=True) row.label(text=BrickStore.path, icon="FILEBROWSER") + else: + row.label(text="No file", icon="FILEBROWSER") + + row = self.layout.row(align=True) + if BrickStore.last_saved: + row.label(text=BrickStore.last_saved, icon="TIME") + else: + row.label(text="Not saved", icon="TIME") row = self.layout.row(align=True) op = row.operator("bim.serialize_brick", icon="EXPORT", text="Save") @@ -55,7 +63,7 @@ class BIM_PT_brickschema(Panel): row = self.layout.row(align=True) row.prop(data=self.props, property="brick_settings_toggled", text="", icon="PREFERENCES") - + if self.props.brick_settings_toggled: box = self.layout.box() row = box.row(align=True) @@ -86,43 +94,43 @@ class BIM_PT_brickschema(Panel): row.prop(data=self.props, property="new_brick_label", text="") prop_with_search(row, self.props, "brick_entity_class", text="") row.operator("bim.add_brick", text="", icon="ADD") - # row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH") row = self.layout.row(align=True) col = row.column() col.alignment = "RIGHT" row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER") row.prop(data=self.props, property="split_screen_toggled", text="", icon="WINDOW") + row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH") grid = self.layout.grid_flow(even_columns=True) - grid1 = grid.column(align=True) - row = grid1.row(align=True) + grid_left = grid.column(align=True) + row = grid_left.row(align=True) if len(self.props.brick_breadcrumbs): op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV") op.split_screen = False row.label(text=self.props.active_brick_class) if self.props.set_list_root_toggled: - row = grid1.row(align=True) + row = grid_left.row(align=True) row.prop(data=self.props, property="brick_list_root", text="") - row = grid1.row() + row = grid_left.row() BIM_UL_bricks.split_screen = False row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index") if self.props.split_screen_toggled: - grid2 = grid.column(align=True) - row = grid2.row(align=True) + grid_right = grid.column(align=True) + row = grid_right.row(align=True) if len(self.props.split_screen_brick_breadcrumbs): op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV") op.split_screen = True row.label(text=self.props.split_screen_active_brick_class) if self.props.set_list_root_toggled: - row = grid2.row(align=True) + row = grid_right.row(align=True) row.prop(data=self.props, property="split_screen_brick_list_root", text="") - row = grid2.row() + row = grid_right.row() BIM_UL_bricks.split_screen = True row.template_list("BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index") @@ -161,12 +169,10 @@ class BIM_PT_brickschema(Panel): prop_with_search(row, self.props, "new_brick_relation_type", text="") row.prop(data=self.props, property="new_brick_relation_object", text="") row.operator("bim.add_brick_relation", text="", icon="ADD") - if self.props.brick_create_relations_toggled and self.props.add_relation_failed: row = self.layout.row(align=True) row.label(text="Failed to find this entity!", icon="ERROR") - for relation in BrickschemaData.data["active_relations"]: row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 8758cc49d1..0afac1de09 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -23,6 +23,7 @@ import ifcopenshell.util.brick import blenderbim.core.tool import blenderbim.tool as tool from contextlib import contextmanager +import datetime try: import brickschema @@ -308,6 +309,7 @@ class Brick(blenderbim.core.tool.Brick): with BrickStore.graph.new_changeset("PROJECT") as cs: cs.load_file(filepath) BrickStore.path = filepath + cls.set_last_saved() BrickStore.load_namespaces() BrickStore.load_entity_classes() BrickStore.load_relationships() @@ -378,6 +380,7 @@ class Brick(blenderbim.core.tool.Brick): @classmethod def serialize_brick(cls): BrickStore.get_project().serialize(destination=BrickStore.path, format="turtle") + cls.set_last_saved() @classmethod def add_namespace(cls, alias, uri): @@ -391,11 +394,18 @@ class Brick(blenderbim.core.tool.Brick): else: bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear() + @classmethod + def set_last_saved(cls): + save = os.path.getmtime(BrickStore.path) + save = datetime.datetime.fromtimestamp(save) + BrickStore.last_saved = f"{save.year}-{save.month}-{save.day} {save.hour}:{save.minute}" + class BrickStore: schema = None # this is now a os path path = None # file path if the project was loaded in graph = None # this is the VersionedGraphCollection with 2 arbitrarily named graphs: "schema" and "project" # "SCHEMA" holds the Brick.ttl metadata; "PROJECT" holds all the authored entities + last_saved = None history = [] future = [] current_changesets = 0 @@ -415,6 +425,7 @@ class BrickStore: BrickStore.schema = None BrickStore.graph = None BrickStore.path = None + BrickStore.last_saved = None BrickStore.namespaces = [] BrickStore.entity_classes = {} BrickStore.relationships = [] From 0b151ea559e0dc0cca44b2d984f2fc1a00f7584a Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Thu, 10 Aug 2023 14:00:56 -0700 Subject: [PATCH 08/32] Second level query for Brick equipment+point roots --- src/blenderbim/blenderbim/tool/brick.py | 36 ++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 0afac1de09..f16e959c4a 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -310,6 +310,7 @@ class Brick(blenderbim.core.tool.Brick): cs.load_file(filepath) BrickStore.path = filepath cls.set_last_saved() + BrickStore.load_sub_roots() BrickStore.load_namespaces() BrickStore.load_entity_classes() BrickStore.load_relationships() @@ -323,6 +324,7 @@ class Brick(blenderbim.core.tool.Brick): with BrickStore.graph.new_changeset("SCHEMA") as cs: cs.load_file(BrickStore.schema) BrickStore.graph.bind("digitaltwin", Namespace("https://example.org/digitaltwin#")) + BrickStore.load_sub_roots() BrickStore.load_namespaces() BrickStore.load_entity_classes() BrickStore.load_relationships() @@ -411,12 +413,7 @@ class BrickStore: current_changesets = 0 history_size = 64 namespaces = [] - root_classes = ["Equipment", - "Electrical_Equipment", "Fire_Safety_Equipment", "HVAC_Equipment", "Lighting_Equipment", "Meter", - "Location", - "System", - "Point", - "Alarm", "Command", "Parameter", "Sensor", "Setpoint", "Status"] + root_classes = ["Equipment", "Location", "System", "Point"] entity_classes = {} relationships = [] @@ -427,6 +424,7 @@ class BrickStore: BrickStore.path = None BrickStore.last_saved = None BrickStore.namespaces = [] + BrickStore.root_classes = ["Equipment", "Location", "System", "Point"] BrickStore.entity_classes = {} BrickStore.relationships = [] @@ -434,6 +432,32 @@ class BrickStore: def get_project(cls): return BrickStore.graph.graph_at(graph="PROJECT") + @classmethod + def load_sub_roots(cls): + query = BrickStore.graph.query( + """ + PREFIX brick: + PREFIX rdfs: + SELECT ?subRoot ?subClasses WHERE { + { + SELECT ?subRoot (COUNT(?subClass) as ?subClasses) WHERE { + { + ?subRoot rdfs:subClassOf brick:Equipment . + } UNION { + ?subRoot rdfs:subClassOf brick:Point . + } + ?subClass rdfs:subClassOf* ?subRoot . + } + GROUP BY ?subRoot + } + FILTER(?subClasses > 3) + } + """ + ) + for row in query: + sub_root = row.get("subRoot").toPython().split("#")[-1] + BrickStore.root_classes.append(sub_root) + @classmethod def load_namespaces(cls): BrickStore.namespaces = [] From d575a43aee0c5651b2299814a2c3664b3c18dcf3 Mon Sep 17 00:00:00 2001 From: rileywong311 Date: Fri, 11 Aug 2023 13:01:25 -0700 Subject: [PATCH 09/32] Make refresh Brick viewer refresh both screens --- src/blenderbim/blenderbim/bim/module/brick/operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/brick/operator.py b/src/blenderbim/blenderbim/bim/module/brick/operator.py index 64358b12a6..01a66418b9 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/operator.py +++ b/src/blenderbim/blenderbim/bim/module/brick/operator.py @@ -219,10 +219,10 @@ class RefreshBrickViewer(bpy.types.Operator, Operator): bl_label = "Refresh Brick Viewer" bl_options = {"REGISTER", "UNDO"} bl_description = "Refresh the list view" - split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"}) def _execute(self, context): - core.refresh_brick_viewer(tool.Brick, split_screen=self.split_screen) + core.refresh_brick_viewer(tool.Brick) + core.refresh_brick_viewer(tool.Brick, split_screen=True) class RemoveBrick(bpy.types.Operator, Operator): From c45b84c850fe9903320058e9b6ab16180720f291 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 16:16:36 +1000 Subject: [PATCH 10/32] Fix #3577. Don't force uppercase in schedules. --- src/blenderbim/blenderbim/bim/module/drawing/scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py index db501355d3..d7338fd382 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py @@ -351,7 +351,7 @@ class Scheduler: wrap_text: if True, text will be wrapped to fit in cell cell_width: width of cell, used for wrapping text """ - text_lines = [str(p).upper() for p in p_tags] + text_lines = [str(p) for p in p_tags] box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment) text_params = { "font-size": font_size, From b0a5a570a9164ad0027f19e756499e70d92ff725 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 16:40:43 +1000 Subject: [PATCH 11/32] IfcCSV now uses the new filter query system. --- .../blenderbim/bim/module/csv/operator.py | 15 ++++++++------- src/ifccsv/ifccsv.py | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index 71dabc5b21..152c671f39 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -129,9 +129,8 @@ class ExportCsvAttributes(bpy.types.Operator): class ExportIfcCsv(bpy.types.Operator): bl_idname = "bim.export_ifccsv" bl_label = "Export IFC" - #filename_ext = ".csv" + filename_ext = ".csv" filepath: bpy.props.StringProperty(subtype="FILE_PATH") - def invoke(self, context, event): props = context.scene.CsvProperties @@ -149,8 +148,7 @@ class ExportIfcCsv(bpy.types.Operator): ifc_file = IfcStore.get_file() else: ifc_file = ifcopenshell.open(props.csv_ifc_file) - selector = ifcopenshell.util.selector.Selector() - results = selector.parse(ifc_file, props.ifc_selector) + results = ifcopenshell.util.selector.filter_elements(ifc_file, props.ifc_selector) ifc_csv = ifccsv.IfcCsv() attributes = [a.name for a in props.csv_attributes] sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter @@ -196,9 +194,12 @@ class EyedropIfcCsv(bpy.types.Operator): global_ids = [] self.file = IfcStore.get_file() for obj in context.selected_objects: - if hasattr(obj, "BIMObjectProperties") and obj.BIMObjectProperties.ifc_definition_id: - global_ids.append("#" + self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId) - context.scene.CsvProperties.ifc_selector = "|".join(global_ids) + element = tool.Ifc.get_entity(obj) + if element: + global_id = getattr(element, "GlobalId", None) + if global_id: + global_ids.append(global_id) + context.scene.CsvProperties.ifc_selector = ",".join(global_ids) return {"FINISHED"} diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index 6497fe3671..08a4e3da58 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -302,7 +302,7 @@ if __name__ == "__main__": parser.add_argument("-i", "--ifc", type=str, required=True, help="The IFC file") parser.add_argument("-s", "--spreadsheet", type=str, default="data.csv", help="The spreadsheet file") parser.add_argument("-f", "--format", type=str, default="csv", help="The format, chosen from csv, ods, or xlsx") - parser.add_argument("-q", "--query", type=str, default="", help='Specify a IFC query selector, such as ".IfcWall"') + parser.add_argument("-q", "--query", type=str, default="", help='Specify a IFC query selector, such as "IfcWall"') parser.add_argument( "-a", "--arguments", @@ -315,7 +315,7 @@ if __name__ == "__main__": if args.export: ifc_file = ifcopenshell.open(args.ifc) - results = ifcopenshell.util.selector.Selector.parse(ifc_file, args.query) + results = ifcopenshell.util.selector.filter_elements(ifc_file, args.query) ifc_csv = IfcCsv() ifc_csv.export(ifc_file, results, args.arguments or [], output=args.spreadsheet, format=args.format) elif getattr(args, "import"): From 7afd358c3cd8eab33da380caa0c0b8c40f3c7fd4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 16:41:57 +1000 Subject: [PATCH 12/32] Fix #3556. Bug where quantities could not be imported using IfcCSV. Also make IfcCSV support any qset name, not just "Qto" prefixed names. --- src/ifccsv/ifccsv.py | 46 ++++++++++---------------------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index 08a4e3da58..d69f890df8 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -58,51 +58,25 @@ class IfcAttributeSetter: return element if "." not in key: return element - if key[0:3] == "Qto": - qto_name, prop = key.split(".", 1) - qto = IfcAttributeSetter.get_element_qto(element, qto_name) - if qto: - IfcAttributeSetter.set_qto_property(qto, prop, value) - return element pset_name, prop = key.split(".", 1) - pset = IfcAttributeSetter.get_element_pset(element, pset_name) + pset = ifcopenshell.util.element.get_pset(element, pset_name, should_inherit=True) if pset: - IfcAttributeSetter.set_pset_property(ifc_file, pset, prop, value) - return element + pset = ifc_file.by_id(pset["id"]) + if pset.is_a("IfcElementQuantity"): + IfcAttributeSetter.set_qto_property(pset, prop, value) + else: + IfcAttributeSetter.set_pset_property(ifc_file, pset, prop, value) return element - @staticmethod - def get_element_qto(element, name): - for relationship in element.IsDefinedBy: - if ( - relationship.is_a("IfcRelDefinesByProperties") - and relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity") - and relationship.RelatingPropertyDefinition.Name == name - ): - return relationship.RelatingPropertyDefinition - @staticmethod def set_qto_property(qto, name, value): for prop in qto.Quantities: if prop.Name != name: continue - setattr(prop, prop.is_a()[len("IfcQuantity") :] + "Value", value) - - @staticmethod - def get_element_pset(element, name): - if element.is_a("IfcTypeObject"): - if element.HasPropertySets: - for pset in element.HasPropertySets: - if pset.is_a("IfcPropertySet") and pset.Name == name: - return pset - else: - for relationship in element.IsDefinedBy: - if ( - relationship.is_a("IfcRelDefinesByProperties") - and relationship.RelatingPropertyDefinition.is_a("IfcPropertySet") - and relationship.RelatingPropertyDefinition.Name == name - ): - return relationship.RelatingPropertyDefinition + try: + setattr(prop, prop.is_a()[len("IfcQuantity") :] + "Value", float(value)) + except: + pass @staticmethod def set_pset_property(ifc_file, pset, name, value): From 694dbeb96c9fd7e2e711d02b2112e3b4cce4b10a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 17:06:22 +1000 Subject: [PATCH 13/32] Fix #3576. Explicit drawing annotations are now immune to "include / exclude" filters. --- src/blenderbim/blenderbim/bim/module/drawing/ui.py | 2 +- src/blenderbim/blenderbim/tool/drawing.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index 25155f5096..e96eb9defe 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -314,7 +314,7 @@ class BIM_PT_references(Panel): if not self.props.is_editing_references: row = self.layout.row(align=True) - row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="LONGDISPLAY") + row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="OBJECT_HIDDEN") row.operator("bim.load_references", text="", icon="IMPORT") return diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 9ae057a57b..903794d4fa 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -1511,8 +1511,8 @@ class Drawing(blenderbim.core.tool.Drawing): else: base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement")) elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"} - annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) - elements.update(annotations) + annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) + elements.update(annotations) exclude = pset.get("Exclude", None) if exclude: From 04fa59784481adba390eddedda82dc3934a71c5f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 17:09:48 +1000 Subject: [PATCH 14/32] Fix #3564. Bug where IfcBuiltSystems didn't show up due to missing icon in IFC4X3. --- src/blenderbim/blenderbim/bim/module/system/ui.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index b0b411e23a..5b60c151e7 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -123,6 +123,7 @@ class BIM_PT_object_systems(Panel): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } for system in ObjectSystemData.data["systems"]: @@ -284,6 +285,7 @@ class BIM_UL_systems(UIList): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } if item: @@ -316,6 +318,7 @@ class BIM_UL_object_systems(UIList): "IfcDistributionSystem": "NETWORK_DRIVE", "IfcDistributionCircuit": "DRIVER", "IfcBuildingSystem": "MOD_BUILD", + "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } if item: From db7be640d4a4ab23a4b8170ca283988322a043ac Mon Sep 17 00:00:00 2001 From: Massimo Fabbro <79401028+maxfb87@users.noreply.github.com> Date: Sat, 12 Aug 2023 11:03:58 +0200 Subject: [PATCH 15/32] Fix #3569 Variable name override caused the bug (#3575) --- src/blenderbim/blenderbim/bim/module/model/space.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py index fb52923279..2af8ae020c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/space.py +++ b/src/blenderbim/blenderbim/bim/module/model/space.py @@ -95,19 +95,19 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True) for obj in bpy.context.visible_objects: - element = tool.Ifc.get_entity(obj) + visible_element = tool.Ifc.get_entity(obj) if ( - not element + not visible_element or obj.type != "MESH" - or not self.is_bounding_class(element) + or not self.is_bounding_class(visible_element) or not tool.Drawing.is_intersecting_plane(obj, self.cut_point, self.cut_normal) ): continue old_mesh = None - if element.HasOpenings: - new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, element)) + if visible_element.HasOpenings: + new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, visible_element)) old_mesh = obj.data obj.data = new_mesh From b2eddf55d2ff18a122fa301d2bc2df8a819dcea9 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sat, 12 Aug 2023 12:58:16 +0100 Subject: [PATCH 16/32] Fix contracting/expanding cost schedule tree and add tests to avoid proof changes --- .../blenderbim/bim/module/cost/__init__.py | 83 ++++++++++--------- .../blenderbim/bim/module/cost/operator.py | 11 +++ .../blenderbim/bim/module/cost/ui.py | 4 +- src/blenderbim/blenderbim/tool/cost.py | 4 +- src/blenderbim/test/bim/feature/cost.feature | 48 +++++++++++ 5 files changed, 105 insertions(+), 45 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 8a9ef49798..d279ad317c 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -20,64 +20,65 @@ import bpy from . import ui, prop, operator classes = ( + operator.AddCostColumn, + operator.AddCostItem, + operator.AddCostItemQuantity, operator.AddCostSchedule, - operator.RemoveCostSchedule, - operator.EditCostSchedule, + operator.AddCostValue, + operator.AddCurrency, + operator.AddSummaryCostItem, + operator.AssignCostItemQuantity, + operator.AssignCostItemType, + operator.AssignCostValue, + operator.CalculateCostItemResourceValue, + operator.ChangeParentCostItem, + operator.ClearCostItemAssignments, + operator.ContractCostItem, + operator.ContractCostItemRate, + operator.ContractCostItems, + operator.CopyCostItem, + operator.CopyCostItemValues, + operator.DisableEditingCostItem, + operator.DisableEditingCostItemQuantity, + operator.DisableEditingCostItemValue, + operator.DisableEditingCostSchedule, operator.EditCostItem, operator.EditCostItemQuantity, operator.EditCostItemValue, operator.EditCostItemValueFormula, - operator.EnableEditingCostSchedule, - operator.EnableEditingCostItems, + operator.EditCostSchedule, operator.EnableEditingCostItem, - operator.ExportCostSchedules, - operator.ExpandCostItems, operator.EnableEditingCostItemQuantities, operator.EnableEditingCostItemQuantity, - operator.EnableEditingCostItemValues, + operator.EnableEditingCostItems, operator.EnableEditingCostItemValue, operator.EnableEditingCostItemValueFormula, - operator.DisableEditingCostItem, - operator.DisableEditingCostSchedule, - operator.DisableEditingCostItemQuantity, - operator.DisableEditingCostItemValue, - operator.AddCostColumn, - operator.RemoveCostColumn, - operator.AddCostItem, - operator.AddSummaryCostItem, + operator.EnableEditingCostItemValues, + operator.EnableEditingCostSchedule, operator.ExpandCostItem, - operator.ContractCostItem, + operator.ExpandCostItemRate, + operator.ExpandCostItems, + operator.ExportCostSchedules, + operator.HighlightProductCostItem, + operator.ImportCostScheduleCsv, + operator.LoadCostItemElementQuantities, + operator.LoadCostItemQuantities, + operator.LoadCostItemResourceQuantities, + operator.LoadCostItemTaskQuantities, + operator.LoadCostItemTypes, + operator.LoadProductCostItems, + operator.LoadScheduleOfRates, + operator.RemoveCostColumn, operator.RemoveCostItem, - operator.AssignCostItemType, - operator.UnassignCostItemType, - operator.AssignCostItemQuantity, - operator.UnassignCostItemQuantity, - operator.AddCostItemQuantity, operator.RemoveCostItemQuantity, - operator.AddCostValue, operator.RemoveCostItemValue, - operator.CopyCostItemValues, + operator.RemoveCostSchedule, + operator.ReorderCostItem, operator.SelectCostItemProducts, operator.SelectCostScheduleProducts, - operator.ImportCostScheduleCsv, - operator.LoadCostItemQuantities, - operator.LoadCostItemTypes, - operator.AssignCostValue, - operator.LoadScheduleOfRates, - operator.ExpandCostItemRate, - operator.ContractCostItemRate, - operator.CalculateCostItemResourceValue, - operator.ClearCostItemAssignments, - operator.HighlightProductCostItem, - operator.LoadProductCostItems, - operator.ReorderCostItem, operator.SelectUnassignedProducts, - operator.LoadCostItemElementQuantities, - operator.LoadCostItemTaskQuantities, - operator.LoadCostItemResourceQuantities, - operator.ChangeParentCostItem, - operator.CopyCostItem, - operator.AddCurrency, + operator.UnassignCostItemQuantity, + operator.UnassignCostItemType, prop.CostItem, prop.CostItemQuantity, prop.CostItemType, diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 02c7b2e9a8..91bf921347 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -156,6 +156,17 @@ class ContractCostItem(bpy.types.Operator, tool.Ifc.Operator): core.contract_cost_item(tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item)) +class ContractCostItems(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.contract_cost_items" + bl_label = "Contract Cost Item" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Collapse cost item tree" + cost_item: bpy.props.IntProperty() + + def _execute(self, context): + core.contract_cost_items(tool.Cost) + + class RemoveCostItem(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_cost_item" bl_label = "Remove Cost Item" diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 832e69c871..3c5b6e49a5 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -139,8 +139,8 @@ class BIM_PT_cost_schedules(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" row.operator("bim.add_summary_cost_item", text="Add Summary Cost", icon="ADD") - row.operator("bim.expand_all_tasks", text="Expand All") - row.operator("bim.contract_all_tasks", text="Contract All") + row.operator("bim.expand_cost_items", text="Expand All") + row.operator("bim.contract_cost_items", text="Contract All") row = self.layout.row(align=True) row.alignment = "RIGHT" if self.props.cost_items and self.props.active_cost_item_index < len(self.props.cost_items): diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index f6c83b051c..698625bc33 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -96,11 +96,11 @@ class Cost(blenderbim.core.tool.Cost): props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod - def contract_cost_item(cls, cost_item_id): + def contract_cost_item(cls, cost_item): props = bpy.context.scene.BIMCostProperties if not hasattr(cls, "contracted_cost_items"): cls.contracted_cost_items = json.loads(props.contracted_cost_items) - cls.contracted_cost_items.append(cost_item_id) + cls.contracted_cost_items.append(cost_item.id()) props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod diff --git a/src/blenderbim/test/bim/feature/cost.feature b/src/blenderbim/test/bim/feature/cost.feature index 1098c96716..aefe3e5ff9 100644 --- a/src/blenderbim/test/bim/feature/cost.feature +++ b/src/blenderbim/test/bim/feature/cost.feature @@ -3,6 +3,7 @@ Feature: Cost Scenario: Add cost schedule Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" When I press "bim.add_cost_schedule" Then nothing happens @@ -111,6 +112,53 @@ Scenario: Add cost item When I press "bim.add_cost_item(cost_item={cost_item})" Then nothing happens +Scenario: Contract Cost Item + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + When I press "bim.contract_cost_item(cost_item={cost_item})" + Then nothing happens + +Scenario: Contract All Cost Items + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + When I press "bim.contract_cost_items" + Then nothing happens + +Scenario: Expand Cost Item + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + And I press "bim.contract_cost_item(cost_item={cost_item})" + When I press "bim.expand_cost_item(cost_item={cost_item})" + Then nothing happens + + +Scenario: Expand All Cost Items + Given an empty IFC project + And I set "scene.BIMCostProperties.cost_schedule_predefined_types" to "COSTPLAN" + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item()" + And I press "bim.add_cost_item(cost_item={cost_item})" + When I press "bim.expand_cost_items" + Then nothing happens + + Scenario: Enable editing cost item quantities Given an empty IFC project And I press "bim.add_cost_schedule" From 5e98dee46fd127a068ded605cffbe65424714270 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 12 Aug 2023 22:25:20 +1000 Subject: [PATCH 17/32] Fix #3554. Support editing IfcIndexedPolyCurve profiles where IfcLineIndex has >2 indices. --- src/blenderbim/blenderbim/tool/model.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index a4e9295d72..904f7019ed 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -358,7 +358,7 @@ class Model(blenderbim.core.tool.Model): is_closed = False if curve.Segments: for segment in curve.Segments: - if len(segment[0]) == 3: # IfcArcIndex + if segment.is_a("IfcArcIndex"): is_arc = True local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1]) global_point = position @ Vector(local_point).to_3d() @@ -368,12 +368,13 @@ class Model(blenderbim.core.tool.Model): cls.vertices.append(global_point) cls.arcs.append([len(cls.vertices) - 2, len(cls.vertices) - 1]) else: - local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1]) - global_point = position @ Vector(local_point).to_3d() - cls.vertices.append(global_point) - if is_arc: - cls.arcs[-1].append(len(cls.vertices) - 1) - is_arc = False + for segment_index in segment[0][0:-1]: + local_point = cls.convert_unit_to_si(curve.Points.CoordList[segment_index - 1]) + global_point = position @ Vector(local_point).to_3d() + cls.vertices.append(global_point) + if is_arc: + cls.arcs[-1].append(len(cls.vertices) - 1) + is_arc = False if curve.Segments[0][0][0] == curve.Segments[-1][0][-1]: is_closed = True From 703f0a6b70c1f3084a683066a9635cfe44a0660e Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sat, 12 Aug 2023 14:57:01 +0100 Subject: [PATCH 18/32] fix orphaned data when unassigning element quantities from cost item control --- .../ifcopenshell/api/cost/unassign_cost_item_quantity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py index e4b48bafa0..967cec4162 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py @@ -98,4 +98,7 @@ class Usecase: count = 0 for rel in self.settings["cost_item"].Controls: count += len(rel.RelatedObjects) - quantity[3] = count + if count: + quantity[3] = count + else: + self.file.remove(quantity) From 07b5c4918be53354901dc98deb80b43ecb5c79f9 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sat, 12 Aug 2023 15:17:32 +0100 Subject: [PATCH 19/32] fix api logic where only quantities of the same type can be assignmed to a cost control --- .../api/cost/assign_cost_item_quantity.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py index 754b8b02ca..eee40ed06e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py @@ -77,19 +77,30 @@ class Usecase: cost_item=item, products=[slab], prop_name="NetVolume") """ self.file = file - self.settings = {"cost_item": cost_item, "products": products or [], "prop_name": prop_name} + self.settings = { + "cost_item": cost_item, + "products": products or [], + "prop_name": prop_name, + } def execute(self): if self.settings["prop_name"]: self.quantities = set(self.settings["cost_item"].CostQuantities or []) + print(self.settings["cost_item"].CostQuantities) for product in self.settings["products"]: - ifcopenshell.api.run( - "control.assign_control", - self.file, - related_object=product, - relating_control=self.settings["cost_item"], - ) if self.settings["prop_name"]: + if ( + self.settings["cost_item"].CostQuantities + and self.settings["cost_item"].CostQuantities[0].Name.lower() + != self.settings["prop_name"].lower() + ): + continue + ifcopenshell.api.run( + "control.assign_control", + self.file, + related_object=product, + relating_control=self.settings["cost_item"], + ) self.add_quantity_from_related_object(product) if self.settings["prop_name"]: self.settings["cost_item"].CostQuantities = list(self.quantities) @@ -107,7 +118,10 @@ class Usecase: if not qto.is_a("IfcElementQuantity"): return for prop in qto.Quantities: - if prop.is_a("IfcPhysicalSimpleQuantity") and prop.Name.lower() == self.settings["prop_name"].lower(): + if ( + prop.is_a("IfcPhysicalSimpleQuantity") + and prop.Name.lower() == self.settings["prop_name"].lower() + ): self.quantities.add(prop) def update_cost_item_count(self): From c1283d2ab01685d65541e80e5d12e0bc58a592e3 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sat, 12 Aug 2023 21:36:40 +0100 Subject: [PATCH 20/32] fix SyntaxError: '(' was never closed --- .../api/profile/add_arbitrary_profile_with_voids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py index a6fecfb55c..0e784ad207 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py @@ -69,7 +69,7 @@ class Usecase: outer_curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in outer_points]) inner_curves = [] for inner_point in inner_points: - inner_curves.append(self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point]) + inner_curves.append(self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point])) else: outer_curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(outer_points)) inner_curves = [] From 84a749605e9c7ef5db13b74efd84e40ccb507a69 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 14:48:36 +1000 Subject: [PATCH 21/32] See #3579. Support Google Colab as an installation option. --- .../docs/ifcopenshell-python/installation.rst | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst index 043fa0689c..439d4cec69 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst @@ -1,16 +1,19 @@ Installation ============ -There are different methods of installation, depending on your situation. +There are different methods of installation, depending on your situation. If +you aren't sure which to choose, if you're a programmer, go for the **Pre-built +packages**. If you aren't a programmer, go for the **BlenderBIM Add-on**. 1. **Pre-built packages** is recommended for users wanting to use the latest IfcOpenShell builds. 2. **PyPI** is recommended for developers using Pip. 3. **Conda** is recommended for developers using Anaconda. 4. **Docker** is recommended for developers using Docker. 5. **AWS Lambda** is recommended for developers using AWS Lambda functions. -6. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface. -7. **From source with precompiled binaries** is recommended for developers actively working with the Python code. -8. **Compiling from source** is recommended for developers actively working with the C++ core. +6. **Google Colab** is recommended for developers using Google Colab. +7. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface. +8. **From source with precompiled binaries** is recommended for developers actively working with the Python code. +9. **Compiling from source** is recommended for developers actively working with the C++ core. Pre-built packages ------------------ @@ -182,6 +185,18 @@ Gateways, etc. the AWS documentation. Some tools that could be useful are AWS CloudFormaton, AWS CDK, pulumi or terraform. +Google Colab +------------ + +The Google Colab environment is based on the distribution from PyPI, but lets +you run it in an online notebook without any local setup required. This is +great for educators and those wanting to try it out without control on their +local system. + +`Click here +`__ +to launch a simple notebook. + Using the BlenderBIM Add-on --------------------------- From 840039ef1e9f1c458f1240e3d9b0c823acd4eda9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 15:49:23 +1000 Subject: [PATCH 22/32] Fix #3472. You can now toggle selectability of linked IFCs. --- .../blenderbim/bim/module/project/__init__.py | 1 + .../blenderbim/bim/module/project/operator.py | 21 +++++++++++++++++++ .../blenderbim/bim/module/project/prop.py | 1 + .../blenderbim/bim/module/project/ui.py | 7 +++++++ 4 files changed, 30 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index df38a8c86d..2bd50dd132 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -41,6 +41,7 @@ classes = ( operator.SaveLibraryFile, operator.SelectLibraryFile, operator.ToggleFilterCategories, + operator.ToggleLinkSelectability, operator.ToggleLinkVisibility, operator.UnassignLibraryDeclaration, operator.UnlinkIfc, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index a929cb638e..b510494fd7 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -880,6 +880,27 @@ class LoadLink(bpy.types.Operator): return {"FINISHED"} +class ToggleLinkSelectability(bpy.types.Operator): + bl_idname = "bim.toggle_link_selectability" + bl_label = "Toggle Link Selectability" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Toggle selectability" + link: bpy.props.StringProperty() + + def execute(self, context): + props = context.scene.BIMProjectProperties + link = props.links.get(self.link) + for collection in self.get_linked_collections(): + collection.hide_select = not collection.hide_select + link.is_selectable = not collection.hide_select + return {"FINISHED"} + + def get_linked_collections(self): + return [ + c for c in bpy.data.collections if "IfcProject" in c.name and c.library and c.library.filepath == self.link + ] + + class ToggleLinkVisibility(bpy.types.Operator): bl_idname = "bim.toggle_link_visibility" bl_label = "Toggle Link Visibility" diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index e65f5c4e48..bbc3c64f00 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -93,6 +93,7 @@ class FilterCategory(PropertyGroup): class Link(PropertyGroup): name: StringProperty(name="Name") is_loaded: BoolProperty(name="Is Loaded", default=False) + is_selectable: BoolProperty(name="Is Selectable", default=True) is_wireframe: BoolProperty(name="Is Wireframe", default=False) is_hidden: BoolProperty(name="Is Hidden", default=False) diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 788769b159..7558fc3762 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -350,6 +350,13 @@ class BIM_UL_links(UIList): row = layout.row(align=True) if item.is_loaded: row.label(text=item.name) + op = row.operator( + "bim.toggle_link_selectability", + text="", + icon="RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON", + emboss=False, + ) + op.link = item.name op = row.operator( "bim.toggle_link_visibility", text="", From e8437a9c541cfd3c18f0dfcc5cbab1d091ba71ad Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 20:07:33 +1000 Subject: [PATCH 23/32] Fix #3253. Bug where duplicating elements with fillings incorrectly recreated fillings at the cursor (i.e. the cursor was used as a target destination). Now, instead of trying to be clever and regenerating filling locations and opening types, we simply copy the existing scenario. This also means it'll be more stable for weird fillings coming from proprietary apps. --- .../bim/module/geometry/operator.py | 9 +++ src/blenderbim/blenderbim/tool/root.py | 61 +++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 59e47c7c02..8414c6d018 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -637,6 +637,11 @@ class OverrideDuplicateMove(bpy.types.Operator): element = tool.Ifc.get_entity(obj) if element and element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": continue # For now, don't copy drawings until we stabilise a bit more. It's tricky. + + # Prior to duplicating, sync the object placement to make decomposition recreation more stable. + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + new_obj = obj.copy() if obj.data: new_obj.data = obj.data.copy() @@ -705,6 +710,10 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator): relationships = tool.Root.get_decomposition_relationships(context.selected_objects) old_to_new = {} for obj in context.selected_objects: + # Prior to duplicating, sync the object placement to make decomposition recreation more stable. + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + new_obj = obj.copy() if obj.data: new_obj.data = obj.data.copy() diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py index b7424ff3bc..ce198c9f00 100644 --- a/src/blenderbim/blenderbim/tool/root.py +++ b/src/blenderbim/blenderbim/tool/root.py @@ -23,6 +23,7 @@ import blenderbim.core.tool import blenderbim.core.geometry import blenderbim.tool as tool from mathutils import Vector +from blenderbim.bim.module.model.opening import FilledOpeningGenerator class Root(blenderbim.core.tool.Root): @@ -57,7 +58,10 @@ class Root(blenderbim.core.tool.Root): if not source.Representation: return dest.Representation = ifcopenshell.util.element.copy_deep( - tool.Ifc.get(), source.Representation, exclude=["IfcGeometricRepresentationContext"], exclude_callback=exclude_callback + tool.Ifc.get(), + source.Representation, + exclude=["IfcGeometricRepresentationContext"], + exclude_callback=exclude_callback, ) elif dest.is_a("IfcTypeProduct"): if not source.RepresentationMaps: @@ -140,9 +144,58 @@ class Root(blenderbim.core.tool.Root): for i, new_subelement in enumerate(new_subelements): new_element = new_elements[i] if data["type"] == "fill": - obj1 = tool.Ifc.get_object(new_element) - obj2 = tool.Ifc.get_object(new_subelement) - bpy.ops.bim.add_filled_opening(voided_obj=obj1.name, filling_obj=obj2.name) + element = new_element + filling = new_subelement + voided_obj = tool.Ifc.get_object(new_element) + filling_obj = tool.Ifc.get_object(new_subelement) + + existing_opening_occurrence = subelement.FillsVoids[0].RelatingOpeningElement + opening = ifcopenshell.api.run( + "root.copy_class", tool.Ifc.get(), product=existing_opening_occurrence + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", + tool.Ifc.get(), + product=opening, + matrix=ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement), + is_si=False, + ) + + representation = ifcopenshell.util.representation.get_representation( + existing_opening_occurrence, "Model", "Body", "MODEL_VIEW" + ) + representation = ifcopenshell.util.representation.resolve_representation(representation) + mapped_representation = ifcopenshell.api.run( + "geometry.map_representation", tool.Ifc.get(), representation=representation + ) + ifcopenshell.api.run( + "geometry.assign_representation", + tool.Ifc.get(), + product=opening, + representation=mapped_representation, + ) + ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element) + ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling) + + voided_objs = [voided_obj] + # Openings affect all subelements of an aggregate + for subelement in ifcopenshell.util.element.get_decomposition(element): + subobj = tool.Ifc.get_object(subelement) + if subobj: + voided_objs.append(subobj) + + for voided_obj in voided_objs: + if voided_obj.data: + representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=voided_obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) @classmethod def run_geometry_add_representation( From eb94c164653e2f5c5a7aac6d499ef64f169e4262 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sun, 13 Aug 2023 12:16:29 +0100 Subject: [PATCH 24/32] fix the bug fix #07b5c49 - where counting objects controlled by a cost item stopped working --- .../api/cost/assign_cost_item_quantity.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py index eee40ed06e..71b4cf61f2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py @@ -88,28 +88,31 @@ class Usecase: self.quantities = set(self.settings["cost_item"].CostQuantities or []) print(self.settings["cost_item"].CostQuantities) for product in self.settings["products"]: + self.assign_cost_control( + related_object=product, cost_item=self.settings["cost_item"] + ) if self.settings["prop_name"]: if ( self.settings["cost_item"].CostQuantities and self.settings["cost_item"].CostQuantities[0].Name.lower() != self.settings["prop_name"].lower() - ): + ) or not product.is_a("IfcObject"): continue - ifcopenshell.api.run( - "control.assign_control", - self.file, - related_object=product, - relating_control=self.settings["cost_item"], - ) self.add_quantity_from_related_object(product) if self.settings["prop_name"]: self.settings["cost_item"].CostQuantities = list(self.quantities) else: self.update_cost_item_count() + def assign_cost_control(self, related_object, cost_item): + return ifcopenshell.api.run( + "control.assign_control", + self.file, + related_object=related_object, + relating_control=cost_item, + ) + def add_quantity_from_related_object(self, element): - if not element.is_a("IfcObject"): - return for relationship in element.IsDefinedBy: if relationship.is_a("IfcRelDefinesByProperties"): self.add_quantity_from_qto(relationship.RelatingPropertyDefinition) @@ -128,7 +131,7 @@ class Usecase: # This is a bold assumption # https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564 if not self.settings["cost_item"].CostQuantities: - return ifcopenshell.api.run( + ifcopenshell.api.run( "cost.add_cost_item_quantity", self.file, cost_item=self.settings["cost_item"], From 31a12faef1e822ebc01f05b4e895022674841ca2 Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sun, 13 Aug 2023 12:18:48 +0100 Subject: [PATCH 25/32] woopsies --- .../ifcopenshell/api/cost/assign_cost_item_quantity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py index 71b4cf61f2..b09e7f07fa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py @@ -86,7 +86,6 @@ class Usecase: def execute(self): if self.settings["prop_name"]: self.quantities = set(self.settings["cost_item"].CostQuantities or []) - print(self.settings["cost_item"].CostQuantities) for product in self.settings["products"]: self.assign_cost_control( related_object=product, cost_item=self.settings["cost_item"] From 56187e4da3027f758bd00f1e3a3a3eb421e6d9fc Mon Sep 17 00:00:00 2001 From: "Sigma Dimensions (Yass)" <79010126+myoualid@users.noreply.github.com> Date: Sun, 13 Aug 2023 13:23:31 +0100 Subject: [PATCH 26/32] Improve Cost Schedule Layout --- .../blenderbim/bim/module/cost/data.py | 11 +++++- .../blenderbim/bim/module/cost/ui.py | 37 ++++++++----------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/cost/data.py b/src/blenderbim/blenderbim/bim/module/cost/data.py index c713b9eb39..9786151c6b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/data.py +++ b/src/blenderbim/blenderbim/bim/module/cost/data.py @@ -117,6 +117,7 @@ class CostSchedulesData: data["TotalAppliedValue"] = 0.0 data["TotalCost"] = 0.0 has_unit_basis = False + is_sum = False if root_element.is_a("IfcCostItem"): values = root_element.CostValues elif root_element.is_a("IfcConstructionResource"): @@ -130,6 +131,11 @@ class CostSchedulesData: data["UnitBasisValueComponent"] = cost_value_data["UnitBasis"]["ValueComponent"] data["UnitBasisUnitSymbol"] = cost_value_data["UnitBasis"]["UnitSymbol"] has_unit_basis = True + else: + data["UnitBasisValueComponent"] = 1 + data["UnitBasisUnitSymbol"] = "U" + if cost_value.Category == "*": + is_sum = True if has_unit_basis: data["TotalCost"] = data["TotalAppliedValue"] / data["UnitBasisValueComponent"] else: @@ -137,7 +143,8 @@ class CostSchedulesData: data["TotalCost"] = data["TotalAppliedValue"] * data["TotalCostQuantity"] else: data["TotalCost"] = data["TotalAppliedValue"] - data["TotalAppliedValue"] = None + if is_sum: + data["TotalAppliedValue"] = None @classmethod def _load_cost_item_quantities(cls, cost_item, data): @@ -154,7 +161,7 @@ class CostSchedulesData: if unit: data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit) else: - data["UnitSymbol"] = None + data["UnitSymbol"] = "U" # same_unit_nested_cost_item = set() # data["DerivedTotalCostQuantity"] = None diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 3c5b6e49a5..bdf2e7ef8b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -620,18 +620,22 @@ class BIM_UL_cost_items_trait: else: row.label(text="", icon="DOT") - def draw_total_cost_column(self, layout, cost_item): - format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ") - currency = CostSchedulesData.data["currency"] - text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers - layout.label(text=text) - def draw_quantity_column(self, layout, cost_item): if CostSchedulesData.data["is_editing_rates"]: self.draw_uom_column(layout, cost_item) else: self.draw_total_quantity_column(layout, cost_item) + def draw_uom_column(self, layout, cost_item): + layout.label(text=cost_item["UnitBasisUnitSymbol"]) + + def draw_total_quantity_column(self, layout, cost_item): + if cost_item["TotalCostQuantity"]: + label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}" + layout.label(text=label) + else: + layout.label(text="-") + def draw_value_column(self, layout, cost_item): if cost_item["TotalAppliedValue"]: text = "{0:,.2f}".format(cost_item["TotalAppliedValue"]).replace(",", " ") @@ -643,8 +647,11 @@ class BIM_UL_cost_items_trait: else: layout.label(text="-") - def draw_uom_column(self, layout, cost_item): - layout.label(text=cost_item["UnitBasisUnitSymbol"] or "-" if cost_item["UnitBasisValueComponent"] else "-") + def draw_total_cost_column(self, layout, cost_item): + format_numbers = "{0:,.2f}".format(cost_item["TotalCost"]).replace(",", " ") + currency = CostSchedulesData.data["currency"] + text = "{} {}".format(format_numbers, currency["name"]) if currency else format_numbers + layout.label(text=text) def draw_order_operator(self, row, ifc_definition_id, cost_item): if cost_item["NestingIndex"] is not None: @@ -657,19 +664,7 @@ class BIM_UL_cost_items_trait: op.cost_item = ifc_definition_id op.new_index = cost_item["NestingIndex"] - 1 - def draw_total_quantity_column(self, layout, cost_item): - if cost_item["TotalCostQuantity"]: - label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}" - layout.label(text=label) - else: - layout.label(text="-") - # if cost_item["DerivedTotalCostQuantity"] not in [None, 0]: - # layout.label(text="{0:.2f}".format(cost_item["DerivedTotalCostQuantity"]) + f" {cost_item['DerivedUnitSymbol'] or '-'}") - # else: - # if cost_item["TotalCostQuantity"] == 0: - # layout.label(text="-") - # else: - # layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}") + class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList): From 7edabce65726a109561ff43ef9adc63dd3bfebf4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 23:02:49 +1000 Subject: [PATCH 27/32] Fix #3562. Non-manifold geometry is no longer considered in join criteria as there is no such thing as a closed polygon. --- .../blenderbim/bim/module/drawing/operator.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index c0bf829553..d85cd2dbe9 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -889,6 +889,21 @@ class CreateDrawing(bpy.types.Operator): ) return classes + def is_manifold(self, obj): + result = self.is_manifold_cache.get(obj.data.name, None) + if result is not None: + return result + + bm = bmesh.new() + bm.from_mesh(obj.data) + for edge in bm.edges: + if not edge.is_manifold: + bm.free() + self.is_manifold_cache[obj.data.name] = False + return False + self.is_manifold_cache[obj.data.name] = True + return True + def merge_linework_and_add_metadata(self, root): join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria") if join_criteria: @@ -899,6 +914,7 @@ class CreateDrawing(bpy.types.Operator): group = root.findall(".//{http://www.w3.org/2000/svg}g")[0] joined_paths = {} + self.is_manifold_cache = {} ifc = tool.Ifc.get() for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"): @@ -908,6 +924,10 @@ class CreateDrawing(bpy.types.Operator): classes.append("cut") el.set("class", " ".join(classes)) + obj = tool.Ifc.get_object(element) + if not self.is_manifold(obj): + continue + # An element group will contain a bunch of paths representing the # cut of that element. However IfcOpenShell may not correctly # create closed paths. We post-process all paths with shapely to From 8bf55ab46cb1407c5c7a14d5a8067ab4c03a4629 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 13 Aug 2023 23:34:06 +1000 Subject: [PATCH 28/32] See #3561. Misc annotation decorator now only highlights non-internal edges. --- .../blenderbim/bim/module/drawing/decoration.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index 9daf4ede50..07ce208e95 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -248,8 +248,13 @@ class BaseDecorator: if check_mode and obj.data.is_editmode: return self.get_editmesh_geom(obj) - vertices = [obj.matrix_world @ v.co for v in obj.data.vertices] - indices = [e.vertices for e in obj.data.edges] + bm = bmesh.new() + bm.from_mesh(obj.data) + vertices = [obj.matrix_world @ v.co for v in bm.verts] + # In object mode, it's nicer to not show "internal edges". Most will be dissolved anyway. + indices = [[v.index for v in e.verts] for e in bm.edges if len(e.link_faces) != 2] + bm.free() + return vertices, indices def get_editmesh_geom(self, obj): From ed4abacd1e4732ca27dbe063432d4718d4dd036f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 14 Aug 2023 11:55:15 +1000 Subject: [PATCH 29/32] Fix #3578. Support querying surface styles in tools like IfcCSV. --- .../ifcopenshell/util/element.py | 49 +++++++++++++++++++ .../ifcopenshell/util/selector.py | 4 ++ .../test/util/test_element.py | 47 ++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 2e8669b75d..bf0ddf616b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -339,6 +339,8 @@ def get_material(element, should_skip_usage=False, should_inherit=True): The material may be a single material, material set (layered, profiled, or constituent), or a material set usage. + :param element: The element to get the material of. + :type element: ifcopenshell.entity_instance.entity_instance :param should_skip_usage: If set to True, if the material is a material set usage, the material set itself will be returned. Useful if you don't care about occurrence usage parameters. If False, the usage will be @@ -378,6 +380,8 @@ def get_materials(element, should_inherit=True): If the element has a material set, the individual materials of that set are returned as a list. + :param element: The element to get the materials of. + :type element: ifcopenshell.entity_instance.entity_instance :param should_inherit: If True, any inherited materials from associated types will be considered. :return: The associated materials of the element. @@ -403,6 +407,51 @@ def get_materials(element, should_inherit=True): return [c.Material for c in material.MaterialConstituents] +def get_styles(element): + """Retrieves the styles used in an element's representation. + + Styles may be retreived from the material or the body representation. + + :param element: The element to get the styles of. + :type element: ifcopenshell.entity_instance.entity_instance + :return: A list of surface styles + :rtype: list[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + wall = file.by_type("IfcWall")[0] + styles = ifcopenshell.util.element.get_styles(wall) + """ + styles = [] + + materials = ifcopenshell.util.element.get_materials(element) + for material in materials: + for material_definition_representation in material.HasRepresentation or []: + for representation in material_definition_representation.Representations: + for item in representation.Items: + styles.extend([s for s in item.Styles if s.is_a("IfcSurfaceStyle")]) + + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + if not body: + return styles + + for representation in [body]: + queue = list(representation.Items) + while queue: + item = queue.pop() + if item.is_a("IfcMappedItem"): + queue.extend(item.MappingSource.MappedRepresentation.Items) + if item.is_a("IfcBooleanResult"): + queue.append(item.FirstOperand) + queue.append(item.SecondOperand) + if item.StyledByItem: + styles.extend([s for s in item.StyledByItem[0].Styles if s.is_a("IfcSurfaceStyle")]) + return styles + + + def get_elements_by_material(ifc_file, material): """Retrieves the elements related to a material. diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 6f940bdf8b..f44160fbf6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -520,6 +520,10 @@ class Selector: value = ifcopenshell.util.element.get_type(value) elif key in ("material", "mat"): value = ifcopenshell.util.element.get_material(value, should_skip_usage=True) + elif key in ("materials", "mats"): + value = ifcopenshell.util.element.get_materials(value) + elif key == "styles": + value = ifcopenshell.util.element.get_styles(value) elif key in ("item", "i"): if value.is_a("IfcMaterialLayerSet"): value = value.MaterialLayers diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index c4e8652472..499f448567 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -386,6 +386,53 @@ class TestGetMaterial(test.bootstrap.IFC4): assert subject.get_material(element, should_inherit=False) is None +class TestGetMaterials(test.bootstrap.IFC4): + def test_getting_the_materials_of_a_product(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + material = ifcopenshell.api.run("material.add_material", self.file) + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + assert subject.get_materials(element) == [material] + + +class TestGetStyles(test.bootstrap.IFC4): + def test_getting_the_styles_of_a_product(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + assert subject.get_styles(element) == [] + + model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model") + body = ifcopenshell.api.run("context.add_context", self.file, + context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model) + + material = ifcopenshell.api.run("material.add_material", self.file) + ifcopenshell.api.run("material.assign_material", self.file, product=element, material=material) + + style = ifcopenshell.api.run("style.add_style", self.file) + ifcopenshell.api.run("style.add_surface_style", self.file, + style=style, ifc_class="IfcSurfaceStyleShading", attributes={ + "SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 }, + "Transparency": 0., # 0 is opaque, 1 is transparent + }) + ifcopenshell.api.run("style.assign_material_style", self.file, material=material, style=style, context=body) + + assert subject.get_styles(element) == [style] + + style2 = ifcopenshell.api.run("style.add_style", self.file) + ifcopenshell.api.run("style.add_surface_style", self.file, + style=style2, ifc_class="IfcSurfaceStyleShading", attributes={ + "SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 }, + "Transparency": 0., # 0 is opaque, 1 is transparent + }) + + representation = ifcopenshell.api.run("geometry.add_wall_representation", self.file, + context=body, length=5, height=3, thickness=0.118) + + ifcopenshell.api.run("geometry.assign_representation", self.file, product=element, representation=representation) + ifcopenshell.api.run("style.assign_representation_styles", self.file, shape_representation=representation, styles=[style2]) + + assert subject.get_styles(element) == [style, style2] + + class TestGetElementsByMaterial(test.bootstrap.IFC4): def test_getting_elements_of_a_material(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") From 793b28e43b773e07a92a8fb0fd7c810371d0d97a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 14 Aug 2023 11:04:55 +0500 Subject: [PATCH 30/32] Remove empty IfcDocumentInformationRelationship #3581 --- .../ifcopenshell/api/document/remove_information.py | 7 +++++++ .../test/api/document/test_remove_information.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py index 9506c51478..64fba5975c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/remove_information.py @@ -46,10 +46,17 @@ class Usecase: def execute(self): for reference in self.settings["information"].HasDocumentReferences or []: ifcopenshell.api.run("document.remove_reference", self.file, reference=reference) + for rel in self.settings["information"].IsPointer or []: for information in rel.RelatedDocuments: ifcopenshell.api.run("document.remove_information", self.file, information=information) self.file.remove(rel) + + # remove IfcDocumentInformationRelationship so it won't become invalid + for rel in self.settings["information"].IsPointedTo or []: + if rel.RelatedDocuments == (self.settings["information"],): + self.file.remove(rel) + for rel in self.settings["information"].DocumentInfoForObjects or []: self.file.remove(rel) self.file.remove(self.settings["information"]) diff --git a/src/ifcopenshell-python/test/api/document/test_remove_information.py b/src/ifcopenshell-python/test/api/document/test_remove_information.py index 4df4857052..d03a7341ad 100644 --- a/src/ifcopenshell-python/test/api/document/test_remove_information.py +++ b/src/ifcopenshell-python/test/api/document/test_remove_information.py @@ -37,6 +37,19 @@ class TestRemoveInformation(test.bootstrap.IFC4): assert len(self.file.by_type("IfcDocumentReference")) == 0 assert len(self.file.by_type("IfcRelAssociatesDocument")) == 0 + # test removing relationship to another information if it was the only relating element + information = ifcopenshell.api.run("document.add_information", self.file, parent=None) + information1 = ifcopenshell.api.run("document.add_information", self.file, parent=information) + information2 = ifcopenshell.api.run("document.add_information", self.file, parent=information) + + ifcopenshell.api.run("document.remove_information", self.file, information=information1) + assert len(self.file.by_type("IfcDocumentInformation")) == 2 + assert len(self.file.by_type("IfcDocumentInformationRelationship")) == 1 + + ifcopenshell.api.run("document.remove_information", self.file, information=information2) + assert len(self.file.by_type("IfcDocumentInformation")) == 1 + assert len(self.file.by_type("IfcDocumentInformationRelationship")) == 0 + def test_removing_all_subdocuments_and_their_references_too(self): project = self.file.createIfcProject() information = ifcopenshell.api.run("document.add_information", self.file, parent=None) From bc85f0910984e771dfa9ded7819c009c90b377c8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 14 Aug 2023 11:08:50 +0500 Subject: [PATCH 31/32] Small info message on using validate --- src/blenderbim/blenderbim/bim/module/debug/operator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py index e8cb0efed8..2b67815858 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/operator.py +++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py @@ -154,6 +154,8 @@ class ValidateIfcFile(bpy.types.Operator): logger = logging.getLogger("validate") logger.setLevel(logging.DEBUG) ifcopenshell.validate.validate(IfcStore.get_file(), logger, express_rules=True) + + self.report({"INFO"}, "Check validation results in the system console.") return {"FINISHED"} From 0ed8dc970db7d06d1264b1b2444e8dd3f0acc87b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 14 Aug 2023 16:14:16 +1000 Subject: [PATCH 32/32] New simplified IfcFM parser for basic object data. --- src/ifcfm/ifcfm/parser.py | 269 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) diff --git a/src/ifcfm/ifcfm/parser.py b/src/ifcfm/ifcfm/parser.py index d055063f69..f74ce4ad78 100644 --- a/src/ifcfm/ifcfm/parser.py +++ b/src/ifcfm/ifcfm/parser.py @@ -22,9 +22,278 @@ import ifcopenshell.util.fm import ifcopenshell.util.selector import ifcopenshell.util.date import ifcopenshell.util.schema +import ifcopenshell.util.system +import ifcopenshell.util.placement import ifcopenshell.util.classification +class Parser2: + def __init__(self, preset="BASIC"): + self.file = None + self.categories = {} + self.get_category_elements = {} + self.get_element_data = {} + self.get_custom_element_data = {} + self.duplicate_keys = [] + + if preset == "BASIC": + self.get_category_elements = { + "actors": get_actors, + "facilities": get_facilities, + "storeys": get_storeys, + "spaces": get_spaces, + "zones": get_zones, + "types": get_types, + "elements": get_elements, + "systems": get_systems, + } + self.get_element_data = { + "actors": get_actor_data, + "facilities": get_facility_data, + "storeys": get_storey_data, + "spaces": get_space_data, + "zones": get_zone_data, + "types": get_type_data, + "elements": get_element_data, + "systems": get_system_data, + } + + def parse(self, ifc_file): + for category_name, get_category_elements in self.get_category_elements.items(): + self.categories.setdefault(category_name, {}) + for element in get_category_elements(ifc_file): + data = self.get_element_data[category_name](ifc_file, element) or {} + custom_data = ( + self.get_custom_element_data.get(category_name, lambda x, y: None)(ifc_file, element) or {} + ) + data.update(custom_data) + + if data: + if data["key"] in self.categories[category_name]: + self.duplicate_keys.append((self.categories[category_name][data["key"]], data)) + self.categories[category_name][data["key"]] = data + + +def get_actors(ifc_file): + return ifc_file.by_type("IfcActor") + + +def get_facilities(ifc_file): + return ifc_file.by_type("IfcBuilding") + + +def get_storeys(ifc_file): + return ifc_file.by_type("IfcBuildingStorey") + + +def get_spaces(ifc_file): + return ifc_file.by_type("IfcSpace") + + +def get_zones(ifc_file): + zones = [] + for zone in ifc_file.by_type("IfcZone"): + for rel in zone.IsGroupedBy: + zones.extend([(zone, space) for space in rel.RelatedObjects]) + return zones + + +def get_types(ifc_file): + return ifcopenshell.util.fm.get_fmhem_types(ifc_file) + + +def get_elements(ifc_file): + elements = set() + for element_type in ifcopenshell.util.fm.get_fmhem_types(ifc_file): + elements.update(ifcopenshell.util.element.get_types(element_type)) + return elements + + +def get_systems(ifc_file): + return ifc_file.by_type("IfcSystem") + + +def get_actor_data(ifc_file, element): + return { + "key": element.TheActor.Name, + "Name": element.TheActor.Name, + "Category": get_classification(element), + "Email": get_actor_address(element, "ElectronicMailAddresses"), + "Phone": get_actor_address(element, "TelephoneNumbers"), + "CompanyURL": get_actor_address(element, "WWWHomePageURL"), + "Department": get_actor_address(element, "InternalLocation"), + "Address1": get_actor_address(element, "AddressLines"), + "Address2": get_actor_address(element, "Town"), + "StateRegion": get_actor_address(element, "Region"), + "PostalCode": get_actor_address(element, "PostalCode"), + "Country": get_actor_address(element, "Country"), + } + + +def get_facility_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(ifc_file.by_type("IfcProject")[0]), + "Category": get_classification(element), + "ProjectName": ifc_file.by_type("IfcProject")[0].Name, + "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None), + "LinearUnits": "millimeters", + "AreaUnits": "square meters", + "AreaMeasurement": "BIM Software", + "Phase": ifc_file.by_type("IfcProject")[0].Phase, + "ModelSoftware": get_owner_application(element), + "ModelProjectID": ifc_file.by_type("IfcProject")[0].GlobalId, + "ModelSiteID": getattr(get_facility_parent(element, "IfcSite"), "GlobalId", None), + "ModelBuildingID": element.GlobalId, + } + + +def get_storey_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": "Level", + "ModelSoftware": get_owner_application(element), + "ModelObject": element.is_a(), + "ModelID": element.GlobalId, + "Elevation": ifcopenshell.util.placement.get_storey_elevation(element), + } + + +def get_space_data(ifc_file, element): + psets = ifcopenshell.util.element.get_psets(element) + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None), + "Description": element.LongName, + "ModelSoftware": get_owner_application(element), + "ModelID": element.GlobalId, + "AreaGross": get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2), + "AreaNet": get_property(psets, "Qto_SpaceBaseQuantities", "NetFloorArea", decimals=2), + } + + +def get_zone_data(ifc_file, element): + zone, space = element + return { + "key": (element.Name or "Unnamed") + (space.Name or "Unnamed"), + "Name": zone.Name, + "AuthorOrganizationName": get_owner_name(zone), + "AuthorDate": get_owner_creation_date(zone), + "SpaceName": space.Name, + "ModelSoftware": get_owner_application(zone), + "ModelID": zone.GlobalId, + } + + +def get_type_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "Description": element.Description, + "ModelSoftware": get_owner_application(element), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), + "ModelTag": element.Tag, + "ModelID": element.GlobalId, + } + + +def get_element_data(ifc_file, element): + space = ifcopenshell.util.element.get_container(element) + space_name = space.Name if space.is_a("IfcSpace") else None + systems = ifcopenshell.util.system.get_element_systems(element) + system = systems[0].Name if systems else None + return { + "key": element.Name, + "Name": element.Name, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "TypeName": ifcopenshell.util.element.get_type(element).Name, + "SpaceName": space_name, + "SystemName": system, + "ModelSoftware": get_owner_application(element), + "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), + "ModelID": element.GlobalId, + } + + +def get_system_data(ifc_file, element): + return { + "key": element.Name, + "Name": element.Name, + "Description": element.Description, + "AuthorOrganizationName": get_owner_name(element), + "AuthorDate": get_owner_creation_date(element), + "Category": get_classification(element), + "ModelSoftware": get_owner_application(element), + "ModelID": element.GlobalId, + } + + +def get_owner_name(element): + if not getattr(element, "OwnerHistory", None): + return + return element.OwnerHistory.OwningUser.TheOrganization.Name + + +def get_owner_creation_date(element): + if not getattr(element, "OwnerHistory", None): + return + return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() + + +def get_owner_application(element): + if not getattr(element, "OwnerHistory", None): + return + return element.OwnerHistory.OwningApplication.ApplicationFullName + + +def get_facility_parent(element, ifc_class): + parent = ifcopenshell.util.element.get_aggregate(element) + while parent: + if parent.is_a(ifc_class): + return parent + if parent.is_a("IfcProject"): + return + parent = ifcopenshell.util.element.get_aggregate(parent) + + +def get_classification(element): + references = list(ifcopenshell.util.classification.get_references(element)) + if references: + if hasattr(references[0], "Identification"): + return "{}:{}".format(references[0].Identification, references[0].Name) + return "{}:{}".format(references[0].ItemReference, references[0].Name) + + +def get_actor_address(element, name): + for address in element.TheActor.Addresses or []: + if hasattr(address, name) and getattr(address, name, None): + result = getattr(address, name) + if isinstance(result, tuple): + return result[0] + return result + + +def get_property(psets, pset_name, prop_name, decimals=None): + if pset_name in psets: + result = psets[pset_name].get(prop_name, None) + if decimals is None or result is None: + return result + return round(result, decimals) + + class Parser: def __init__(self, logger): self.logger = logger