diff --git a/README.md b/README.md index e9416797e8..e8670871c8 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ Those marked with an asterisk are part of IfcOpenShell. | ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* | | ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | | ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | -| ifccobie | Extract IFC data for COBie handover requirements | LGPL-3.0-or-later | | ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | | ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | | ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | diff --git a/src/bcf/README.md b/src/bcf/README.md index a792c4f79f..aa1e4951e3 100644 --- a/src/bcf/README.md +++ b/src/bcf/README.md @@ -1,84 +1,5 @@ # bcf -A simple Python implementation of BCF. -Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API -is available via `bcfapi.py`. - -It tries to support BCF-XML version 2.1 and 3.0, and BCF-API 3.0. - -## bcfxml - -The `bcfxml.load` function lets you read a BCF-XML file. -It takes care of using the right version based on the "bcf.version" file contained in the BCF package. - -The BCF files are extracted and parsed on-demand, and edits are stored in memory until you call the `save` method. - -```python -from bcf.bcfxml import load - -# Load a project -with load("/path/to/file.bcf") as bcfxml: - project = bcfxml.project - print(project.name) - - # To edit a project, just modify the object directly - bcfxml.project.name = "New name" - - # Get a dictionary of topics - topics = bcfxml.topics - - for topic_guid, topic_handler in bcfxml.topics.items(): - topic = topic_handler.topic - print("Topic guid is", topic.guid) - print("Topic title is", topic.title) - - # Fetch extra data about a topic - header = topic_handler.header - comments = topic_handler.comments - viewpoints = topic_handler.viewpoints - - for comment in comments: - print(comment.guid) - print(comment.comment) - print(comment.author) - - # Get a particular topic - topic = bcfxml.get_topic(guid) - - # Modify a topic - topic.title = "New title" - - bcfxml.save() -``` - -## bcfapi - -The `bcfapi` module lets you interact with the BCF-API standard. - -```python -from bcf.v3.bcfapi import FoundationClient, BcfClient - -foundation_client = FoundationClient("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET", "OPENCDE_BASEURL") -auth_methods = foundation_client.get_auth_methods() - -# Our library currently only implements the authorization_code flow -if "authorization_code" in auth_methods: - foundation_client.login() - -bcf_client = BcfClient(foundation_client) - -versions = foundation_client.get_versions() -for version in versions: -if "3.0" in versions: - if version["api_id"] == "bcf" and version["version_id"] == "3.0": - bcf_client.set_version(version) - -data = bcf_client.get_projects() -print(data) -project_id = data[0]["project_id"] -print(project_id) -data = bcf_client.get_project(project_id) -print(data) -data = bcf_client.get_extensions(project_id) -print(data) -``` +A simple Python implementation of the BCF standard. Manipulation of BCF-XML is +available via `bcfxml.py` and manipulation of BCF-API is available via +`bcfapi.py`. diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 5d3ceeeee3..08f291afa1 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -201,8 +201,8 @@ endif cp -r dist/working/IfcOpenShell-0.7.0/src/ifctester/ifctester dist/blenderbim/libs/site/packages/ # Provides IFCFM functionality cp -r dist/working/IfcOpenShell-0.7.0/src/ifcfm/ifcfm dist/blenderbim/libs/site/packages/ - # Provides IFCCOBie functionality - cp -r dist/working/IfcOpenShell-0.7.0/src/ifccobie/* dist/blenderbim/libs/site/packages/ + # Provides bSDD functionality + cp -r dist/working/IfcOpenShell-0.7.0/src/bsdd/* dist/blenderbim/libs/site/packages/ # Provides IFCDiff functionality cp -r dist/working/IfcOpenShell-0.7.0/src/ifcdiff/* dist/blenderbim/libs/site/packages/ # Provides IFCCSV functionality @@ -594,14 +594,6 @@ endif cd dist/working/parse_type-0.5.2/ && cp -r parse_type ../../blenderbim/libs/site/packages/ rm -rf dist/working - # Required by IFCCOBie for XLSX support - # TODO: see if we can replace this with openpyxl which does both read/write - mkdir dist/working - cd dist/working && wget https://files.pythonhosted.org/packages/0c/bc/82d6783f83f65f56d8b77d052773c4a2f952fa86385f0cd54e1e006658d7/XlsxWriter-1.2.9.tar.gz - cd dist/working && tar -xzvf XlsxWriter* - cd dist/working/XlsxWriter-1.2.9/ && cp -r xlsxwriter ../../blenderbim/libs/site/packages/ - rm -rf dist/working - # Required by augin mkdir dist/working cd dist/working && wget https://files.pythonhosted.org/packages/76/b4/b7baffbda025efd5dc8fcd8d2e953e3aa939c236a484084fa8f4c3588ee9/boto3-1.17.17.tar.gz diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 515abba2a3..ccafb12784 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -30,6 +30,7 @@ modules = { "project": None, "search": None, "bcf": None, + "bsdd": None, "root": None, "unit": None, "model": None, @@ -45,7 +46,6 @@ modules = { "aggregate": None, "geometry": None, "fm": None, - "cobie": None, "resource": None, "cost": None, "sequence": None, diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index aa2100b0c5..85b0ddf607 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -325,7 +325,7 @@ def draw_filter(layout, props, data, module): row.prop(ifc_filter, "value", text="", icon="OUTLINER") elif ifc_filter.type == "location": row = box.row(align=True) - row.prop(ifc_filter, "name", text="", icon="PACKAGE") + row.prop(ifc_filter, "value", text="", icon="PACKAGE") elif ifc_filter.type == "query": row = box.row(align=True) row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA") diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index ecfff3bcf0..762a482382 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -20,6 +20,7 @@ import os import re import bpy import time +import json import bmesh import logging import mathutils @@ -145,12 +146,15 @@ class MaterialCreator: faces_remap = None texture_map = None if coordinates.is_a("IfcIndexedPolygonalTextureMap"): - faces_remap = [[coordinates_remap[i-1] for i in tex_coord_index.TexCoordsOf.CoordIndex] - for tex_coord_index in coordinates.TexCoordIndices] + faces_remap = [ + [coordinates_remap[i - 1] for i in tex_coord_index.TexCoordsOf.CoordIndex] + for tex_coord_index in coordinates.TexCoordIndices + ] texture_map = [tex_coord_index.TexCoordIndex for tex_coord_index in coordinates.TexCoordIndices] elif coordinates.is_a("IfcIndexedTriangleTextureMap"): - faces_remap = [[coordinates_remap[i-1] for i in triangle_face] - for triangle_face in coordinates.MappedTo.CoordIndex] + faces_remap = [ + [coordinates_remap[i - 1] for i in triangle_face] for triangle_face in coordinates.MappedTo.CoordIndex + ] texture_map = coordinates.TexCoordIndex # apply uv to each face @@ -165,7 +169,7 @@ class MaterialCreator: ) # apply uv to each loop for loop, i in zip(bface.loops, texCoordIndex): - loop[uv_layer].uv = coordinates.TexCoords.TexCoordsList[i-1] + loop[uv_layer].uv = coordinates.TexCoords.TexCoordsList[i - 1] # Finish up, write the bmesh back to the mesh bm.to_mesh(self.mesh) @@ -262,6 +266,8 @@ class IfcImporter: self.profile_code("Calculate unit scale") self.calculate_model_offset() self.profile_code("Calculate model offset") + self.predict_dense_mesh() + self.profile_code("Predict dense mesh") self.set_units() self.profile_code("Set units") self.create_project() @@ -306,6 +312,7 @@ class IfcImporter: self.set_default_context() self.profile_code("Setting default context") self.setup_viewport_camera() + self.setup_arrays() self.update_progress(100) bpy.context.window_manager.progress_end() @@ -346,6 +353,7 @@ class IfcImporter: ) if self.body_contexts: self.settings.set_context_ids(self.body_contexts) + self.settings_body_2d.set_context_ids(self.body_contexts) # Annotation ContextType is to accommodate broken Revit files # See https://github.com/Autodesk/revit-ifc/issues/187 self.plan_contexts = [ @@ -496,6 +504,26 @@ class IfcImporter: products.extend(self.get_products_from_shape_representation(inverse_element)) return products + def predict_dense_mesh(self): + threshold = 10000 # Just from experience. + + faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")] + if faces and max(faces) > threshold: + self.ifc_import_settings.should_use_native_meshes = True + return + + if self.file.schema == "IFC2X3": + return + + faces = [len(e.Faces) for e in self.file.by_type("IfcPolygonalFaceSet")] + if faces and max(faces) > threshold: + self.ifc_import_settings.should_use_native_meshes = True + return + + faces = [len(e.CoordIndex) for e in self.file.by_type("IfcTriangulatedFaceSet")] + if faces and max(faces) > threshold: + self.ifc_import_settings.should_use_native_meshes = True + def calculate_model_offset(self): props = bpy.context.scene.BIMGeoreferenceProperties if props.has_blender_offset: @@ -1908,6 +1936,15 @@ class IfcImporter: bpy.ops.view3d.view_selected() bpy.ops.object.select_all(action="DESELECT") + def setup_arrays(self): + for element in self.file.by_type("IfcElement"): + pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Array") + if not pset_data or not pset_data.get("Data", None): # skip array children + continue + for i in range(len(json.loads(pset_data["Data"]))): + tool.Blender.Modifier.Array.set_children_lock_state(element, i, True) + tool.Blender.Modifier.Array.constrain_children_to_parent(element) + class IfcImportSettings: def __init__(self): diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index d09179f6b1..bee67a413e 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -202,6 +202,7 @@ class BIM_OT_select_aggregate(bpy.types.Operator): aggregate_obj = tool.Ifc.get_object(aggregate) if aggregate_obj in context.selectable_objects: aggregate_obj.select_set(True) + bpy.context.view_layer.objects.active = aggregate_obj return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cobie/__init__.py b/src/blenderbim/blenderbim/bim/module/bsdd/__init__.py similarity index 61% rename from src/blenderbim/blenderbim/bim/module/cobie/__init__.py rename to src/blenderbim/blenderbim/bim/module/bsdd/__init__.py index a1ad500ae2..63f62931a6 100644 --- a/src/blenderbim/blenderbim/bim/module/cobie/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/bsdd/__init__.py @@ -1,5 +1,5 @@ # BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult +# Copyright (C) 2023 Dion Moult # # This file is part of BlenderBIM Add-on. # @@ -20,17 +20,23 @@ import bpy from . import ui, prop, operator classes = ( - operator.SelectCobieIfcFile, - operator.SelectCobieJsonFile, - operator.ExecuteIfcCobie, - prop.COBieProperties, - ui.BIM_PT_cobie, + operator.GetBSDDClassificationProperties, + operator.LoadBSDDDomains, + operator.SearchBSDDClassifications, + operator.SetActiveBSDDDomain, + prop.BSDDDomain, + prop.BSDDClassification, + prop.BSDDPset, + prop.BIMBSDDProperties, + ui.BIM_UL_bsdd_domains, + ui.BIM_UL_bsdd_classifications, + ui.BIM_PT_bsdd, ) def register(): - bpy.types.Scene.COBieProperties = bpy.props.PointerProperty(type=prop.COBieProperties) + bpy.types.Scene.BIMBSDDProperties = bpy.props.PointerProperty(type=prop.BIMBSDDProperties) def unregister(): - del bpy.types.Scene.COBieProperties + del bpy.types.Scene.BIMBSDDProperties diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/operator.py b/src/blenderbim/blenderbim/bim/module/bsdd/operator.py new file mode 100644 index 0000000000..f3ecba2204 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/bsdd/operator.py @@ -0,0 +1,132 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import os +import bpy +import bsdd +import json +import ifcopenshell +import blenderbim.tool as tool + + +class LoadBSDDDomains(bpy.types.Operator): + bl_idname = "bim.load_bsdd_domains" + bl_label = "Load bSDD Domains" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMBSDDProperties + props.domains.clear() + client = bsdd.Client() + for domain in sorted(client.Domain(), key=lambda x: x["name"]): + new = props.domains.add() + new.name = domain["name"] + new.namespace_uri = domain["namespaceUri"] + new.default_language_code = domain["defaultLanguageCode"] + new.organization_name_owner = domain["organizationNameOwner"] + new.status = domain["status"] + new.version = domain["version"] + return {"FINISHED"} + + +class SetActiveBSDDDomain(bpy.types.Operator): + bl_idname = "bim.set_active_bsdd_domain" + bl_label = "Load bSDD Domains" + bl_options = {"REGISTER", "UNDO"} + name: bpy.props.StringProperty() + uri: bpy.props.StringProperty() + + def execute(self, context): + props = context.scene.BIMBSDDProperties + props.active_domain = self.name + props.active_uri = self.uri + return {"FINISHED"} + + +class SearchBSDDClassifications(bpy.types.Operator): + bl_idname = "bim.search_bsdd_classifications" + bl_label = "Search bSDD Classifications" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMBSDDProperties + props.classifications.clear() + client = bsdd.Client() + related_ifc_entities = [] + if len(props.keyword) < 3: + return {"FINISHED"} + if props.should_filter_ifc_class and context.active_object: + element = tool.Ifc.get_entity(context.active_object) + if element: + related_ifc_entities = [element.is_a()] + results = client.ClassificationSearchOpen(props.keyword, DomainNamespaceUris=[props.active_uri], RelatedIfcEntities=related_ifc_entities) + for result in sorted(results["classifications"], key=lambda x: x["referenceCode"]): + new = props.classifications.add() + new.name = result["name"] + new.reference_code = result["referenceCode"] + new.description = result.get("description", "") + new.namespace_uri = result["namespaceUri"] + new.domain_name = result["domainName"] + new.domain_namespace_uri = result["domainNamespaceUri"] + return {"FINISHED"} + + +class GetBSDDClassificationProperties(bpy.types.Operator): + bl_idname = "bim.get_bsdd_classification_properties" + bl_label = "Search bSDD Classifications" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + bprops = context.scene.BIMBSDDProperties + bprops.classification_psets.clear() + bsdd_classification = bprops.classifications[bprops.active_classification_index] + client = bsdd.Client() + data = client.Classification(bsdd_classification.namespace_uri) + + properties = data.get("classificationProperties", None) + if not properties: + return {"FINISHED"} + + psets = {} + + for prop in properties: + if prop.get("propertyDomainName") != "IFC": + continue + pset = prop.get("propertySet", None) + if not pset: + continue + psets.setdefault(pset, {}) + + predefined_value = prop.get("predefinedValue") + if predefined_value: + possible_values = [predefined_value] + else: + possible_values = prop.get("possibleValues", []) or [] + possible_values = [v["value"] for v in possible_values] + + psets[pset][prop["name"]] = possible_values + + for pset_name, pset in psets.items(): + new = bprops.classification_psets.add() + new.name = pset_name + for name, values in pset.items(): + new2 = new.properties.add() + new2.name = name + new2.enum_items = json.dumps(values) + new2.data_type = "enum" + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/prop.py b/src/blenderbim/blenderbim/bim/module/bsdd/prop.py new file mode 100644 index 0000000000..cf8e3e95e1 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/bsdd/prop.py @@ -0,0 +1,66 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +from bpy.types import PropertyGroup +from blenderbim.bim.prop import Attribute, StrProperty +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +class BSDDDomain(PropertyGroup): + name: StringProperty(name="Name") + namespace_uri: StringProperty(name="URI") + default_language_code: StringProperty(name="Language") + organization_name_owner: StringProperty(name="Organization") + status: StringProperty(name="Status") + version: StringProperty(name="Version") + + +class BSDDClassification(PropertyGroup): + name: StringProperty(name="Name") + reference_code: StringProperty(name="Reference Code") + description: StringProperty(name="Description") + namespace_uri: StringProperty(name="Namespace URI") + domain_name: StringProperty(name="Domain Name") + domain_namespace_uri: StringProperty(name="Domain Namespace URI") + + +class BSDDPset(PropertyGroup): + name: StringProperty(name="Name") + properties: CollectionProperty(name="Properties", type=Attribute) + + +class BIMBSDDProperties(PropertyGroup): + active_domain: StringProperty(name="Active Domain") + active_uri: StringProperty(name="Active URI") + domains: CollectionProperty(name="Domains", type=BSDDDomain) + active_domain_index: IntProperty(name="Active Domain Index") + classifications: CollectionProperty(name="Classifications", type=BSDDClassification) + active_classification_index: IntProperty(name="Active Classification Index") + keyword: StringProperty(name="Keyword") + should_filter_ifc_class: BoolProperty(name="Filter Active IFC Class", default=True) + classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset) diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/ui.py b/src/blenderbim/blenderbim/bim/module/bsdd/ui.py new file mode 100644 index 0000000000..8577c3242a --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/bsdd/ui.py @@ -0,0 +1,71 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.tool as tool +from bpy.types import Panel, UIList +from blenderbim.bim.ifc import IfcStore + + +class BIM_PT_bsdd(Panel): + bl_label = "buildingSMART Data Dictionary" + bl_idname = "BIM_PT_bsdd" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_project_setup" + + def draw(self, context): + props = context.scene.BIMBSDDProperties + if props.active_domain: + row = self.layout.row() + row.label(text="Active: " + props.active_domain, icon="URL") + else: + row = self.layout.row() + row.label(text="No Active bSDD Domain", icon="ERROR") + + if len(props.domains): + self.layout.template_list( + "BIM_UL_bsdd_domains", + "", + props, + "domains", + props, + "active_domain_index", + ) + else: + row = self.layout.row() + row.operator("bim.load_bsdd_domains") + + +class BIM_UL_bsdd_domains(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=f"{item.name} ({item.organization_name_owner})") + op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF") + op.name = item.name + op.uri = item.namespace_uri + + +class BIM_UL_bsdd_classifications(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.reference_code) + row.label(text=item.name) diff --git a/src/blenderbim/blenderbim/bim/module/classification/__init__.py b/src/blenderbim/blenderbim/bim/module/classification/__init__.py index f5fdcaaaed..d59b2656b3 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/classification/__init__.py @@ -21,7 +21,9 @@ from . import ui, prop, operator classes = ( operator.AddClassification, + operator.AddClassificationFromBSDD, operator.AddClassificationReference, + operator.AddClassificationReferenceFromBSDD, operator.ChangeClassificationLevel, operator.DisableEditingClassification, operator.DisableEditingClassificationReference, diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index d9b1b56845..8ec27ad731 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -54,6 +54,25 @@ class AddClassification(bpy.types.Operator, tool.Ifc.Operator): ) +class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_classification_from_bsdd" + bl_label = "Add Classification From bSDD" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + props = context.scene.BIMBSDDProperties + domain = [d for d in props.domains if d.name == props.active_domain][0] + for element in tool.Ifc.get().by_type("IfcClassification"): + if element.Name == props.active_domain or element.Location == domain.namespace_uri: + return + classification = ifcopenshell.api.run( + "classification.add_classification", tool.Ifc.get(), classification=props.active_domain + ) + classification.Source = domain.organization_name_owner + classification.Location = domain.namespace_uri + classification.Edition = domain.version + + class EnableEditingClassification(bpy.types.Operator): bl_idname = "bim.enable_editing_classification" bl_label = "Enable Editing Classification" @@ -254,6 +273,70 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator): ) +class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_classification_reference_from_bsdd" + bl_label = "Add Classification Reference From bSDD" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + obj_type: bpy.props.StringProperty() + + def _execute(self, context): + if self.obj_type == "Object": + if context.selected_objects: + objects = [o.name for o in context.selected_objects] + else: + objects = [context.active_object.name] + else: + objects = [self.obj] + props = context.scene.BIMClassificationProperties + bprops = context.scene.BIMBSDDProperties + + bsdd_classification = bprops.classifications[bprops.active_classification_index] + + classification = None + for element in tool.Ifc.get().by_type("IfcClassification"): + if ( + element.Name == bsdd_classification.domain_name + or element.Location == bsdd_classification.domain_namespace_uri + ): + classification = element + break + + if not classification: + classification = ifcopenshell.api.run( + "classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name + ) + classification.Location = bsdd_classification.domain_namespace_uri + + for obj in objects: + ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) + if not ifc_definition_id: + continue + element = tool.Ifc.get().by_id(ifc_definition_id) + reference = ifcopenshell.api.run( + "classification.add_reference", + tool.Ifc.get(), + product=element, + classification=classification, + identification=bsdd_classification.reference_code, + name=bsdd_classification.name, + ) + reference.Location = bsdd_classification.namespace_uri + + for classification_pset in bprops.classification_psets: + pset = ifcopenshell.util.element.get_pset(element, classification_pset.name) + if pset: + pset = tool.Ifc.get().by_id(pset["id"]) + else: + pset = ifcopenshell.api.run( + "pset.add_pset", tool.Ifc.get(), product=element, name=classification_pset.name + ) + properties = {} + for prop in classification_pset.properties: + properties[prop.name] = prop.get_value() + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=properties) + + class ChangeClassificationLevel(bpy.types.Operator): bl_idname = "bim.change_classification_level" bl_label = "Change Classification Level" diff --git a/src/blenderbim/blenderbim/bim/module/classification/prop.py b/src/blenderbim/blenderbim/bim/module/classification/prop.py index eee7b14952..22c4e6af5c 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/prop.py +++ b/src/blenderbim/blenderbim/bim/module/classification/prop.py @@ -48,6 +48,15 @@ class ClassificationReference(PropertyGroup): class BIMClassificationProperties(PropertyGroup): + classification_source: EnumProperty( + items=[ + ("FILE", "IFC File", ""), + ("BSDD", "buildingSMART Data Dictionary", ""), + ("MANUAL", "Manual Entry", ""), + ], + name="Classification Source", + default="FILE", + ) available_classifications: EnumProperty(items=get_available_classifications, name="Available Classifications") classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute) active_classification_id: IntProperty(name="Active Classification Id") diff --git a/src/blenderbim/blenderbim/bim/module/classification/ui.py b/src/blenderbim/blenderbim/bim/module/classification/ui.py index 5a4de6bf65..f63c887fdf 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/ui.py +++ b/src/blenderbim/blenderbim/bim/module/classification/ui.py @@ -48,6 +48,42 @@ class BIM_PT_classifications(Panel): self.props = context.scene.BIMClassificationProperties + row = self.layout.row(align=True) + row.label(text="Source", icon="OUTLINER") + row.prop(self.props, "classification_source", text="") + + if self.props.classification_source == "FILE": + self.draw_add_file_ui(context) + elif self.props.classification_source == "BSDD": + self.draw_add_bsdd_ui(context) + elif self.props.classification_source == "MANUAL": + self.draw_add_manual_ui(context) + + for classification in ClassificationsData.data["classifications"]: + if self.props.active_classification_id == classification["id"]: + self.draw_editable_ui() + else: + self.draw_ui(classification) + + def draw_add_manual_ui(self, context): + row = self.layout.row() + row.label(text="TODO", icon="ERROR") + + def draw_add_bsdd_ui(self, context): + self.bprops = context.scene.BIMBSDDProperties + + if not self.bprops.active_domain: + row = self.layout.row() + row.label(text="No Active bSDD Domain", icon="ERROR") + return + + row = self.layout.row() + row.label(text="Active: " + self.bprops.active_domain, icon="URL") + + row = self.layout.row() + row.operator("bim.add_classification_from_bsdd", icon="ADD") + + def draw_add_file_ui(self, context): if ClassificationsData.data["has_classification_file"]: row = self.layout.row(align=True) row.prop(self.props, "available_classifications", text="") @@ -58,12 +94,6 @@ class BIM_PT_classifications(Panel): row.label(text="No Active Classification Library") row.operator("bim.load_classification_library", text="", icon="IMPORT") - for classification in ClassificationsData.data["classifications"]: - if self.props.active_classification_id == classification["id"]: - self.draw_editable_ui() - else: - self.draw_ui(classification) - def draw_editable_ui(self): row = self.layout.row(align=True) row.operator("bim.edit_classification", text="Save changes", icon="CHECKMARK") @@ -84,6 +114,7 @@ class ReferenceUI: obj = context.active_object self.oprops = obj.BIMObjectProperties self.sprops = context.scene.BIMClassificationProperties + self.bprops = context.scene.BIMBSDDProperties self.props = obj.BIMClassificationReferenceProperties self.file = IfcStore.get_file() @@ -100,14 +131,76 @@ class ReferenceUI: self.draw_reference_ui(reference) def draw_add_ui(self, context): + row = self.layout.row(align=True) + row.label(text="Source", icon="OUTLINER") + row.prop(self.sprops, "classification_source", text="") + + if self.sprops.classification_source == "FILE": + self.draw_add_file_ui(context) + elif self.sprops.classification_source == "BSDD": + self.draw_add_bsdd_ui(context) + elif self.sprops.classification_source == "MANUAL": + self.draw_add_manual_ui(context) + + def draw_add_manual_ui(self, context): + row = self.layout.row() + row.label(text="TODO", icon="ERROR") + + def draw_add_bsdd_ui(self, context): + if not self.bprops.active_domain: + row = self.layout.row() + row.label(text="No Active bSDD Domain", icon="ERROR") + return + + row = self.layout.row() + row.label(text="Active: " + self.bprops.active_domain, icon="URL") + + row = self.layout.row(align=True) + row.prop(self.bprops, "keyword", text="") + row.operator("bim.search_bsdd_classifications", text="", icon="VIEWZOOM") + + row = self.layout.row() + row.prop(self.bprops, "should_filter_ifc_class") + + if len(self.bprops.classifications): + self.layout.template_list( + "BIM_UL_bsdd_classifications", + "", + self.bprops, + "classifications", + self.bprops, + "active_classification_index", + ) + else: + row = self.layout.row() + row.label(text="No Search Results") + + if self.bprops.active_classification_index < len(self.bprops.classifications): + row = self.layout.row(align=True) + op = row.operator( + "bim.add_classification_reference_from_bsdd", text="Add Classification Reference", icon="ADD" + ) + op.obj = self.obj + op.obj_type = self.obj_type + row.operator("bim.get_bsdd_classification_properties", text="", icon="COPY_ID") + + if len(self.bprops.classification_psets): + for pset in self.bprops.classification_psets: + box = self.layout.box() + row = box.row() + row.label(text=pset.name, icon="COPY_ID") + blenderbim.bim.helper.draw_attributes(pset.properties, box) + + def draw_add_file_ui(self, context): if not self.data.data["active_classification_library"]: row = self.layout.row(align=True) - row.label(text="No Active Classification Library") + row.label(text="No Active Classification Library", icon="ERROR") row.operator("bim.load_classification_library", text="", icon="IMPORT") return + row = self.layout.row(align=True) row.label(text=f"Active Classification Library: {self.data.data['active_classification_library']}") - #row.prop(self.sprops, "available_classifications", text="") + # row.prop(self.sprops, "available_classifications", text="") if not self.sprops.available_library_references: op = row.operator("bim.change_classification_level", text="", icon="GREASEPENCIL") op.parent_id = int(self.sprops.available_classifications) diff --git a/src/blenderbim/blenderbim/bim/module/cobie/operator.py b/src/blenderbim/blenderbim/bim/module/cobie/operator.py deleted file mode 100644 index 3f1a512116..0000000000 --- a/src/blenderbim/blenderbim/bim/module/cobie/operator.py +++ /dev/null @@ -1,124 +0,0 @@ -# BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult -# -# This file is part of BlenderBIM Add-on. -# -# BlenderBIM Add-on is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# BlenderBIM Add-on is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with BlenderBIM Add-on. If not, see . - -import bpy -import os -import logging -import ifcopenshell -import json -import webbrowser -import tempfile -from blenderbim.bim.ifc import IfcStore - - -class SelectCobieIfcFile(bpy.types.Operator): - bl_idname = "bim.select_cobie_ifc_file" - bl_label = "Select COBie IFC File" - bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - context.scene.COBieProperties.cobie_ifc_file = self.filepath - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class SelectCobieJsonFile(bpy.types.Operator): - bl_idname = "bim.select_cobie_json_file" - bl_label = "Select COBie JSON File" - bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - context.scene.COBieProperties.cobie_json_file = self.filepath - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class ExecuteIfcCobie(bpy.types.Operator): - bl_idname = "bim.execute_ifc_cobie" - bl_label = "Execute IFCCOBie" - file_format: bpy.props.StringProperty() - - @classmethod - def poll(cls, context): - props = context.scene.COBieProperties - return props.should_load_from_memory or props.cobie_ifc_file - - def execute(self, context): - from cobie import IfcCobieParser - - props = context.scene.COBieProperties - - if props.should_load_from_memory: - output_dir = tempfile.gettempdir() - else: - output_dir = os.path.dirname(props.cobie_ifc_file) - - output = os.path.join(output_dir, "output") - logger = logging.getLogger("IFCtoCOBie") - fh = logging.FileHandler(os.path.join(output_dir, "cobie.log")) - fh.setLevel(logging.DEBUG) - fh.setFormatter(logging.Formatter("%(asctime)s : %(levelname)s : %(message)s")) - logger = logging.getLogger("IFCtoCOBie") - logger.addHandler(fh) - selector = ifcopenshell.util.selector.Selector() - if props.cobie_json_file: - with open(props.cobie_json_file, "r") as f: - custom_data = json.load(f) - else: - custom_data = {} - parser = IfcCobieParser(logger, selector) - - ifc_file = IfcStore.get_file() - - if not (ifc_file and props.should_load_from_memory): - ifc_file = props.cobie_ifc_file - - parser.parse( - ifc_file, - props.cobie_types, - props.cobie_components, - custom_data, - ) - if self.file_format == "xlsx": - from cobie import CobieXlsWriter - - writer = CobieXlsWriter(parser, output) - writer.write() - webbrowser.open("file://" + output + "." + self.file_format) - elif self.file_format == "ods": - from cobie import CobieOdsWriter - - writer = CobieOdsWriter(parser, output) - writer.write() - webbrowser.open("file://" + output + "." + self.file_format) - else: - from cobie import CobieCsvWriter - - writer = CobieCsvWriter(parser, output_dir) - writer.write() - webbrowser.open("file://" + output_dir) - webbrowser.open("file://" + output_dir + "/cobie.log") - return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cobie/prop.py b/src/blenderbim/blenderbim/bim/module/cobie/prop.py deleted file mode 100644 index 567a0f9960..0000000000 --- a/src/blenderbim/blenderbim/bim/module/cobie/prop.py +++ /dev/null @@ -1,38 +0,0 @@ -# BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult -# -# This file is part of BlenderBIM Add-on. -# -# BlenderBIM Add-on is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# BlenderBIM Add-on is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with BlenderBIM Add-on. If not, see . - -import bpy -from bpy.types import PropertyGroup -from bpy.props import ( - PointerProperty, - StringProperty, - EnumProperty, - BoolProperty, - IntProperty, - FloatProperty, - FloatVectorProperty, - CollectionProperty, -) - - -class COBieProperties(PropertyGroup): - cobie_ifc_file: StringProperty(default="", name="COBie IFC File") - cobie_types: StringProperty(default=".COBieType", name="COBie Types") - cobie_components: StringProperty(default=".COBie", name="COBie Components") - cobie_json_file: StringProperty(default="", name="COBie JSON File") - should_load_from_memory: BoolProperty(default=False, name="Load from Memory") diff --git a/src/blenderbim/blenderbim/bim/module/cobie/ui.py b/src/blenderbim/blenderbim/bim/module/cobie/ui.py deleted file mode 100644 index a78cff2b62..0000000000 --- a/src/blenderbim/blenderbim/bim/module/cobie/ui.py +++ /dev/null @@ -1,63 +0,0 @@ -# BlenderBIM Add-on - OpenBIM Blender Add-on -# Copyright (C) 2020, 2021 Dion Moult -# -# This file is part of BlenderBIM Add-on. -# -# BlenderBIM Add-on is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# BlenderBIM Add-on is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with BlenderBIM Add-on. If not, see . - -import blenderbim.tool as tool -from bpy.types import Panel -from blenderbim.bim.ifc import IfcStore - - -class BIM_PT_cobie(Panel): - bl_label = "COBie" - bl_idname = "BIM_PT_cobie" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - bl_parent_id = "BIM_PT_tab_handover" - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - - scene = context.scene - props = scene.COBieProperties - - if IfcStore.get_file(): - row = layout.row() - row.prop(props, "should_load_from_memory") - - if not IfcStore.get_file() or not props.should_load_from_memory: - row = layout.row(align=True) - row.prop(props, "cobie_ifc_file") - row.operator("bim.select_cobie_ifc_file", icon="FILE_FOLDER", text="") - - row = layout.row() - row.prop(props, "cobie_types") - row = layout.row() - row.prop(props, "cobie_components") - - row = layout.row(align=True) - row.prop(props, "cobie_json_file") - row.operator("bim.select_cobie_json_file", icon="FILE_FOLDER", text="") - - row = layout.row() - op = row.operator("bim.execute_ifc_cobie", text="CSV") - op.file_format = "csv" - op = row.operator("bim.execute_ifc_cobie", text="ODS") - op.file_format = "ods" - op = row.operator("bim.execute_ifc_cobie", text="XLSX") - op.file_format = "xlsx" diff --git a/src/blenderbim/blenderbim/bim/module/debug/__init__.py b/src/blenderbim/blenderbim/bim/module/debug/__init__.py index 1c0de63341..74d59a6eec 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/debug/__init__.py @@ -20,18 +20,19 @@ import bpy from . import ui, prop, operator classes = ( + operator.ConvertToBlender, operator.CopyDebugInformation, operator.CreateAllShapes, operator.CreateShapeFromStepId, operator.InspectFromObject, operator.InspectFromStepId, + operator.OverrideDisplayType, operator.ParseExpress, operator.PrintIfcFile, operator.PrintObjectPlacement, operator.ProfileImportIFC, operator.PurgeHdf5Cache, operator.PurgeIfcLinks, - operator.ConvertToBlender, operator.RewindInspector, operator.SelectExpressFile, operator.SelectHighPolygonMeshes, diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py index 9eecda3251..32cfbce6d3 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/operator.py +++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py @@ -443,3 +443,14 @@ class PurgeHdf5Cache(bpy.types.Operator): def execute(self, context): core.purge_hdf5_cache(tool.Debug) return {"FINISHED"} + + +class OverrideDisplayType(bpy.types.Operator): + bl_idname = "bim.override_display_type" + bl_label = "Override Display Type" + display: bpy.props.StringProperty() + + def execute(self, context): + for obj in context.selected_objects: + obj.display_type = self.display + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/debug/prop.py b/src/blenderbim/blenderbim/bim/module/debug/prop.py index 08e7699a61..0825b0f0f6 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/prop.py +++ b/src/blenderbim/blenderbim/bim/module/debug/prop.py @@ -40,3 +40,13 @@ class BIMDebugProperties(PropertyGroup): inverse_attributes: CollectionProperty(name="Inverse Attributes", type=Attribute) inverse_references: CollectionProperty(name="Inverse References", type=Attribute) express_file: StringProperty(name="Express File") + display_type: EnumProperty( + items=[ + ("BOUNDS", "Bounds", ""), + ("WIRE", "Wire", ""), + ("SOLID", "Solid", ""), + ("TEXTURED", "Textured", ""), + ], + name="Display Type", + default="BOUNDS", + ) diff --git a/src/blenderbim/blenderbim/bim/module/debug/ui.py b/src/blenderbim/blenderbim/bim/module/debug/ui.py index e8629f7854..205c08f480 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/ui.py +++ b/src/blenderbim/blenderbim/bim/module/debug/ui.py @@ -83,6 +83,10 @@ class BIM_PT_debug(Panel): ).percentile = context.scene.BIMDebugProperties.percentile_of_polygons row.prop(props, "percentile_of_polygons", text="") + row = layout.split(factor=0.5, align=True) + row.prop(props, "display_type", text="") + row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type + if context.active_object and context.active_object.data: mprops = context.active_object.data.BIMMeshProperties row = layout.row() diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index 6b21e95bee..ebff8b188d 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -1926,7 +1926,7 @@ class DecorationsHandler: if cls.installed: cls.uninstall() handler = cls() - # NOTE: we USE POST_PIXEL here so that we can draw use both 3D_POLYLINE_UNIFORM_COLOR + # NOTE: we USE POST_PIXEL here so that we can use both 3D_POLYLINE_UNIFORM_COLOR # and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL") @@ -1944,6 +1944,7 @@ class DecorationsHandler: self.decorators[object_type] = self.decorators["FALL"] def get_objects_and_decorators(self, collection): + # TODO: do it in data instead of the handler for performance? results = [] for obj in collection.all_objects: diff --git a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py index f452bcc158..ee0e5506e8 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py @@ -345,8 +345,12 @@ class SheetBuilder: def build_drawings(self, root, sheet): for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'): drawing_id = int(view.attrib["data-id"]) - reference = tool.Ifc.get().by_id(int(view.attrib["data-id"])) - drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"]) + try: + reference = tool.Ifc.get().by_id(int(view.attrib["data-id"])) + drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"]) + except: + # Perhaps the SVG has outdated content or is edited externally which we cannot control. + continue images = view.findall("{http://www.w3.org/2000/svg}image") @@ -387,8 +391,12 @@ class SheetBuilder: def build_schedules(self, root, sheet): for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'): - reference = tool.Ifc.get().by_id(int(view.attrib["data-id"])) - schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"])) + try: + reference = tool.Ifc.get().by_id(int(view.attrib["data-id"])) + schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"])) + except: + # Perhaps the SVG has outdated content or is edited externally which we cannot control. + continue images = view.findall("{http://www.w3.org/2000/svg}image") diff --git a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py index c6f2a1e1c9..29503856bc 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py @@ -32,6 +32,7 @@ classes = ( operator.OverrideDuplicateMoveLinkedMacro, operator.OverrideDuplicateMoveMacro, operator.OverrideJoin, + operator.OverrideMeshSeparate, operator.OverrideModeSetEdit, operator.OverrideModeSetObject, operator.OverrideOriginSet, @@ -53,6 +54,7 @@ classes = ( ui.BIM_PT_mesh, ui.BIM_PT_workarounds, ui.BIM_MT_object_set_origin, + ui.BIM_MT_separate, ) @@ -73,6 +75,7 @@ def register(): bpy.types.VIEW3D_MT_object.append(ui.object_menu) bpy.types.OUTLINER_MT_object.append(ui.outliner_menu) bpy.types.VIEW3D_MT_object_context_menu.append(ui.object_menu) + bpy.types.VIEW3D_MT_edit_mesh.append(ui.edit_mesh_menu) wm = bpy.context.window_manager if wm.keyconfigs.addon: km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY") @@ -116,6 +119,7 @@ def unregister(): bpy.types.OBJECT_PT_transform.remove(ui.BIM_PT_transform) bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu) bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu) + bpy.types.VIEW3D_MT_edit_mesh.remove(ui.edit_mesh_menu) del bpy.types.Scene.BIMGeometryProperties del bpy.types.Object.BIMGeometryProperties wm = bpy.context.window_manager diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index b8ed85a5d8..dcf63827b9 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -57,6 +57,39 @@ class EditObjectPlacement(bpy.types.Operator, Operator): core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) +class OverrideMeshSeparate(bpy.types.Operator, Operator): + bl_idname = "bim.override_mesh_separate" + bl_label = "IFC Mesh Separate" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + type: bpy.props.StringProperty() + + def _execute(self, context): + obj = context.active_object + + # You cannot separate meshes if the representation is mapped. + relating_type = tool.Root.get_element_type(tool.Ifc.get_entity(obj)) + if relating_type and tool.Root.does_type_have_representations(relating_type): + # We toggle edit mode to ensure that once representations are + # unmapped, our Blender mesh only has a single user. + tool.Blender.toggle_edit_mode(context) + bpy.ops.bim.unassign_type(related_object=obj.name) + tool.Blender.toggle_edit_mode(context) + + selected_objects = context.selected_objects + bpy.ops.mesh.separate(type=self.type) + bpy.ops.object.mode_set(mode="OBJECT", toggle=False) + new_objs = [obj] + for new_obj in context.selected_objects: + if new_obj == obj: + continue + # This is not very efficient, it needlessly copies the representations first. + blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj) + new_objs.append(new_obj) + for new_obj in new_objs: + bpy.ops.bim.update_representation(obj=new_obj.name) + + class OverrideOriginSet(bpy.types.Operator, Operator): bl_idname = "bim.override_origin_set" bl_label = "IFC Origin Set" @@ -88,8 +121,15 @@ class AddRepresentation(bpy.types.Operator, Operator): bl_options = {"REGISTER", "UNDO"} representation_conversion_method: bpy.props.EnumProperty( items=[ - ("OUTLINE", "Trace Outline", ""), - ("BOX", "Bounding Box", ""), + ("OUTLINE", "Trace Outline", "Traces outline by local XY axes, for Profile - by local XZ axes."), + ( + "BOX", + "Bounding Box", + "Creates a bounding box representation.\n" + "For Plan context - 2D bounding box by local XY axes,\n" + "for Profile - 2D bounding box by local XZ axes.\n" + "For other contexts - bounding box is 3d.", + ), ("PROJECT", "Full Representation", ""), ], name="Representation Conversion Method", @@ -499,6 +539,8 @@ class OverrideDelete(bpy.types.Operator): def _execute(self, context): if self.is_batch: ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get()) + + self.process_arrays(context) for obj in context.selected_objects: element = tool.Ifc.get_entity(obj) if element: @@ -508,6 +550,7 @@ class OverrideDelete(bpy.types.Operator): tool.Geometry.delete_ifc_object(obj) else: bpy.data.objects.remove(obj) + if self.is_batch: old_file = tool.Ifc.get() old_file.end_transaction() @@ -528,6 +571,30 @@ class OverrideDelete(bpy.types.Operator): data["old_file"].redo() tool.Ifc.set(data["new_file"]) + def process_arrays(self, context): + selected_objects = set(context.selected_objects) + array_parents = set() + for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if not element: + continue + pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") + if not pset: + continue + array_parents.add(tool.Ifc.get().by_guid(pset["Parent"])) + + for array_parent in array_parents: + array_parent_obj = tool.Ifc.get_object(array_parent) + data = [(i, data) for i, data in enumerate(tool.Blender.Modifier.Array.get_modifiers_data(array_parent))] + # NOTE: there is a way to remove arrays more precisely but it's more complex + for i, modifier_data in reversed(data): + children = set(tool.Blender.Modifier.Array.get_children_objects(modifier_data)) + if children.issubset(selected_objects): + with context.temp_override(active_object=array_parent_obj): + bpy.ops.bim.remove_array(item=i) + else: + break # allows to remove only n last layers of an array + class OverrideOutlinerDelete(bpy.types.Operator): bl_idname = "bim.override_outliner_delete" @@ -538,7 +605,7 @@ class OverrideOutlinerDelete(bpy.types.Operator): @classmethod def poll(cls, context): - return len(context.selected_ids) > 0 + return len(getattr(context, "selected_ids", [])) > 0 def execute(self, context): # In this override, we don't check self.hierarchy. This effectively @@ -653,6 +720,13 @@ class OverrideDuplicateMove(bpy.types.Operator): return len(context.selected_objects) > 0 def execute(self, context): + return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False) + + def _execute(self, context): + return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context) + + @staticmethod + def execute_duplicate_operator(self, context, linked=False): # Deep magick from the dawn of time if IfcStore.get_file(): IfcStore.execute_ifc_operator(self, context) @@ -663,7 +737,7 @@ class OverrideDuplicateMove(bpy.types.Operator): new_active_obj = None for obj in context.selected_objects: new_obj = obj.copy() - if obj.data: + if linked and obj.data: new_obj.data = obj.data.copy() if obj == context.active_object: new_active_obj = new_obj @@ -675,32 +749,50 @@ class OverrideDuplicateMove(bpy.types.Operator): context.view_layer.objects.active = new_active_obj return {"FINISHED"} - def _execute(self, context): + @staticmethod + def execute_ifc_duplicate_operator(self, context, linked=False): + objects_to_duplicate = set(context.selected_objects) + + # handle arrays + arrays_to_duplicate, array_children = OverrideDuplicateMove.process_arrays(self, context) + objects_to_duplicate -= array_children + for child in array_children: + child.select_set(False) + self.new_active_obj = None # Track decompositions so they can be recreated after the operation - relationships = tool.Root.get_decomposition_relationships(context.selected_objects) + relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate) old_to_new = {} - for obj in context.selected_objects: + + for obj in objects_to_duplicate: 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. + linked_non_ifc_object = linked and not element + # 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() temp_data = None - if obj.data: + + if obj.data and not linked_non_ifc_object: # assure root.copy_class won't replace the previous mesh globally temp_data = obj.data.copy() new_obj.data = temp_data + if obj == context.active_object: self.new_active_obj = new_obj for collection in obj.users_collection: collection.objects.link(new_obj) obj.select_set(False) new_obj.select_set(True) + + if linked_non_ifc_object: + continue + # clear object's collection so it will be able to have it's own new_obj.BIMObjectProperties.collection = None # copy the actual class @@ -711,17 +803,58 @@ class OverrideDuplicateMove(bpy.types.Operator): tool.Blender.remove_data_block(temp_data) if new: - array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array") - if array_pset: - array_pset = tool.Ifc.get().by_id(array_pset["id"]) - ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset) - old_to_new[tool.Ifc.get_entity(obj)] = [new] + # TODO: handle array data for other cases of duplication + array_data = arrays_to_duplicate.get(obj, None) + tool.Model.handle_array_on_copied_element(new, array_data) + if array_data: + for child in tool.Blender.Modifier.Array.get_all_children_objects(new): + child.select_set(True) + + # TODO: add new array children to recreate their decomposition too + old_to_new[element] = [new] if new.is_a("IfcRelSpaceBoundary"): tool.Boundary.decorate_boundary(new_obj) + # Recreate decompositions tool.Root.recreate_decompositions(relationships, old_to_new) blenderbim.bim.handler.refresh_ui_data() + @staticmethod + def process_arrays(self, context): + selected_objects = set(context.selected_objects) + array_parents = set() + arrays_to_create = dict() + array_children = set() # will be ignored during the duplication + + for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if not element: + continue + pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") + if not pset: + continue + array_parents.add(tool.Ifc.get().by_guid(pset["Parent"])) + + for array_parent in array_parents: + array_parent_obj = tool.Ifc.get_object(array_parent) + if array_parent_obj not in selected_objects: + continue + + array_data = [] + for modifier_data in tool.Blender.Modifier.Array.get_modifiers_data(array_parent): + children = set(tool.Blender.Modifier.Array.get_children_objects(modifier_data)) + if children.issubset(selected_objects): + modifier_data["children"] = [] + array_data.append(modifier_data) + array_children.update(children) + else: + break # allows to duplicate only n first layers of an array + + if array_data: + arrays_to_create[array_parent_obj] = array_data + + return arrays_to_create, array_children + class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro): bl_idname = "bim.override_object_duplicate_move_linked_macro" @@ -739,57 +872,10 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator): return len(context.selected_objects) > 0 def execute(self, context): - # Deep magick from the dawn of time - if IfcStore.get_file(): - IfcStore.execute_ifc_operator(self, context) - if self.new_active_obj: - context.view_layer.objects.active = self.new_active_obj - return {"FINISHED"} - - new_active_obj = None - for obj in context.selected_objects: - new_obj = obj.copy() - if obj == context.active_object: - new_active_obj = new_obj - for collection in obj.users_collection: - collection.objects.link(new_obj) - obj.select_set(False) - new_obj.select_set(True) - if new_active_obj: - context.view_layer.objects.active = new_active_obj - return {"FINISHED"} + return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=True) def _execute(self, context): - self.new_active_obj = None - # Track decompositions so they can be recreated after the operation - 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() - if obj == context.active_object: - self.new_active_obj = new_obj - for collection in obj.users_collection: - collection.objects.link(new_obj) - obj.select_set(False) - new_obj.select_set(True) - # Copy the actual class - new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj) - if new: - array_pset = ifcopenshell.util.element.get_pset(new, "BBIM_Array") - if array_pset: - array_pset = tool.Ifc.get().by_id(array_pset["id"]) - ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset) - old_to_new[tool.Ifc.get_entity(obj)] = new - # Recreate decompositions - tool.Root.recreate_decompositions(relationships, old_to_new) - blenderbim.bim.handler.refresh_ui_data() - return {"FINISHED"} + return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True) class OverrideDuplicateMoveAggregateMacro(bpy.types.Macro): @@ -809,28 +895,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator): return len(context.selected_objects) > 0 def execute(self, context): - # Deep magick from the dawn of time - if IfcStore.get_file(): - IfcStore.execute_ifc_operator(self, context) - if self.new_active_obj: - context.view_layer.objects.active = self.new_active_obj - return {"FINISHED"} - - new_active_obj = None - - for obj in context.selected_objects: - new_obj = obj.copy() - if obj.data: - new_obj.data = obj.data.copy() - if obj == context.active_object: - new_active_obj = new_obj - for collection in obj.users_collection: - collection.objects.link(new_obj) - obj.select_set(False) - new_obj.select_set(True) - if new_active_obj: - context.view_layer.objects.active = new_active_obj - return {"FINISHED"} + return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False) def _execute(self, context): self.new_active_obj = None @@ -1028,12 +1093,7 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator): ) if new_entity: - # Checks if the object belongs to an Ifc Array - array_pset = ifcopenshell.util.element.get_pset(new_entity, "BBIM_Array") - if array_pset: - array_pset = tool.Ifc.get().by_id(array_pset["id"]) - ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new_entity, pset=array_pset) - + tool.Model.handle_array_on_copied_element(new_entity) blenderbim.core.aggregate.unassign_object( tool.Ifc, tool.Aggregate, @@ -1068,6 +1128,17 @@ class OverrideDuplicateMoveAggregate(bpy.types.Operator): recreate_data_structure(new_root_entity) + # Remove connections with old objects + for new in old_to_new.values(): + for connection in new[0].ConnectedTo: + entity = connection.RelatedElement + if entity in old_to_new.keys(): + core.remove_connection(tool.Geometry, connection=connection) + for connection in new[0].ConnectedFrom: + entity = connection.RelatingElement + if entity in old_to_new.keys(): + core.remove_connection(tool.Geometry, connection=connection) + old_objs = [] for old, new in old_to_new.items(): old_objs.append(tool.Ifc.get_object(old)) @@ -1161,12 +1232,7 @@ class RefreshAggregate(bpy.types.Operator): ) if new_entity: - # Checks if the object belongs to an Ifc Array - array_pset = ifcopenshell.util.element.get_pset(new_entity, "BBIM_Array") - if array_pset: - array_pset = tool.Ifc.get().by_id(array_pset["id"]) - ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new_entity, pset=array_pset) - + tool.Model.handle_array_on_copied_element(new_entity) blenderbim.core.aggregate.unassign_object( tool.Ifc, tool.Aggregate, @@ -1180,22 +1246,33 @@ class RefreshAggregate(bpy.types.Operator): return new_entity if len(context.selected_objects) != 1: + self.report({"INFO"}, "Only 1 object need to be selected.") return {"FINISHED"} selected_root_obj = context.selected_objects[0] selected_root_entity = tool.Ifc.get_entity(selected_root_obj) - if not selected_root_entity.is_a("IfcElementAssembly"): + if selected_root_entity.is_a("IfcElementAssembly"): + pass + elif selected_root_entity.Decomposes: + if selected_root_entity.Decomposes[0].RelatingObject.is_a("IfcElementAssembly"): + selected_root_entity = selected_root_entity.Decomposes[0].RelatingObject + selected_root_obj = tool.Ifc.get_object(selected_root_entity) + else: + self.report({"INFO"}, "Object is not part of a IfcElementAssembly.") return {"FINISHED"} + pset = ifcopenshell.util.element.get_pset(selected_root_entity, "BBIM_Aggregate_Data") if not pset: + self.report({"INFO"}, "Object is not part of an assembly aggregate.") return {"FINISHED"} pset_data = json.loads(pset["Data"])[0] instance_of = pset_data["instance_of"][0] original_root_entity = tool.Ifc.get().by_guid(instance_of) if original_root_entity == selected_root_entity: + self.report({"INFO"}, "Cannot refresh original assembly. Select an assembly instance.") return {"FINISHED"} parents = remove_objects(selected_root_entity) @@ -1208,13 +1285,25 @@ class RefreshAggregate(bpy.types.Operator): for parent in parents: duplicate_children(parent) + + # Remove connections with old objects + for new in old_to_new.values(): + for connection in new[0].ConnectedTo: + entity = connection.RelatedElement + if entity in old_to_new.keys(): + core.remove_connection(tool.Geometry, connection=connection) + for connection in new[0].ConnectedFrom: + entity = connection.RelatingElement + if entity in old_to_new.keys(): + core.remove_connection(tool.Geometry, connection=connection) + old_objs = [] for old, new in old_to_new.items(): old_objs.append(tool.Ifc.get_object(old)) new_obj = tool.Ifc.get_object(new[0]) - matrix_diff = new_obj.matrix_world @ original_matrix + matrix_diff = Matrix.inverted(original_matrix) @ new_obj.matrix_world new_matrix = selected_matrix @ matrix_diff new_obj.matrix_world = new_matrix diff --git a/src/blenderbim/blenderbim/bim/module/geometry/ui.py b/src/blenderbim/blenderbim/bim/module/geometry/ui.py index 92a8167443..a91cf340ee 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/ui.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/ui.py @@ -32,6 +32,21 @@ def object_menu(self, context): self.layout.menu("BIM_MT_object_set_origin", icon="PLUGIN") +def edit_mesh_menu(self, context): + self.layout.separator() + self.layout.menu("BIM_MT_separate", icon="PLUGIN") + + +class BIM_MT_separate(Menu): + bl_idname = "BIM_MT_separate" + bl_label = "IFC Separate" + + def draw(self, context): + self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC Selection").type = "SELECTED" + self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Material").type = "MATERIAL" + self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Loose Parts").type = "LOOSE" + + class BIM_MT_object_set_origin(Menu): bl_idname = "BIM_MT_object_set_origin" bl_label = "IFC Set Origin" diff --git a/src/blenderbim/blenderbim/bim/module/misc/operator.py b/src/blenderbim/blenderbim/bim/module/misc/operator.py index 3de1e0846e..6806c3635a 100644 --- a/src/blenderbim/blenderbim/bim/module/misc/operator.py +++ b/src/blenderbim/blenderbim/bim/module/misc/operator.py @@ -22,6 +22,7 @@ import ifcopenshell import blenderbim.bim.handler import blenderbim.tool as tool import blenderbim.core.misc as core +import blenderbim.core.geometry as core_geometry from blenderbim.bim.ifc import IfcStore from mathutils import Vector, Matrix, Euler @@ -140,7 +141,51 @@ class SplitAlongEdge(bpy.types.Operator, Operator): return context.selected_objects and tool.Ifc.get() def _execute(self, context): - core.split_along_edge(tool.Misc, cutter=context.active_object, objs=context.selected_objects) + cutter = context.active_object + objs = [o for o in context.selected_objects if o != cutter] + + # Splitting only works on meshes + for obj in objs: + # You cannot split meshes if the representation is mapped. + element = tool.Ifc.get_entity(obj) + if element: + relating_type = tool.Root.get_element_type(element) + if relating_type and tool.Root.does_type_have_representations(relating_type): + bpy.ops.bim.unassign_type(related_object=obj.name) + + representation = tool.Geometry.get_active_representation(obj) + core_geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + apply_openings=False, + ) + + if not tool.Geometry.is_meshlike(representation): + bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="IfcTessellatedFaceSet") + + new_objs = tool.Misc.split_objects_with_cutter(objs, cutter) + for obj in new_objs: + blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj) + bpy.ops.bim.update_representation(obj=obj.name) + for obj in objs: + bpy.ops.bim.update_representation(obj=obj.name) + + representation = tool.Geometry.get_active_representation(obj) + core_geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + apply_openings=True, + ) class GetConnectedSystemElements(bpy.types.Operator, Operator): @@ -205,46 +250,50 @@ class DrawSystemArrows(bpy.types.Operator, Operator): return context.selected_objects and tool.Ifc.get() def _execute(self, context): - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - curve = bpy.data.objects.new("System Arrows", bpy.data.curves.new("System Arrows", "CURVE")) - curve.data.dimensions = "3D" - context.scene.collection.objects.link(curve) + sinks = [] + sources = [] + for obj in bpy.context.selected_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue + element = tool.Ifc.get_entity(obj) - sources = [] - sinks = [] - for rel in getattr(element, "HasPorts", []) or []: - if rel.RelatingPort.FlowDirection == "SOURCE": - sources.append( - self.get_absolute_matrix( - ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement) - ) - ) - elif rel.RelatingPort.FlowDirection == "SINK": - sinks.append( - self.get_absolute_matrix( - ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement) - ) - ) + sources_current = [] + sinks_current = [] + + for port in tool.System.get_ports(element): + local_placement = ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement) + m = self.get_absolute_matrix(local_placement) + if port.FlowDirection == "SOURCE": + sources_current.append(m) + elif port.FlowDirection == "SINK": + sinks_current.append(m) else: - sources.append( - self.get_absolute_matrix( - ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement) - ) - ) - sinks.append( - self.get_absolute_matrix( - ifcopenshell.util.placement.get_local_placement(rel.RelatingPort.ObjectPlacement) - ) - ) - for sink in sinks: - for source in sources: + sources_current.append(m) + sinks_current.append(m) + + if sinks_current or sources_current: + sinks.append(sinks_current) + sources.append(sources_current) + + if not sinks: + self.report({"INFO"}, "No sinks/sources found for selected objects.") + return {"FINISHED"} + + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + curve = bpy.data.objects.new("System Arrows", bpy.data.curves.new("System Arrows", "CURVE")) + curve.data.dimensions = "3D" + curve.show_in_front = True + context.scene.collection.objects.link(curve) + + for i in range(len(sinks)): + for sink in sinks[i]: + for source in sources[i]: polyline = curve.data.splines.new("POLY") polyline.points.add(1) polyline.points[0].co = (Matrix(sink).translation * unit_scale).to_4d() polyline.points[1].co = (Matrix(source).translation * unit_scale).to_4d() + tool.Blender.select_and_activate_single_object(context, curve) def get_absolute_matrix(self, matrix): props = bpy.context.scene.BIMGeoreferenceProperties diff --git a/src/blenderbim/blenderbim/bim/module/model/array.py b/src/blenderbim/blenderbim/bim/module/model/array.py index 5b1606154b..087bf1e436 100644 --- a/src/blenderbim/blenderbim/bim/module/model/array.py +++ b/src/blenderbim/blenderbim/bim/module/model/array.py @@ -206,15 +206,29 @@ class SelectArrayParent(bpy.types.Operator): bl_idname = "bim.select_array_parent" bl_label = "Select Array Parent" bl_options = {"REGISTER", "UNDO"} - parent: bpy.props.StringProperty(description="Parent Element GUID") + + @classmethod + def poll(cls, context): + if not context.active_object: + cls.poll_message_set("No active object selected") + return False + return True def execute(self, context): - try: - element = tool.Ifc.get().by_guid(self.parent) - except: - self.report({"ERROR"}, f"Couldn't find array parent by guid '{self.parent}'") + object = context.active_object + element = tool.Ifc.get_entity(object) + array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") + if not array_pset: + self.report({"ERROR"}, f"Object is not part of an array.") return {"CANCELLED"} - obj = tool.Ifc.get_object(element) + + try: + parent_element = tool.Ifc.get().by_guid(array_pset["Parent"]) + except: + self.report({"ERROR"}, f"Couldn't find array parent by guid '{array_pset['Parent']}'") + return {"CANCELLED"} + + obj = tool.Ifc.get_object(parent_element) if obj: tool.Blender.select_and_activate_single_object(context, active_object=obj) return {"FINISHED"} @@ -224,13 +238,26 @@ class SelectAllArrayObjects(bpy.types.Operator): bl_idname = "bim.select_all_array_objects" bl_label = "Select All Array Objects" bl_options = {"REGISTER", "UNDO"} - parent: bpy.props.StringProperty(description="Parent Element GUID") + + @classmethod + def poll(cls, context): + if not context.active_object: + cls.poll_message_set("No active object selected") + return False + return True def execute(self, context): + object = context.active_object + element = tool.Ifc.get_entity(object) + array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") + if not array_pset: + self.report({"ERROR"}, f"Object is not part of an array.") + return {"CANCELLED"} + try: - parent_element = tool.Ifc.get().by_guid(self.parent) + parent_element = tool.Ifc.get().by_guid(array_pset["Parent"]) except RuntimeError: - self.report({"ERROR"}, f"Couldn't find array parent by guid '{self.parent}'") + self.report({"ERROR"}, f"Couldn't find array parent by guid '{array_pset['Parent']}'") return {"CANCELLED"} array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element) diff --git a/src/blenderbim/blenderbim/bim/module/model/door.py b/src/blenderbim/blenderbim/bim/module/model/door.py index e470b3e3ac..d51ba058a1 100644 --- a/src/blenderbim/blenderbim/bim/module/model/door.py +++ b/src/blenderbim/blenderbim/bim/module/model/door.py @@ -68,6 +68,14 @@ def update_door_modifier_representation(context, obj): }, } + def get_active_representation_context(obj): + active_representation = tool.Geometry.get_active_representation(obj) + if active_representation: + return active_representation.ContextOfItems + return ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + + previously_active_context = get_active_representation_context(obj) + # ELEVATION_VIEW representation profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW") if profile: @@ -78,6 +86,7 @@ def update_door_modifier_representation(context, obj): tool.Model.replace_object_ifc_representation(profile, obj, elevation_representation) # MODEL_VIEW representation + # (Model/Body defined only BEFORE Plan/Body to prevent #2744) body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") representation_data["context"] = body model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data) @@ -113,14 +122,34 @@ def update_door_modifier_representation(context, obj): ) tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation) - if plan_body or plan_annotation: - # adding switch representation at the end instead of changing order of representations - # to prevent #2744 - core.switch_representation( + # adding switch representation at the end instead of changing order of representations + # to prevent #2744 + if get_active_representation_context(obj) != previously_active_context: + previously_active_representation = ifcopenshell.util.representation.get_representation( + element, + previously_active_context.ContextType, + previously_active_context.ContextIdentifier, + previously_active_context.TargetView, + ) + + if not previously_active_representation: + # we assume there is no representation because it was + # Plan/Annotation/PLAN_VIEW + previously_active_context = ifcopenshell.util.representation.get_context( + ifc_file, "Plan", "Body", "PLAN_VIEW" + ) + previously_active_representation = ifcopenshell.util.representation.get_representation( + element, + previously_active_context.ContextType, + previously_active_context.ContextIdentifier, + previously_active_context.TargetView, + ) + + blenderbim.core.geometry.switch_representation( tool.Ifc, tool.Geometry, obj=obj, - representation=model_representation, + representation=previously_active_representation, should_reload=True, is_global=True, should_sync_changes_first=True, diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index 4a8669d186..9d03590f6a 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -847,6 +847,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): ) body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") tool.Model.replace_object_ifc_representation(body, obj, rep) + tool.Blender.remove_data_block(mesh) pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=transition_type, name="BBIM_Fitting") ifcopenshell.api.run( "pset.edit_pset", @@ -1188,6 +1189,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): ) body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") tool.Model.replace_object_ifc_representation(body, obj, rep) + tool.Blender.remove_data_block(mesh) pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=bend_type, name="BBIM_Fitting") ifcopenshell.api.run( "pset.edit_pset", diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index 0d56fc63e4..8d67810409 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -185,8 +185,13 @@ class AddConstrTypeInstance(bpy.types.Operator): collection_obj = collection.BIMCollectionProperties.obj bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class) + tool.Blender.remove_data_block(mesh) # Remove "Instance" mesh + + mesh_data = obj.data element = tool.Ifc.get_entity(obj) blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) + if obj.data != mesh_data: # remove orphaned mesh from "bim.assign_class" + tool.Blender.remove_data_block(mesh_data) # Update required as core.type.assign_type may change obj.data # TODO: This is inefficient. It literally creates a mesh, then potentially removes it. diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index 8bac8d9199..d2576f1185 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -132,6 +132,7 @@ class DumbProfileGenerator: is_global=True, should_sync_changes_first=False, ) + tool.Blender.remove_data_block(mesh) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"}) diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py index f349aec433..b3cbe091b4 100644 --- a/src/blenderbim/blenderbim/bim/module/model/slab.py +++ b/src/blenderbim/blenderbim/bim/module/model/slab.py @@ -196,6 +196,7 @@ class DumbSlabGenerator: is_global=True, should_sync_changes_first=False, ) + tool.Blender.remove_data_block(mesh) if self.footprint_context: extrusion = tool.Model.get_extrusion(representation) diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 6a0b8005f8..07cd3ec4bd 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -188,10 +188,8 @@ class BIM_PT_array(bpy.types.Panel): if ArrayData.data["parameters"]: row = self.layout.row(align=True) row.label(text=ArrayData.data["parameters"]["parent_name"], icon="CON_CHILDOF") - op = row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="") - op.parent = ArrayData.data["parameters"]["Parent"] - op = row.operator("bim.select_all_array_objects", icon="RESTRICT_SELECT_OFF", text="") - op.parent = ArrayData.data["parameters"]["Parent"] + row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="") + row.operator("bim.select_all_array_objects", icon="RESTRICT_SELECT_OFF", text="") if ArrayData.data["parameters"]["data_dict"]: row.operator("bim.add_array", icon="ADD", text="") diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index e2aa7e6a56..54b81214a0 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -637,6 +637,7 @@ class DumbWallGenerator: is_global=True, should_sync_changes_first=False, ) + tool.Blender.remove_data_block(mesh) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbLayer2"}) obj.select_set(True) @@ -827,10 +828,10 @@ class DumbWallJoiner: return for rel in element1.ConnectedTo: - if rel.RelatingConnectionType in ["ATSTART", "ATEND"]: + if rel.is_a("IfcRelConnectsPathElements") and rel.RelatingConnectionType in ["ATSTART", "ATEND"]: rel.RelatingConnectionType = "ATSTART" if rel.RelatingConnectionType == "ATEND" else "ATEND" for rel in element1.ConnectedFrom: - if rel.RelatedConnectionType in ["ATSTART", "ATEND"]: + if rel.is_a("IfcRelConnectsPathElements") and rel.RelatedConnectionType in ["ATSTART", "ATEND"]: rel.RelatedConnectionType = "ATSTART" if rel.RelatedConnectionType == "ATEND" else "ATEND" layers1 = tool.Model.get_material_layer_parameters(element1) diff --git a/src/blenderbim/blenderbim/bim/module/model/window.py b/src/blenderbim/blenderbim/bim/module/model/window.py index 24499ee6b6..e432be6797 100644 --- a/src/blenderbim/blenderbim/bim/module/model/window.py +++ b/src/blenderbim/blenderbim/bim/module/model/window.py @@ -81,6 +81,8 @@ def update_simple_openings(element, opening_width, opening_height): has_replaced_opening_representation = True tool.Model.reload_body_representation(voided_objs) + with bpy.context.temp_override(selected_objects=[tool.Ifc.get_object(f) for f in fillings]): + bpy.ops.bim.recalculate_fill() def update_window_modifier_representation(context, obj): @@ -116,6 +118,14 @@ def update_window_modifier_representation(context, obj): } representation_data["panel_properties"].append(panel_data) + def get_active_representation_context(obj): + active_representation = tool.Geometry.get_active_representation(obj) + if active_representation: + return active_representation.ContextOfItems + return ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") + + previously_active_context = get_active_representation_context(obj) + # ELEVATION_VIEW representation profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW") if profile: @@ -126,6 +136,7 @@ def update_window_modifier_representation(context, obj): tool.Model.replace_object_ifc_representation(profile, obj, elevation_representation) # MODEL_VIEW representation + # (Model/Body defined only BEFORE Plan/Body to prevent #2744) body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") representation_data["context"] = body model_representation = ifcopenshell.api.run("geometry.add_window_representation", ifc_file, **representation_data) @@ -140,13 +151,20 @@ def update_window_modifier_representation(context, obj): ) tool.Model.replace_object_ifc_representation(plan, obj, plan_representation) - # adding switch representation at the end instead of changing order of representations - # to prevent #2744 + # adding switch representation at the end instead of changing order of representations + # to prevent #2744 + if get_active_representation_context(obj) != previously_active_context: + previously_active_representation = ifcopenshell.util.representation.get_representation( + element, + previously_active_context.ContextType, + previously_active_context.ContextIdentifier, + previously_active_context.TargetView, + ) blenderbim.core.geometry.switch_representation( tool.Ifc, tool.Geometry, obj=obj, - representation=model_representation, + representation=previously_active_representation, should_reload=True, is_global=True, should_sync_changes_first=True, diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index f8e3d19c0c..6af2600dee 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -347,14 +347,21 @@ class BimToolUI: "IfcDuctSegment", "IfcPipeSegment", ): - add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "") add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "") + add_layout_hotkey_operator( + cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__ + ) + if context.region.type != "TOOL_HEADER": + cls.layout.operator("bim.mep_add_bend") + cls.layout.operator("bim.mep_add_transition") + cls.layout.operator("bim.mep_add_obstruction") + cls.layout.operator("bim.mep_connect_elements") else: add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "") add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "") add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "") add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__) - add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__) + add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__) row.operator("bim.extend_profile", icon="X", text="").join_type = "" elif ( @@ -427,6 +434,11 @@ class BimToolUI: add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings") add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition") + cls.layout.separator() + add_layout_hotkey_operator( + cls.layout, "Calculate All Quantities", "S_Q", bpy.ops.bim.calculate_all_quantities.__doc__ + ) + @classmethod def draw_header_interface(cls): cls.draw_type_selection_interface() @@ -667,7 +679,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if self.active_material_usage == "LAYER2": bpy.ops.bim.recalculate_wall() elif self.active_material_usage == "PROFILE": - bpy.ops.bim.recalculate_profile() + if self.active_class in ( + "IfcCableCarrierSegment", + "IfcCableSegment", + "IfcDuctSegment", + "IfcPipeSegment", + ): + bpy.ops.bim.regenerate_distribution_element() + else: + bpy.ops.bim.recalculate_profile() elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"): bpy.ops.bim.recalculate_fill() elif self.active_class in ("IfcSpace"): diff --git a/src/blenderbim/blenderbim/bim/module/pset/calc_quantity_function_mapper.py b/src/blenderbim/blenderbim/bim/module/pset/calc_quantity_function_mapper.py index d366033073..c55b78b00b 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/calc_quantity_function_mapper.py +++ b/src/blenderbim/blenderbim/bim/module/pset/calc_quantity_function_mapper.py @@ -71,10 +71,10 @@ mapper = { 'Area' : "get_net_side_area", }, 'Qto_DuctSegmentBaseQuantities' : { - 'Length' : None, + 'Length' : "get_length", 'GrossCrossSectionArea' : None, 'NetCrossSectionArea' : None, - 'OuterSurfaceArea' : None, + 'OuterSurfaceArea' : "get_outer_surface_area", 'GrossWeight' : None, }, 'Qto_TransformerBaseQuantities' : { @@ -203,10 +203,10 @@ mapper = { 'Weight' : None, }, 'Qto_DuctFittingBaseQuantities' : { - 'Length' : None, + 'Length' : "get_length", 'GrossCrossSectionArea' : None, 'NetCrossSectionArea' : None, - 'OuterSurfaceArea' : None, + 'OuterSurfaceArea' : "get_outer_surface_area", 'GrossWeight' : None, }, 'Qto_UnitaryControlElementBaseQuantities' : { diff --git a/src/blenderbim/blenderbim/bim/module/pset/notes_about_mapped_calculated_quantities.txt b/src/blenderbim/blenderbim/bim/module/pset/notes_about_mapped_calculated_quantities.txt index fb56c86ad0..73acaecd2f 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/notes_about_mapped_calculated_quantities.txt +++ b/src/blenderbim/blenderbim/bim/module/pset/notes_about_mapped_calculated_quantities.txt @@ -31,6 +31,9 @@ NetFloorArea: it doesn't count the following entities contained in the spatial e NetVolume: like NetFloorArea, it doesn't count the following entities contained in the spatial entity: IfcColumn and IfcWall Also, the entire IfcColumn (or IfcWall) object volume is substracted, so it should be better to substract only the shared volume between IfcSpace and IfcColumn. Look at todo list +DUCT SEGMENTS AND DUCT FITTINGS +The length is calculated like a beam. Also outer surface area. Gross cross section area, net cross section area and weight are not calculated right now because the parametrically cross section area seems filled (without hole). + WEIGHT The object weight is calculated by multiplying the object mass density with the object volume (net or gross). It's only calculated if the object material has a MassDensity property in the Pset_MaterialCommon. diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index adbb9cfbcd..340a6b9f8c 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -223,7 +223,7 @@ class EnablePsetEditing(bpy.types.Operator): new.is_selected = enum in selected_enum_items else: if prop.is_a("IfcPropertySingleValue"): - value = prop.NominalValue.wrappedValue + value = prop.NominalValue.wrappedValue if prop.NominalValue else None elif prop.is_a("IfcPhysicalSimpleQuantity"): value = prop[3] new_prop = self.props.properties.add() @@ -356,24 +356,7 @@ class AddPset(bpy.types.Operator, Operator): obj_type: bpy.props.StringProperty() def _execute(self, context): - self.file = IfcStore.get_file() - pset_name = get_pset_props(context, self.obj, self.obj_type).pset_name - if self.obj_type == "Object": - if context.selected_objects: - objects = [o.name for o in tool.Blender.get_selected_objects()] - else: - objects = [context.active_object.name] - else: - objects = [self.obj] - for obj in objects: - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) - if not ifc_definition_id: - continue - element = tool.Ifc.get().by_id(ifc_definition_id) - if pset_name in blenderbim.bim.schema.ifc.psetqto.get_applicable_names(element.is_a(), pset_only=True): - bpy.ops.bim.enable_pset_editing( - pset_id=0, pset_name=pset_name, pset_type="PSET", obj=obj, obj_type=self.obj_type - ) + core.add_pset(tool.Ifc, tool.Pset, tool.Blender, obj_name=self.obj, obj_type=self.obj_type) class AddQto(bpy.types.Operator, Operator): diff --git a/src/blenderbim/blenderbim/bim/module/resource/__init__.py b/src/blenderbim/blenderbim/bim/module/resource/__init__.py index 8512a370dd..8ab1410e3f 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/resource/__init__.py @@ -49,7 +49,6 @@ classes = ( operator.ExpandResource, operator.GoToResource, operator.ImportResources, - operator.LoadResourceProperties, operator.LoadResources, operator.RemoveResource, operator.RemoveResourceQuantity, diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index fbdd4374f7..9d64cdb0dd 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -33,19 +33,6 @@ class LoadResources(bpy.types.Operator): return {"FINISHED"} -class LoadResourceProperties(bpy.types.Operator): - bl_idname = "bim.load_resource_properties" - bl_label = "Load Resource Properties" - bl_options = {"REGISTER", "UNDO"} - resource: bpy.props.IntProperty() - - def execute(self, context): - core.load_resource_properties( - tool.Resource, resource=tool.Ifc.get().by_id(self.resource) if self.resource else None - ) - return {"FINISHED"} - - class AddResource(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_resource" bl_label = "Add Resource" @@ -437,4 +424,4 @@ class CalculateResourceUsage(bpy.types.Operator, tool.Ifc.Operator): return False def _execute(self, context): - core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(tool.Resource.get_highlighted_resource())) + core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Resource.get_highlighted_resource()) diff --git a/src/blenderbim/blenderbim/bim/module/resource/prop.py b/src/blenderbim/blenderbim/bim/module/resource/prop.py index 9ea20d446c..2f7e539eb9 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/prop.py +++ b/src/blenderbim/blenderbim/bim/module/resource/prop.py @@ -81,8 +81,7 @@ def update_active_resource_index(self, context): def updateResourceUsage(self, context): - props = context.scene.BIMResourceProperties - if not props.is_resource_update_enabled: + if not context.scene.BIMResourceProperties.is_resource_update_enabled: return if not self.schedule_usage: return diff --git a/src/blenderbim/blenderbim/bim/module/search/__init__.py b/src/blenderbim/blenderbim/bim/module/search/__init__.py index 3dd6435f99..da8076d64d 100644 --- a/src/blenderbim/blenderbim/bim/module/search/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/search/__init__.py @@ -20,7 +20,7 @@ import bpy from . import ui, prop, operator classes = ( - operator.ActivateIfcBuildingStoreyFilter, + operator.ActivateContainerFilter, operator.ActivateIfcClassFilter, operator.AddFilter, operator.AddFilterGroup, @@ -58,6 +58,7 @@ classes = ( prop.SearchQueryGroup, prop.IfcSelectorProperties, ui.BIM_PT_search, + ui.BIM_PT_filter, ui.BIM_PT_colour_by_property, ui.BIM_PT_select_similar, ui.BIM_UL_colourscheme, diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py index c929938632..ee018ccef8 100644 --- a/src/blenderbim/blenderbim/bim/module/search/operator.py +++ b/src/blenderbim/blenderbim/bim/module/search/operator.py @@ -466,8 +466,8 @@ class ToggleFilterSelection(Operator): if props.filter_type == "CLASSES": for ifc_class in props.filter_classes: ifc_class.is_selected = self.selecting_actionbool - elif props.filter_type == "BUILDINGSTOREYS": - for building_storey in props.filter_building_storeys: + elif props.filter_type == "CONTAINER": + for building_storey in props.filter_container: building_storey.is_selected = self.selecting_actionbool return {"FINISHED"} @@ -525,11 +525,11 @@ class ActivateIfcClassFilter(Operator): row.operator("bim.toggle_filter_selection", text="Deselect All").action = "DESELECT" -class ActivateIfcBuildingStoreyFilter(Operator): +class ActivateContainerFilter(Operator): """Filter the current selection by Building Storey""" - bl_idname = "bim.activate_ifc_building_storey_filter" - bl_label = "Filter by Building Storey" + bl_idname = "bim.activate_ifc_container_filter" + bl_label = "Filter by Container" @classmethod def poll(cls, context): @@ -540,27 +540,29 @@ class ActivateIfcBuildingStoreyFilter(Operator): def invoke(self, context, event): props = bpy.context.scene.BIMSearchProperties - props.filter_building_storeys.clear() + props.filter_container.clear() - ifc_building_storeys = {} + containers = {} + containers.setdefault("None", 0) for obj in context.selected_objects: - storey = tool.Misc.get_object_storey(obj) - if not storey: + container = tool.Spatial.get_container(tool.Ifc.get_entity(obj)) + if not container: + containers["None"] += 1 continue - ifc_building_storeys.setdefault(storey.Name, 0) - ifc_building_storeys[storey.Name] += 1 + containers.setdefault(container.Name, 0) + containers[container.Name] += 1 - for name, total in dict(sorted(ifc_building_storeys.items())).items(): - new = props.filter_building_storeys.add() + for name, total in dict(sorted(containers.items())).items(): + new = props.filter_container.add() new.name = name new.total = total - props.filter_type = "BUILDINGSTOREYS" + props.filter_type = "CONTAINER" return context.window_manager.invoke_props_dialog(self, width=250) def execute(self, context): - bpy.context.scene.BIMSearchProperties.filter_building_storeys.clear() + bpy.context.scene.BIMSearchProperties.filter_container.clear() return {"FINISHED"} def draw(self, context): @@ -568,12 +570,12 @@ class ActivateIfcBuildingStoreyFilter(Operator): "BIM_UL_ifc_building_storey_filter", "", context.scene.BIMSearchProperties, - "filter_building_storeys", + "filter_container", context.scene.BIMSearchProperties, - "filter_building_storeys_index", + "filter_container_index", rows=20 - if len(bpy.context.scene.BIMSearchProperties.filter_building_storeys) > 20 - else len(bpy.context.scene.BIMSearchProperties.filter_building_storeys), + if len(bpy.context.scene.BIMSearchProperties.filter_container) > 20 + else len(bpy.context.scene.BIMSearchProperties.filter_container), ) row = self.layout.row(align=True) row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT" diff --git a/src/blenderbim/blenderbim/bim/module/search/prop.py b/src/blenderbim/blenderbim/bim/module/search/prop.py index bdcc3d223c..53b5db1554 100644 --- a/src/blenderbim/blenderbim/bim/module/search/prop.py +++ b/src/blenderbim/blenderbim/bim/module/search/prop.py @@ -70,15 +70,15 @@ def update_is_class_selected(self, context): new.obj = obj -def update_is_level_selected(self, context): +def update_is_container_selected(self, context): if self.is_selected: for obj in self.unselected_objects: obj.obj.select_set(True) self.unselected_objects.clear() else: for obj in context.selected_objects: - level = tool.Misc.get_object_storey(obj) - if level and level.Name == self.name: + container = tool.Spatial.get_container(tool.Ifc.get_entity(obj)) + if (container and container.Name == self.name) or (not container and self.name== "None"): obj.select_set(False) new = self.unselected_objects.add() new.obj = obj @@ -93,7 +93,7 @@ class BIMFilterClasses(PropertyGroup): class BIMFilterBuildingStoreys(PropertyGroup): name: StringProperty(name="Name") - is_selected: BoolProperty(name="Is Level Selected", default=True, update=update_is_level_selected) + is_selected: BoolProperty(name="Is Level Selected", default=True, update=update_is_container_selected) total: IntProperty(name="Total") unselected_objects: CollectionProperty(type=ObjProperty, name="Unfiltered Objects") @@ -140,8 +140,8 @@ class BIMSearchProperties(PropertyGroup): filter_type: StringProperty(name="Filter Type") filter_classes: CollectionProperty(type=BIMFilterClasses, name="Filter Classes") filter_classes_index: IntProperty(name="Filter Classes Index") - filter_building_storeys: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level") - filter_building_storeys_index: IntProperty(name="Filter Level Index") + filter_container: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level") + filter_container_index: IntProperty(name="Filter Level Index") def get_classes(self, ifc_product): diff --git a/src/blenderbim/blenderbim/bim/module/search/ui.py b/src/blenderbim/blenderbim/bim/module/search/ui.py index 706f5952e9..a2a24c7b68 100644 --- a/src/blenderbim/blenderbim/bim/module/search/ui.py +++ b/src/blenderbim/blenderbim/bim/module/search/ui.py @@ -42,11 +42,21 @@ class BIM_PT_search(Panel): row = self.layout.row(align=True) row.operator("bim.search", text="Search", icon="VIEWZOOM") - return # Temporary for now whilst searching is being upgraded. + return + +class BIM_PT_filter(Panel): + bl_label = "Filter Selection" + bl_idname = "BIM_PT_filter" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_tab_grouping_and_filtering" + + def draw(self, context): row = self.layout.row(align=True) row.operator("bim.activate_ifc_class_filter", icon="FILTER") - row.operator("bim.activate_ifc_building_storey_filter", icon="FILTER") + row.operator("bim.activate_ifc_container_filter", icon="FILTER") class BIM_PT_colour_by_property(Panel): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index e66d492f3a..a7e6e1ea97 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -372,6 +372,7 @@ class EditTaskTime(bpy.types.Operator, tool.Ifc.Operator): core.edit_task_time( tool.Ifc, tool.Sequence, + tool.Resource, task_time=tool.Ifc.get().by_id(context.scene.BIMWorkScheduleProperties.active_task_time_id), ) @@ -508,7 +509,7 @@ class AssignProcess(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): if self.related_object_type == "RESOURCE": - core.assign_resource(tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task)) + core.assign_resource(tool.Ifc, tool.Sequence, tool.Resource, task=tool.Ifc.get().by_id(self.task)) elif self.related_object_type == "PRODUCT": if self.related_object: core.assign_input_products( @@ -541,6 +542,7 @@ class UnassignProcess(bpy.types.Operator): core.unassign_resource( tool.Ifc, tool.Sequence, + tool.Resource, task=tool.Ifc.get().by_id(self.task), resource=tool.Ifc.get().by_id(self.resource), ) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 83c48d5a02..30dc776cf7 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -224,10 +224,8 @@ def updateTaskDuration(self, context): else: task_time = tool.Ifc.run("sequence.add_task_time", task=task) tool.Ifc.run("sequence.edit_task_time", task_time=task_time, attributes={"ScheduleDuration": duration}) - SequenceData.load() blenderbim.core.sequence.load_task_properties(tool.Sequence) - bpy.ops.bim.load_task_properties() - tool.Sequence.load_resources() + tool.Sequence.refresh_task_resources() def get_schedule_predefined_types(self, context): @@ -331,22 +329,24 @@ def get_saved_color_schemes(self, context): def updateAssignedResourceName(self, context): pass + def updateAssignedResourceUsage(self, context): + if not context.scene.BIMResourceProperties.is_resource_update_enabled: + return if not self.schedule_usage: return resource = tool.Ifc.get().by_id(self.ifc_definition_id) if resource.Usage and resource.Usage.ScheduleUsage == self.schedule_usage: return - tool.Resource.run_edit_resource_time(resource, attributes={ - "ScheduleUsage": self.schedule_usage - }) + tool.Resource.run_edit_resource_time(resource, attributes={"ScheduleUsage": self.schedule_usage}) tool.Sequence.load_task_properties() tool.Resource.load_resource_properties() tool.Sequence.refresh_task_resources() blenderbim.bim.module.resource.data.refresh() - blenderbim.bim.module.sequence.data.refresh() + refresh_sequence_data() blenderbim.bim.module.pset.data.refresh() + class Task(PropertyGroup): name: StringProperty(name="Name", update=updateTaskName) identification: StringProperty(name="Identification", update=updateTaskIdentification) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 50542ffb19..850f507785 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -727,7 +727,11 @@ class BIM_PT_task_icom(Panel): if total_task_outputs: op = row2.operator("bim.unassign_product", icon="REMOVE", text="") op.task = task.ifc_definition_id - if not context.selected_objects and self.props.active_task_output_index < total_task_outputs: + if ( + total_task_outputs + and not context.selected_objects + and self.props.active_task_output_index < total_task_outputs + ): output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id op.relating_product = output_id @@ -770,6 +774,7 @@ class BIM_UL_task_resources(UIList): row.prop(item, "name", emboss=False, text="") row.prop(item, "schedule_usage", emboss=False, text="") + class BIM_UL_animation_colors(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py index a57b7fb5a8..ea95b8965e 100644 --- a/src/blenderbim/blenderbim/bim/module/system/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py @@ -17,7 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy -from . import ui, prop, operator +from . import ui, prop, operator, decorator classes = ( operator.AddPort, @@ -51,7 +51,9 @@ classes = ( def register(): bpy.types.Scene.BIMSystemProperties = bpy.props.PointerProperty(type=prop.BIMSystemProperties) + bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load) def unregister(): del bpy.types.Scene.BIMSystemProperties + bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load) diff --git a/src/blenderbim/blenderbim/bim/module/system/data.py b/src/blenderbim/blenderbim/bim/module/system/data.py index 6c71e88dc9..9dd348fe13 100644 --- a/src/blenderbim/blenderbim/bim/module/system/data.py +++ b/src/blenderbim/blenderbim/bim/module/system/data.py @@ -27,6 +27,7 @@ def refresh(): SystemData.is_loaded = False ObjectSystemData.is_loaded = False PortData.is_loaded = False + SystemDecorationData.is_loaded = False class SystemData: @@ -140,3 +141,19 @@ class PortData: data.append((port, port_obj, connected_element)) return data + + +class SystemDecorationData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = { + "decoration_data": cls.decoration_data(), + } + cls.is_loaded = True + + @classmethod + def decoration_data(cls): + return tool.System.get_decoration_data() diff --git a/src/blenderbim/blenderbim/bim/module/system/decorator.py b/src/blenderbim/blenderbim/bim/module/system/decorator.py new file mode 100644 index 0000000000..244c8d9a5f --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/system/decorator.py @@ -0,0 +1,145 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2023 Dion Moult , @Andrej730 +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import gpu +import bmesh +import blenderbim.tool as tool +from math import sin, cos, radians +from bpy.types import SpaceView3D +from mathutils import Vector, Matrix +from gpu_extras.batch import batch_for_shader +import ifcopenshell +from blenderbim.bim.module.system.data import SystemDecorationData +from bpy.app.handlers import persistent + + +ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED +UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY + + +def transparent_color(color, alpha=0.1): + color = [i for i in color] + color[3] = alpha + return color + + +@persistent +def toggle_decorations_on_load(*args): + if bpy.context.scene.BIMSystemProperties.should_draw_decorations: + SystemDecorator.install(bpy.context) + else: + SystemDecorator.uninstall() + + +class SystemDecorator: + installed = None + + @classmethod + def install(cls, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None): + """Note that operators that change mesh in `exit_edit_mode_callback` can freeze blender. + The workaround is to move their code to function and use it for callback. + + Example: https://devtalk.blender.org/t/calling-operator-that-saves-bmesh-freezes-blender-forever/28595""" + if cls.installed: + cls.uninstall() + handler = cls() + cls.installed = SpaceView3D.draw_handler_add( + handler, (context, get_custom_bmesh, draw_faces, exit_edit_mode_callback), "WINDOW", "POST_VIEW" + ) + + @classmethod + def uninstall(cls): + try: + SpaceView3D.draw_handler_remove(cls.installed, "WINDOW") + except ValueError: + pass + cls.installed = None + + def draw_batch(self, shader_type, content_pos, color, indices=None): + shader = self.line_shader if shader_type == "LINES" else self.shader + batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) + shader.uniform_float("color", color) + batch.draw(shader) + + def draw_faces(self, bm, vertices_coords): + """mutates original bm (triangulates it) + so the triangulation edges will be shown too + """ + traingulated_bm = bm + bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces) + + face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces] + faces_color = transparent_color(self.addon_prefs.decorator_color_special) + self.draw_batch("TRIS", vertices_coords, faces_color, face_indices) + + def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None): + self.addon_prefs = context.preferences.addons["blenderbim"].preferences + selected_elements_color = self.addon_prefs.decorator_color_selected + unselected_elements_color = self.addon_prefs.decorator_color_unselected + special_elements_color = self.addon_prefs.decorator_color_special + + gpu.state.point_size_set(6) + gpu.state.blend_set("ALPHA") + + ### Actually drawing + all_vertices = [] + error_vertices = [] + selected_vertices = [] + unselected_vertices = [] + # special = associated with arcs/circles + special_vertices = [] + special_vertex_indices = {} + selected_edges = [] + unselected_edges = [] + arc_edges = [] + roof_angle_edges = [] + preview_edges = [] + + if not SystemDecorationData.is_loaded: + SystemDecorationData.load() + + decoration_data = SystemDecorationData.data["decoration_data"] + all_vertices = decoration_data["all_vertices"] + preview_edges = decoration_data["preview_edges"] + special_vertices = decoration_data["special_vertices"] + selected_edges = decoration_data["selected_edges"] + selected_vertices = decoration_data["selected_vertices"] + + ### Actually drawing + # 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated + self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() + # POLYLINE_UNIFORM_COLOR specific uniforms + self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) + self.line_shader.uniform_float("lineWidth", 2.0) + + # general shader + self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR") + self.shader.bind() + + self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges) + self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges) + self.draw_batch("LINES", all_vertices, UNSPECIAL_ELEMENT_COLOR, arc_edges) + self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges) + self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges) + + self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5)) + self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR) + self.draw_batch("POINTS", special_vertices, special_elements_color) + self.draw_batch("POINTS", selected_vertices, selected_elements_color) diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py index 71336ec7fb..0b703495b3 100644 --- a/src/blenderbim/blenderbim/bim/module/system/operator.py +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -219,12 +219,15 @@ class DisconnectPort(bpy.types.Operator, Operator): class MEPConnectElements(bpy.types.Operator, Operator): bl_idname = "bim.mep_connect_elements" bl_label = "Connect MEP Elements" - bl_description = "Connects two selected elements if they have ports with matching location" + bl_description = "Connects two selected elements by their closest located ports and adjusts them" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): - return len(context.selected_objects) == 2 + if not len(context.selected_objects) == 2: + cls.poll_message_set("Need to select 2 objects.") + return False + return True def _execute(self, context): obj1 = context.active_object @@ -233,23 +236,32 @@ class MEPConnectElements(bpy.types.Operator, Operator): el1 = tool.Ifc.get_entity(obj1) el2 = tool.Ifc.get_entity(obj2) + connected_elements = ifcopenshell.util.system.get_connected_to(el1) + connected_elements += ifcopenshell.util.system.get_connected_to(el2) + + if el2 in connected_elements: + self.report({"ERROR"}, "MEP elements are already connected to each other.") + return {"CANCELLED"} + obj1_ports = [p for p in tool.System.get_ports(el1) if not tool.System.get_connected_port(p)] obj2_ports = [p for p in tool.System.get_ports(el2) if not tool.System.get_connected_port(p)] if not obj1_ports or not obj2_ports: self.report({"ERROR"}, "Couldn't find free ports to connect.") - return + return {"CANCELLED"} + ports_distance = dict() for port1 in obj1_ports: port1_location = tool.Model.get_element_matrix(port1).translation for port2 in obj2_ports: port2_location = tool.Model.get_element_matrix(port2).translation - if tool.Cad.are_vectors_equal(port1_location, port2_location): - core.connect_port(tool.Ifc, port1, port2) - return {"FINISHED"} + distance = (port1_location - port2_location).length + ports_distance[(port1, port2)] = distance - self.report({"ERROR"}, "Couldn't find any matching ports to connect.") - return {"CANCELLED"} + closest_ports = min(ports_distance, key=lambda x: ports_distance[x]) + core.connect_port(tool.Ifc, *closest_ports) + bpy.ops.bim.regenerate_distribution_element() + return {"FINISHED"} class SetFlowDirection(bpy.types.Operator, Operator): diff --git a/src/blenderbim/blenderbim/bim/module/system/prop.py b/src/blenderbim/blenderbim/bim/module/system/prop.py index 3d31414ffd..f1ffa3263c 100644 --- a/src/blenderbim/blenderbim/bim/module/system/prop.py +++ b/src/blenderbim/blenderbim/bim/module/system/prop.py @@ -18,6 +18,7 @@ import bpy from blenderbim.bim.module.system.data import SystemData +import blenderbim.bim.module.system.decorator as decorator from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -44,6 +45,14 @@ class System(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") +def toggle_decorations(self, context): + toggle = self.should_draw_decorations + if toggle: + decorator.SystemDecorator.install(context) + else: + decorator.SystemDecorator.uninstall() + + class BIMSystemProperties(PropertyGroup): system_attributes: CollectionProperty(name="System Attributes", type=Attribute) is_editing: BoolProperty(name="Is Editing", default=False) @@ -52,3 +61,6 @@ class BIMSystemProperties(PropertyGroup): active_system_index: IntProperty(name="Active System Index") active_system_id: IntProperty(name="Active System Id") system_class: EnumProperty(items=get_system_class, name="Class") + should_draw_decorations: BoolProperty( + name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations + ) diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index 5b60c151e7..d8e6b764c6 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -101,6 +101,10 @@ class BIM_PT_object_systems(Panel): if not ObjectSystemData.is_loaded: ObjectSystemData.load() self.props = context.scene.BIMSystemProperties + + row = self.layout.row(align=True) + row.prop(self.props, "should_draw_decorations") + if self.props.is_editing: row = self.layout.row() row.alignment = "RIGHT" diff --git a/src/blenderbim/blenderbim/bim/module/tester/operator.py b/src/blenderbim/blenderbim/bim/module/tester/operator.py index 4b88c3ae4b..57ed89429d 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/operator.py +++ b/src/blenderbim/blenderbim/bim/module/tester/operator.py @@ -131,7 +131,8 @@ class SelectRequirement(bpy.types.Operator): props.failed_entities.clear() for e in failed_entities: new_entity = props.failed_entities.add() - new_entity.element = e["element"] + new_entity.ifc_id = e["id"] + new_entity.element = f'{e["class"]}/{e["name"]}' new_entity.reason = e["reason"] return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/tester/prop.py b/src/blenderbim/blenderbim/bim/module/tester/prop.py index 53e4c26993..7496d4ba44 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/prop.py +++ b/src/blenderbim/blenderbim/bim/module/tester/prop.py @@ -41,8 +41,9 @@ class Specification(PropertyGroup): class FailedEntities(PropertyGroup): - reason: StringProperty(name="Reason") + ifc_id: IntProperty(name="IFC ID") element: StringProperty(name="Element") + reason: StringProperty(name="Reason") class IfcTesterProperties(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/tester/ui.py b/src/blenderbim/blenderbim/bim/module/tester/ui.py index 2af3f7af98..31c45bc888 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/ui.py +++ b/src/blenderbim/blenderbim/bim/module/tester/ui.py @@ -115,17 +115,9 @@ class BIM_UL_tester_failed_entities(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): props = context.scene.IfcTesterProperties if item: - if props.should_load_from_memory: - ifc_file = tool.Ifc.get() - ifc_id = int(item.element[1 : item.element.find("=")]) - entity = ifc_file.by_id(ifc_id) - report_entity = f"[#{ifc_id}][{entity.is_a()}] {entity.Name}" - else: - report_entity = item.element - row = layout.row(align=True) - row.label(text=report_entity) + row.label(text=item.element) row.label(text=item.reason) if props.should_load_from_memory: op = row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF") - op.ifc_id = entity.id() + op.ifc_id = item.ifc_id diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 9f36116ce2..a8a675ef91 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -42,7 +42,7 @@ from math import radians class SetTab(bpy.types.Operator): bl_idname = "bim.set_tab" - # NOTE: bl_label is set to empty string intentionally + # NOTE: bl_label is set to empty string intentionally # to avoid showing the operator's name in the tooltips, see #3704 bl_label = "" bl_options = {"REGISTER", "UNDO", "INTERNAL"} @@ -197,21 +197,52 @@ class FileAssociate(bpy.types.Operator): @classmethod def poll(cls, context): - if platform.system() == "Linux": + if platform.system() in ("Linux", "Windows"): return True - cls.poll_message_set("Option available only on Linux.") - # TODO Windows and Darwin + cls.poll_message_set("Option available only on Windows & Linux.") + # TODO Darwin # https://stackoverflow.com/questions/1082889/how-to-change-filetype-association-in-the-registry return False + def draw(self, context): + # NOTE: really weird thing on windows that typing this command in cmd works + # when even if you create .bat with the command below and run it as administrator it won't + # Haven't found a workaround yet to automate process completely. + command = "ASSOC .IFC=BLENDERBIM" + self.layout.label(text="On the next step to create file association ") + self.layout.label(text="the system console will be opened ") + self.layout.label(text=f"and you will be asked to type command") + self.layout.label(text=f'"{command}"') + self.layout.label(text="to create an association.") + + def invoke(self, context, event): + if platform.system() == "Windows": + return context.window_manager.invoke_props_dialog(self) + else: + return self.execute(context) + def execute(self, context): src_dir = os.path.join(os.path.dirname(__file__), "../libs/desktop") binary_path = bpy.app.binary_path if platform.system() == "Linux": destdir = os.path.join(os.environ["HOME"], ".local") self.install_desktop_linux(src_dir=src_dir, destdir=destdir, binary_path=binary_path) + elif platform.system() == "Windows": + self.install_desktop_windows(src_dir, binary_path) + self.report({"INFO"}, "Associations established.") return {"FINISHED"} + def install_desktop_windows(self, src_dir, binary_path): + # very important to clear this regitstry key before creating new association + # tried to do the regitsry change from powershell/cmd - but even admin rights are not enough + # this is why we're using .reg + reg_change_path = os.path.join(src_dir, "windows_bbim_association.reg") + subprocess.run(["cmd", "/c", reg_change_path]) + + ps_script_path = os.path.join(src_dir, "windows_bbim_association.ps1") + # NOTE: call powershell with RunAs to get admin rights from user + subprocess.run(["powershell", "-file", ps_script_path, binary_path], shell=True) + def install_desktop_linux(self, src_dir=None, destdir="/tmp", binary_path="/usr/bin/blender"): """Creates linux file assocations and launcher icon""" @@ -272,17 +303,29 @@ class FileUnassociate(bpy.types.Operator): @classmethod def poll(cls, context): - if platform.system() == "Linux": + if platform.system() in ("Linux", "Windows"): return True - cls.poll_message_set("Option available only on Linux.") + cls.poll_message_set("Option available only on Windows & Linux.") return False def execute(self, context): if platform.system() == "Linux": destdir = os.path.join(os.environ["HOME"], ".local") self.uninstall_desktop_linux(destdir=destdir) + elif platform.system() == "Windows": + self.uninstall_desktop_windows() return {"FINISHED"} + def uninstall_desktop_windows(self): + # NOTE: call powershell with RunAs to get admin rights from user + cmd = [ + "powershell", + "-Command", + "Start-Process -Verb RunAs -Wait cmd -ArgumentList '/c reg delete HKCR\\BLENDERBIM /f'", + ] + subprocess.run(cmd, check=True) + self.report({"INFO"}, "Association removed.") + def uninstall_desktop_linux(self, destdir="/tmp"): """Removes linux file assocations and launcher icon""" for rel_path in ( diff --git a/src/blenderbim/blenderbim/core/cost.py b/src/blenderbim/blenderbim/core/cost.py index d8f5d9166b..19bf23f02a 100644 --- a/src/blenderbim/blenderbim/core/cost.py +++ b/src/blenderbim/blenderbim/core/cost.py @@ -1,4 +1,4 @@ -def add_cost_schedule(ifc, name, predefined_type,object_type): +def add_cost_schedule(ifc, name, predefined_type, object_type): ifc.run("cost.add_cost_schedule", name=name, predefined_type=predefined_type, object_type=object_type) @@ -112,70 +112,89 @@ def assign_cost_item_quantity(ifc, cost, cost_item, related_object_type, prop_na ifc.run("cost.assign_cost_item_quantity", cost_item=cost_item, products=products, prop_name=prop_name) cost.load_cost_item_quantity_assignments(cost_item, related_object_type=related_object_type) + def load_cost_item_quantities(cost): cost.load_cost_item_quantities() + def load_cost_item_element_quantities(cost): cost_item = cost.get_highlighted_cost_item() cost.load_cost_item_quantity_assignments(cost_item, related_object_type="PRODUCT") + def load_cost_item_task_quantities(cost): cost_item = cost.get_highlighted_cost_item() cost.load_cost_item_quantity_assignments(cost_item, related_object_type="PROCESS") + def load_cost_item_resource_quantities(cost): cost_item = cost.get_highlighted_cost_item() cost.load_cost_item_quantity_assignments(cost_item, related_object_type="RESOURCE") + def assign_cost_value(ifc, cost_item, cost_rate): ifc.run("cost.assign_cost_value", cost_item=cost_item, cost_rate=cost_rate) + def load_schedule_of_rates(cost, schedule_of_rates): cost.load_schedule_of_rates_tree(schedule_of_rates) + def unassign_cost_item_quantity(ifc, cost, cost_item, products): ifc.run("cost.unassign_cost_item_quantity", cost_item=cost_item, products=products) cost.load_cost_item_quantities() + def enable_editing_cost_item_quantities(cost, cost_item): cost.enable_editing_cost_item_quantities(cost_item) + def enable_editing_cost_item_values(cost, cost_item): cost.enable_editing_cost_item_values(cost_item) + def add_cost_item_quantity(ifc, cost_item, ifc_class): ifc.run("cost.add_cost_item_quantity", cost_item=cost_item, ifc_class=ifc_class) + def remove_cost_item_quantity(ifc, cost_item, physical_quantity): ifc.run("cost.remove_cost_item_quantity", cost_item=cost_item, physical_quantity=physical_quantity) - + + def enable_editing_cost_item_quantity(cost, physical_quantity): cost.load_cost_item_quantity_attributes(physical_quantity) cost.enable_editing_cost_item_quantity(physical_quantity) + def disable_editing_cost_item_quantity(cost): cost.disable_editing_cost_item_quantity() + def edit_cost_item_quantity(ifc, cost, physical_quantity): attributes = cost.get_cost_item_quantity_attributes() ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=attributes) cost.disable_editing_cost_item_quantity() cost.load_cost_item_quantities() + def add_cost_value(ifc, cost, parent, cost_type, cost_category): value = ifc.run("cost.add_cost_value", parent=parent) ifc.run( "cost.edit_cost_value", cost_value=value, - attributes=cost.get_attributes_for_cost_value(cost_type, cost_category)) + attributes=cost.get_attributes_for_cost_value(cost_type, cost_category), + ) + def remove_cost_value(ifc, parent, cost_value): ifc.run("cost.remove_cost_value", parent=parent, cost_value=cost_value) + def enable_editing_cost_item_value(cost, cost_value): cost.load_cost_item_value_attributes(cost_value) cost.enable_editing_cost_item_value(cost_value) + def disable_editing_cost_item_value(cost): cost.disable_editing_cost_item_value() @@ -195,7 +214,7 @@ def edit_cost_value(ifc, cost, cost_value): attributes = cost.get_cost_value_attributes() ifc.run("cost.edit_cost_value", cost_value=cost_value, attributes=attributes) cost.disable_editing_cost_item_value() - #cost.load_cost_item_values(cost.get_highlighted_cost_item()) + # cost.load_cost_item_values(cost.get_highlighted_cost_item()) def copy_cost_item_values(ifc, cost, source, destination): @@ -275,11 +294,12 @@ def change_parent_cost_item(ifc, cost, new_parent): cost_item = cost.get_active_cost_item() if cost_item and cost.is_root_cost_item(cost_item): return "Cannot change root cost item" - if cost_item : + if cost_item: ifc.run("nest.change_nest", item=cost_item, new_parent=new_parent) cost.disable_editing_cost_item_parent() cost.load_cost_schedule_tree() + def copy_cost_item(ifc, cost): cost_item = cost.get_highlighted_cost_item() if cost_item: @@ -287,9 +307,10 @@ def copy_cost_item(ifc, cost): cost.disable_editing_cost_item_parent() cost.load_cost_schedule_tree() + def add_currency(ifc, cost): unit = ifc.run("unit.add_monetary_unit") attributes = cost.get_currency_attributes() ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes) ifc.run("unit.assign_unit", units=[unit]) - return unit \ No newline at end of file + return unit diff --git a/src/blenderbim/blenderbim/core/misc.py b/src/blenderbim/blenderbim/core/misc.py index 358f96d5ec..5aff2100c0 100644 --- a/src/blenderbim/blenderbim/core/misc.py +++ b/src/blenderbim/blenderbim/core/misc.py @@ -28,11 +28,3 @@ def resize_to_storey(misc, obj=None, total_storeys=None): misc.move_object_to_elevation(obj, misc.get_storey_elevation_in_si(storey)) misc.scale_object_to_height(obj, height) misc.mark_object_as_edited(obj) - - -def split_along_edge(misc, cutter=None, objs=None): - new_objs = misc.split_objects_with_cutter(objs, cutter) - for obj in new_objs: - misc.run_root_copy_class(obj=obj) - for obj in objs: - misc.mark_object_as_edited(obj) diff --git a/src/blenderbim/blenderbim/core/pset.py b/src/blenderbim/blenderbim/core/pset.py index 36200040f7..023b7ba190 100644 --- a/src/blenderbim/blenderbim/core/pset.py +++ b/src/blenderbim/blenderbim/core/pset.py @@ -43,3 +43,4 @@ def add_pset(ifc, pset, blender, obj_name, obj_type): ifc_pset = pset.get_element_pset(element, pset_name) if not ifc_pset: ifc.run("pset.add_pset", product=element, name=pset_name) + pset.enable_pset_editing(pset_id=0, pset_name=pset_name, pset_type="PSET",obj=obj_name, obj_type=obj_type) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index c026139446..dd3875a04f 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -21,15 +21,11 @@ def load_resources(resource): resource.load_resources() - resource.load_resource_properties() + def add_resource(tool_ifc, resource_tool, ifc_class, parent_resource=None): tool_ifc.run("resource.add_resource", ifc_class=ifc_class, parent_resource=parent_resource) - load_resources(resource_tool) - - -def load_resource_properties(resource_tool, resource=None): - resource_tool.load_resource_properties() + resource_tool.load_resources() def disable_editing_resource(resource_tool): @@ -54,7 +50,7 @@ def edit_resource(ifc, resource_tool, resource): def remove_resource(ifc, resource_tool, resource=None): ifc.run("resource.remove_resource", resource=resource) - load_resources(resource_tool) + resource_tool.load_resources() def enable_editing_resource_time(ifc_tool, resource_tool, resource): @@ -82,7 +78,7 @@ def calculate_resource_work(ifc, resource_tool, resource): nested_resources = resource_tool.get_nested_resources(resource) for nested_resource in nested_resources or []: ifc.run("resource.calculate_resource_work", resource=nested_resource) - load_resources(resource_tool) + resource_tool.load_resources() def enable_editing_resource_costs(resource_tool, resource): @@ -143,17 +139,17 @@ def edit_resource_quantity(resource_tool, ifc, physical_quantity=None): def import_resources(resource_tool, file_path): resource_tool.import_resources(file_path) - load_resources(resource_tool) + resource_tool.load_resources() def expand_resource(resource_tool, resource): resource_tool.expand_resource(resource) - load_resources(resource_tool) + resource_tool.load_resources() def contract_resource(resource_tool, resource): resource_tool.contract_resource(resource) - load_resources(resource_tool) + resource_tool.load_resources() def assign_resource(ifc, spatial, resource=None, products=None): @@ -213,9 +209,11 @@ def remove_usage_constraint(ifc, resource_tool, resource, reference_path): ifc.run("constraint.unassign_constraint", product=resource, constraint=constraint) ifc.run("constraint.remove_constraint", constraint=constraint) + def go_to_resource(resource_tool, resource): resource_tool.go_to_resource(resource) + def calculate_resource_usage(ifc, resource_tool, resource): ifc.run("resource.calculate_resource_usage", resource=resource) - load_resources(resource_tool) \ No newline at end of file + resource_tool.load_resources() diff --git a/src/blenderbim/blenderbim/core/sequence.py b/src/blenderbim/blenderbim/core/sequence.py index 2321796783..d4ecd2bb72 100644 --- a/src/blenderbim/blenderbim/core/sequence.py +++ b/src/blenderbim/blenderbim/core/sequence.py @@ -186,14 +186,14 @@ def enable_editing_task_time(ifc, sequence, task=None): sequence.enable_editing_task_time(task) -def edit_task_time(ifc, sequence, task_time=None): +def edit_task_time(ifc, sequence, resource, task_time=None): attributes = sequence.get_task_time_attributes() # TODO: nasty loop goes on when calendar props are messed up ifc.run("sequence.edit_task_time", task_time=task_time, attributes=attributes) task = sequence.get_active_task() sequence.load_task_properties(task=task) sequence.disable_editing_task_time() - sequence.load_resources() + resource.load_resource_properties() def assign_predecessor(ifc, sequence, task=None): @@ -248,8 +248,8 @@ def unassign_input_products(ifc, sequence, spatial, task=None, products=None): sequence.load_task_inputs(inputs) -def assign_resource(ifc, sequence, task=None): - resource = sequence.get_selected_resource() +def assign_resource(ifc, sequence, resource_tool, task=None): + resource = resource_tool.get_highlighted_resource() sub_resource = ifc.run( "resource.add_resource", parent_resource=resource, @@ -257,17 +257,15 @@ def assign_resource(ifc, sequence, task=None): name="{}/{}".format(resource.Name or "Unnamed", task.Name or ""), ) ifc.run("sequence.assign_process", relating_process=task, related_object=sub_resource) - resources = sequence.get_task_resources(task) - sequence.load_task_resources(resources) - sequence.load_resources() + sequence.load_task_resources(task) + resource_tool.load_resources() -def unassign_resource(ifc, sequence, task=None, resource=None): +def unassign_resource(ifc, sequence, resource_tool, task=None, resource=None): ifc.run("sequence.unassign_process", relating_process=task, related_object=resource) ifc.run("resource.remove_resource", resource=resource) - resources = sequence.get_task_resources(task) - sequence.load_task_resources(resources) - sequence.load_resources() + sequence.load_task_resources(task) + resource_tool.load_resources() def remove_work_calendar(ifc, work_calendar=None): diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index df3f4793ce..0557e0144d 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -726,7 +726,6 @@ class Sequence: def get_recurrence_pattern_attributes(cls, recurrence_pattern): pass def get_recurrence_pattern_times(cls): pass def get_rel_sequence_attributes(cls): pass - def get_selected_resource(cls): pass def get_start_date(cls): pass def get_task_attribute_value(cls, attribute_name): pass def get_task_attributes(cls): pass @@ -758,7 +757,7 @@ class Sequence: def load_task_inputs(cls, inputs): pass def load_task_outputs(cls, outputs): pass def load_task_properties(cls, task): pass - def load_task_resources(cls,resources): pass + def load_task_resources(cls, task): pass def load_task_time_attributes(cls, task_time): pass def load_task_tree(cls, work_schedule): pass def load_work_calendar_attributes(cls, work_calendar): pass diff --git a/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.ps1 b/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.ps1 new file mode 100644 index 0000000000..11926a170a --- /dev/null +++ b/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.ps1 @@ -0,0 +1,16 @@ +param( + [string]$BlenderPath = "BLENDER_EXE" +) + +Start-Process cmd -ArgumentList ` + "/k ", ` + "ASSOC .IFC=", ` + "&", ` + "FTYPE BLENDERBIM=""$BlenderPath"" --python-expr ""import bpy; bpy.ops.bim.load_project(filepath=r'%1')""", + "&", ` + "echo. & echo. & echo To create an association between .IFC files and BlenderBIM", ` + "&", ` + "echo type the command below & echo.", ` + "&", ` + "echo ASSOC .IFC=BLENDERBIM & echo." ` +-Verb RunAs \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 20f825ab25..d544a67787 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -168,7 +168,8 @@ class Blender: """ area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") region = next(region for region in area.regions if region.type == "WINDOW") - context_override = {"area": area, "region": region} + space = next(space for space in area.spaces if space.type == "VIEW_3D") + context_override = {"area": area, "region": region, "space_data": space} return context_override @classmethod @@ -483,6 +484,13 @@ class Blender: if obj: return obj + @classmethod + def lock_transform(cls, obj, lock_state=True): + for prop in ("lock_location", "lock_rotation", "lock_scale"): + attr = getattr(obj, prop) + for axis_idx in range(3): + attr[axis_idx] = lock_state + class Modifier: @classmethod def is_eligible_for_railing_modifier(cls, obj): @@ -559,10 +567,7 @@ class Blender: modifier_data = list(cls.get_modifiers_data(parent_element))[item] children = cls.get_children_objects(modifier_data) for child_obj in children: - for prop in ("lock_location", "lock_rotation", "lock_scale"): - attr = getattr(child_obj, prop) - for axis_idx in range(3): - attr[axis_idx] = lock_state + Blender.lock_transform(child_obj, lock_state) @classmethod def remove_constraints(cls, parent_element): diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index 698625bc33..f51d157862 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -234,7 +234,6 @@ class Cost(blenderbim.core.tool.Cost): @classmethod def get_products(cls, related_object_type=None): - props = bpy.context.scene.BIMCostProperties if related_object_type == "PRODUCT": products = tool.Spatial.get_selected_products() elif related_object_type == "PROCESS": diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 516f5c879a..63af83cbaa 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -249,17 +249,7 @@ class Drawing(blenderbim.core.tool.Drawing): obj_data = obj.data bpy.data.objects.remove(obj) if obj_data and obj_data.users == 0: # in case we have drawing element types - cls.remove_object_data(obj_data) - - @classmethod - def remove_object_data(cls, data): - """also removes all related objects""" - if isinstance(data, bpy.types.Camera): - bpy.data.cameras.remove(data) - elif isinstance(data, bpy.types.Mesh): - bpy.data.meshes.remove(data) - elif isinstance(data, bpy.types.Curve): - bpy.data.curves.remove(data) + tool.Blender.remove_data_block(obj_data) @classmethod def delete_object(cls, obj): @@ -445,7 +435,7 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def get_drawing_target_view(cls, drawing): - return ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("TargetView", "MODEL_VIEW") + return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("TargetView", "MODEL_VIEW") @classmethod def get_group_elements(cls, group): diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index b1ce914f4c..8e624d61f0 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -115,6 +115,12 @@ class Geometry(blenderbim.core.tool.Geometry): for port in ifcopenshell.util.system.get_ports(element): blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port) ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element) + + if isinstance(obj.data, bpy.types.Mesh) and not tool.Ifc.get_entity_by_id( + obj.data.BIMMeshProperties.ifc_definition_id + ): + tool.Blender.remove_data_block(obj.data) + if is_spatial: blenderbim.core.spatial.load_container_manager(tool.Spatial) try: diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py index 9c975e92d8..a655b5ce1a 100644 --- a/src/blenderbim/blenderbim/tool/ifc.py +++ b/src/blenderbim/blenderbim/tool/ifc.py @@ -86,6 +86,15 @@ class Ifc(blenderbim.core.tool.Ifc): except: pass + @classmethod + def get_entity_by_id(cls, entity_id): + """useful to check whether entity_id is still exists in IFC""" + ifc_file = tool.Ifc.get() + try: + return ifc_file.by_id(entity_id) + except RuntimeError: + return None + @classmethod def get_object(cls, element): return IfcStore.get_element(element.id()) diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 1c12b9a92a..e2a2c29773 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -540,11 +540,56 @@ class Model(blenderbim.core.tool.Model): return axes @classmethod - def regenerate_array(cls, parent, data, keep_objs=False): - tool.Blender.Modifier.Array.remove_constraints(tool.Ifc.get_entity(parent)) + def handle_array_on_copied_element(cls, element, array_data=None): + """if no `array_data` is provided then an array will be removed from the element""" + + if array_data is None: + array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") + if not array_pset: + return + + array_pset_data = array_pset["Data"] + array_pset = tool.Ifc.get().by_id(array_pset["id"]) + ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=array_pset) + + # remove constraints + obj = tool.Ifc.get_object(element) + if not array_pset_data: # skip array parents + constraint = next((c for c in obj.constraints if c.type == "CHILD_OF"), None) + if constraint: + matrix = obj.matrix_world.copy() + obj.constraints.remove(constraint) + # keep the matrix before the constraint + # otherwise object will jump to some previous position + obj.matrix_world = matrix + tool.Blender.lock_transform(obj, False) + + else: + obj = tool.Ifc.get_object(element) + array_pset = tool.Pset.get_element_pset(element, "BBIM_Array") + default_data = '[{"children": []}]' + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + pset=array_pset, + properties={"Parent": element.GlobalId, "Data": default_data}, + ) + + tool.Model.regenerate_array(obj, array_data) + + json_data = json.dumps(array_data) + ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=array_pset, properties={"Data": json_data}) + + for i in range(len(array_data)): + tool.Blender.Modifier.Array.set_children_lock_state(element, i, True) + tool.Blender.Modifier.Array.constrain_children_to_parent(element) + + @classmethod + def regenerate_array(cls, parent_obj, data, keep_objs=False): + tool.Blender.Modifier.Array.remove_constraints(tool.Ifc.get_entity(parent_obj)) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - obj_stack = [parent] + obj_stack = [parent_obj] for array in data: if array["sync_children"]: diff --git a/src/blenderbim/blenderbim/tool/pset.py b/src/blenderbim/blenderbim/tool/pset.py index 62f52bdecc..1c115f7e1e 100644 --- a/src/blenderbim/blenderbim/tool/pset.py +++ b/src/blenderbim/blenderbim/tool/pset.py @@ -66,3 +66,10 @@ class Pset(blenderbim.core.tool.Pset): if value is not None: return False return True + + @classmethod + def enable_pset_editing(cls, pset_id=None, pset_name=None, pset_type=None, obj=None, obj_type=None): + #TODO REFACTOR ONCE toll/CORE functions are available + bpy.ops.bim.enable_pset_editing( + pset_id=0, pset_name=tool.Pset.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type + ) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/tool/resource.py b/src/blenderbim/blenderbim/tool/resource.py index 3c5698505d..99293fcbc9 100644 --- a/src/blenderbim/blenderbim/tool/resource.py +++ b/src/blenderbim/blenderbim/tool/resource.py @@ -61,6 +61,7 @@ class Resource(blenderbim.core.tool.Resource): continue create_new_resource_li(resource, 0) cls.load_productivity_data() + cls.load_resource_properties() props.is_resource_update_enabled = True props.is_editing = True @@ -425,7 +426,7 @@ class Resource(blenderbim.core.tool.Resource): contracted_resources.remove(ancestor) bpy.context.scene.BIMResourceProperties.contracted_resources = json.dumps(contracted_resources) cls.load_resources() - cls.load_resource_properties() + resource_props = bpy.context.scene.BIMResourceTreeProperties expanded_resources = [item.ifc_definition_id for item in resource_props.resources] diff --git a/src/blenderbim/blenderbim/tool/sequence.py b/src/blenderbim/blenderbim/tool/sequence.py index 6fea1e3639..c197d7799b 100644 --- a/src/blenderbim/blenderbim/tool/sequence.py +++ b/src/blenderbim/blenderbim/tool/sequence.py @@ -254,14 +254,6 @@ class Sequence(blenderbim.core.tool.Sequence): return None return tool.Ifc.get().by_id(bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id) - @classmethod - def get_selected_resource(cls): - if bpy.context.scene.BIMResourceTreeProperties.resources: - selected_resource_id = bpy.context.scene.BIMResourceTreeProperties.resources[ - bpy.context.scene.BIMResourceProperties.active_resource_index - ].ifc_definition_id - return tool.Ifc.get().by_id(selected_resource_id) - @classmethod def expand_task(cls, task): props = bpy.context.scene.BIMWorkScheduleProperties @@ -409,19 +401,17 @@ class Sequence(blenderbim.core.tool.Sequence): return blenderbim.bim.helper.export_attributes(props.task_time_attributes, callback) @classmethod - def load_task_resources(cls, resources): + def load_task_resources(cls, task): props = bpy.context.scene.BIMWorkScheduleProperties + rprops = bpy.context.scene.BIMResourceProperties props.task_resources.clear() - for resource in resources or []: + rprops.is_resource_update_enabled = False + for resource in cls.get_task_resources(task) or []: new = props.task_resources.add() new.ifc_definition_id = resource.id() new.name = resource.Name or "Unnamed" new.schedule_usage = resource.Usage.ScheduleUsage or 0 if resource.Usage else 0 - - @classmethod - def load_resources(cls): - blenderbim.core.resource.load_resources(tool.Resource) - cls.refresh_task_resources + rprops.is_resource_update_enabled = True @classmethod def get_task_inputs(cls, task): @@ -447,6 +437,8 @@ class Sequence(blenderbim.core.tool.Sequence): @classmethod def get_task_resources(cls, task): + if not task: + return is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_resources return ifcopenshell.util.sequence.get_task_resources(task, is_deep) @@ -1664,17 +1656,16 @@ class Sequence(blenderbim.core.tool.Sequence): return inputs = cls.get_task_inputs(task) outputs = cls.get_task_outputs(task) - resources = cls.get_task_resources(task) cls.load_task_inputs(inputs) cls.load_task_outputs(outputs) - cls.load_task_resources(resources) + cls.load_task_resources(task) @classmethod def refresh_task_resources(cls): task = cls.get_highlighted_task() if not task: return - cls.load_task_resources(cls.get_task_resources(task)) + cls.load_task_resources(task) @classmethod def has_duration(cls, task): diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index 13e25cf6af..87bdcc1a65 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -182,3 +182,80 @@ class System(blenderbim.core.tool.System): @classmethod def set_active_system(cls, system): bpy.context.scene.BIMSystemProperties.active_system_id = system.id() + + @classmethod + def get_decoration_data(cls): + all_vertices = [] + preview_edges = [] + special_vertices = [] + selected_edges = [] + selected_vertices = [] + + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + start_vert_i = 0 + + if bpy.context.active_object and (active_element := tool.Ifc.get_entity(bpy.context.active_object)): + selected_elements = cls.get_connected_elements(active_element) + else: + selected_elements = set() + + # TODO: get only objects visible in viewport + objects = set(bpy.data.objects) - set(bpy.data.collections["Types"].objects) + for obj in objects: + start_vert_i = len(all_vertices) + if obj.hide_get(): + continue + + if not isinstance(obj.data, bpy.types.Mesh): + continue + + element = tool.Ifc.get_entity(obj) + if not element: + continue + + if not cls.is_mep_element(element): + continue + + ports = tool.System.get_ports(element) + + for port in ports: + position = tool.Model.get_element_matrix(port).translation * si_conversion + all_vertices.append(position) + + verts = range(start_vert_i, start_vert_i + len(ports)) + edges = [(i, i + 1) for i in range(start_vert_i, start_vert_i + len(ports) - 1)] + if element in selected_elements: + selected_vertices.extend(verts) + selected_edges.extend(edges) + else: + special_vertices.extend(verts) + preview_edges.extend(edges) + + decoration_data = { + "all_vertices": all_vertices, + "preview_edges": preview_edges, + "special_vertices": [all_vertices[i] for i in special_vertices], + "selected_edges": selected_edges, + "selected_vertices": [all_vertices[i] for i in selected_vertices], + } + return decoration_data + + @classmethod + def get_connected_elements(cls, element, elements=None): + if elements is None: + elements = set((element,)) + + connected_elements = ifcopenshell.util.system.get_connected_from(element) + connected_elements += ifcopenshell.util.system.get_connected_to(element) + + for element in connected_elements: + if element in elements: + continue + elements.add(element) + cls.get_connected_elements(element, elements) + + return elements + + @classmethod + def is_mep_element(cls, element): + return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting") diff --git a/src/blenderbim/test/bim/feature/aggregate.feature b/src/blenderbim/test/bim/feature/aggregate.feature index 662d3b6d15..9d6c35d294 100644 --- a/src/blenderbim/test/bim/feature/aggregate.feature +++ b/src/blenderbim/test/bim/feature/aggregate.feature @@ -61,6 +61,7 @@ Scenario: Add aggregate Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -73,6 +74,7 @@ Scenario: Add aggregate - with the aggregate inheriting the existing spatial col Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is placed in the collection "IfcBuildingStorey/My Storey" @@ -86,6 +88,7 @@ Scenario: Add aggregate - add a nested aggregate Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is placed in the collection "IfcBuildingStorey/My Storey" diff --git a/src/blenderbim/test/bim/feature/attribute.feature b/src/blenderbim/test/bim/feature/attribute.feature index 2e0047e9c9..0810ec6068 100644 --- a/src/blenderbim/test/bim/feature/attribute.feature +++ b/src/blenderbim/test/bim/feature/attribute.feature @@ -38,10 +38,12 @@ Scenario: Copy attribute to selected Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/cost.feature b/src/blenderbim/test/bim/feature/cost.feature index aefe3e5ff9..e74e6b003d 100644 --- a/src/blenderbim/test/bim/feature/cost.feature +++ b/src/blenderbim/test/bim/feature/cost.feature @@ -357,6 +357,7 @@ Scenario: Assign cost item quantity - count based And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')" @@ -371,6 +372,7 @@ Scenario: Assign cost item quantity - quantity based And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -388,6 +390,7 @@ Scenario: Unassign cost item quantity - selection based And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -404,6 +407,7 @@ Scenario: Unassign cost item quantity - explicit object And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -421,6 +425,7 @@ Scenario: Select cost item products And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -437,6 +442,7 @@ Scenario: Select Cost Schedule Products And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/document.feature b/src/blenderbim/test/bim/feature/document.feature index db14740fbe..2157a79b2f 100644 --- a/src/blenderbim/test/bim/feature/document.feature +++ b/src/blenderbim/test/bim/feature/document.feature @@ -89,6 +89,7 @@ Scenario: Assign document And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When I press "bim.assign_document(document={reference})" @@ -105,6 +106,7 @@ Scenario: Unassign document And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.assign_document(document={reference})" diff --git a/src/blenderbim/test/bim/feature/drawing.feature b/src/blenderbim/test/bim/feature/drawing.feature index e9d9ec5a81..28c2b5d80c 100644 --- a/src/blenderbim/test/bim/feature/drawing.feature +++ b/src/blenderbim/test/bim/feature/drawing.feature @@ -5,6 +5,7 @@ Scenario: Duplicate drawing Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()" @@ -17,6 +18,7 @@ Scenario: Create drawing Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()" @@ -31,6 +33,7 @@ Scenario: Create drawing after deleting a duplicated object Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()" @@ -52,6 +55,7 @@ Scenario: Remove drawing Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()" @@ -65,6 +69,7 @@ Scenario: Remove drawing - via object deletion Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()" @@ -79,6 +84,7 @@ Scenario: Remove drawing - deleting active drawing Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "wall1" is "IfcStore.get_file().by_type('IfcWall')[-1].id()" diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index fc9afdd286..206769fc64 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -5,6 +5,7 @@ Scenario: Edit object placement Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -15,6 +16,7 @@ Scenario: Add representation Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -48,6 +50,7 @@ Scenario: Add representation - add a representation with a scale factor applied And the object "Cube" is selected When the object "Cube" is scaled to "2" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" has no scale @@ -60,6 +63,7 @@ Scenario: Add representation - add a representation with a scale factor removed And the object "Cube" is selected When the object "Cube" is scaled to "2" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" has no scale @@ -69,6 +73,7 @@ Scenario: Switch representation Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" @@ -79,6 +84,7 @@ Scenario: Switch representation - current edited representation is updated prior Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier=='Annotation'][0].id()" @@ -96,6 +102,7 @@ Scenario: Switch representation - current edited representation is discarded if Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When the object "IfcWall/Cube" is scaled to "2" @@ -111,6 +118,7 @@ Scenario: Switch representation - existing Blender modifiers must be purged And I add a cube And the object "Cube" is selected And I add an array modifier + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" @@ -122,6 +130,7 @@ Scenario: Remove representation - remove an active representation And I add a cube And the object "Cube" is selected And I add an array modifier + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" @@ -133,6 +142,7 @@ Scenario: Remove representation - remove an unloaded representation And I add a cube And the object "Cube" is selected And I add an array modifier + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[1].id()" @@ -182,6 +192,7 @@ Scenario: Update representation - updating a tessellation And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.update_representation(obj='IfcWall/Cube')" @@ -192,6 +203,7 @@ Scenario: Update representation - updating a layered extrusion And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -219,6 +231,7 @@ Scenario: Update representation - updating a profiled extrusion Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -242,6 +255,7 @@ Scenario: Get representation IFC parameters Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.update_representation(ifc_representation_class='IfcExtrudedAreaSolid/IfcRectangleProfileDef')" @@ -252,6 +266,7 @@ Scenario: Copy representation Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add a cube @@ -273,6 +288,7 @@ Scenario: Override delete - with active IFC data Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -297,6 +313,7 @@ Scenario: Override duplicate move - with active IFC data Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -315,6 +332,7 @@ Scenario: Override duplicate move - copying a coloured representation And I add a cube And the object "Cube" is selected And I add a material + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -347,6 +365,7 @@ Scenario: Override duplicate move - copying a layered extrusion Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -376,6 +395,7 @@ Scenario: Override duplicate move - copying a profiled extrusion Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -412,6 +432,7 @@ Scenario: Override duplicate move linked - with active IFC data Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -440,6 +461,7 @@ Scenario: Override paste buffer - with active IFC data Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/library.feature b/src/blenderbim/test/bim/feature/library.feature index 3cc340d993..1223eccbda 100644 --- a/src/blenderbim/test/bim/feature/library.feature +++ b/src/blenderbim/test/bim/feature/library.feature @@ -113,6 +113,7 @@ Scenario: Assign library reference And the variable "reference" is "{ifc}.by_type('IfcLibraryReference')[-1].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -128,6 +129,7 @@ Scenario: Unassign library reference And the variable "reference" is "{ifc}.by_type('IfcLibraryReference')[-1].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/material.feature b/src/blenderbim/test/bim/feature/material.feature index 18d9b8b0bc..290e8aaf0c 100644 --- a/src/blenderbim/test/bim/feature/material.feature +++ b/src/blenderbim/test/bim/feature/material.feature @@ -72,6 +72,7 @@ Scenario: Assign material - single material Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -83,6 +84,7 @@ Scenario: Unassign material - single material Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -95,6 +97,7 @@ Scenario: Enable editing assigned material - single material Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -107,6 +110,7 @@ Scenario: Disable editing assigned material - single material Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -120,6 +124,7 @@ Scenario: Edit assigned material - single material Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -160,6 +165,7 @@ Scenario: Unassign material - removing inherited material Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" @@ -186,6 +192,7 @@ Scenario: Enable editing assigned material - material layer set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -203,6 +210,7 @@ Scenario: Disable editing assigned material - material layer set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -221,6 +229,7 @@ Scenario: Edit assigned material - material layer set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -266,6 +275,7 @@ Scenario: Enable editing assigned material - material profile set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -283,6 +293,7 @@ Scenario: Disable editing assigned material - material profile set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -301,6 +312,7 @@ Scenario: Edit assigned material - material profile set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -320,6 +332,7 @@ Scenario: Assign material - material constituent set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -331,6 +344,7 @@ Scenario: Unassign material - material constituent set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -343,6 +357,7 @@ Scenario: Enable editing assigned material - material constituent set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -355,6 +370,7 @@ Scenario: Disable editing assigned material - material constituent set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -368,6 +384,7 @@ Scenario: Edit assigned material - material constituent set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -419,6 +436,7 @@ Scenario: Add material set layer Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -438,6 +456,7 @@ Scenario: Remove material set layer Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" diff --git a/src/blenderbim/test/bim/feature/misc.feature b/src/blenderbim/test/bim/feature/misc.feature index 282ec0d16b..e20ef274a8 100644 --- a/src/blenderbim/test/bim/feature/misc.feature +++ b/src/blenderbim/test/bim/feature/misc.feature @@ -28,6 +28,7 @@ Scenario: Resize to storey Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -40,6 +41,7 @@ Scenario: Split along edge Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add a plane of size "4" at "0,0,0" diff --git a/src/blenderbim/test/bim/feature/owner.feature b/src/blenderbim/test/bim/feature/owner.feature index 7852e2d70e..b1b96ae6e4 100644 --- a/src/blenderbim/test/bim/feature/owner.feature +++ b/src/blenderbim/test/bim/feature/owner.feature @@ -284,6 +284,7 @@ Scenario: Assign actor And I press "bim.add_actor" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()" @@ -297,6 +298,7 @@ Scenario: Unassign actor And I press "bim.add_actor" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()" diff --git a/src/blenderbim/test/bim/feature/project.feature b/src/blenderbim/test/bim/feature/project.feature index d61d895b43..c427e2e877 100644 --- a/src/blenderbim/test/bim/feature/project.feature +++ b/src/blenderbim/test/bim/feature/project.feature @@ -436,6 +436,7 @@ Scenario: Export IFC - with changed object scale synchronised Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -450,6 +451,7 @@ Scenario: Export IFC - with changed style colour synchronised And I add a cube And the object "Cube" is selected And I add a material + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -464,6 +466,7 @@ Scenario: Export IFC - with changed style element synchronised And I add a cube And the object "Cube" is selected And I add a material + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/pset.feature b/src/blenderbim/test/bim/feature/pset.feature index b4c87ffb1f..033c64627b 100644 --- a/src/blenderbim/test/bim/feature/pset.feature +++ b/src/blenderbim/test/bim/feature/pset.feature @@ -5,6 +5,7 @@ Scenario: Add pset - object Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -16,11 +17,13 @@ Scenario: Add pset - multiple objects Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -33,6 +36,7 @@ Scenario: Enable pset editing - object Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -48,6 +52,7 @@ Scenario: Enable pset editing - material Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -127,6 +132,7 @@ Scenario: Disable pset editing - object Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -139,6 +145,7 @@ Scenario: Disable pset editing - material Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -204,6 +211,7 @@ Scenario: Edit pset - object Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -216,6 +224,7 @@ Scenario: Edit qto - object Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -229,6 +238,7 @@ Scenario: Edit pset - material Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material(obj='')" @@ -294,10 +304,12 @@ Scenario: Copy property to selected - copy property Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -312,6 +324,7 @@ Scenario: Remove pset - object Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -327,11 +340,13 @@ Scenario: Remove pset - multiple objects Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/resource.feature b/src/blenderbim/test/bim/feature/resource.feature index 3e01cfa926..ac7522e774 100644 --- a/src/blenderbim/test/bim/feature/resource.feature +++ b/src/blenderbim/test/bim/feature/resource.feature @@ -274,6 +274,7 @@ Scenario: Calculate Resource Work And I press "bim.edit_task_time" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -307,6 +308,7 @@ Scenario: Assign Resource And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -322,6 +324,7 @@ Scenario: UnAssign Resource And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/root.feature b/src/blenderbim/test/bim/feature/root.feature index 28b9e71726..ea2a032a27 100644 --- a/src/blenderbim/test/bim/feature/root.feature +++ b/src/blenderbim/test/bim/feature/root.feature @@ -5,6 +5,7 @@ Scenario: Reassign class Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" And I press "object.duplicate_move" @@ -20,6 +21,7 @@ Scenario: Unlink object And I add a cube And the object "Cube" is selected And I add a material + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_material" @@ -32,6 +34,7 @@ Scenario: Copy class Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When I press "bim.copy_class(obj='IfcWall/Cube')" @@ -41,6 +44,7 @@ Scenario: Assign a class to a cube Given an empty IFC project And I add a cube When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" is an "IfcWall" @@ -87,6 +91,7 @@ Scenario: Assign a class to a cube in a collection And I add a cube When the object "Cube" is selected And the object "Cube" is placed in the collection "IfcBuildingStorey/My Storey" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" is contained in "My Storey" @@ -95,6 +100,7 @@ Scenario: Copy a wall Given an empty IFC project And I add a cube When the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I duplicate the selected objects diff --git a/src/blenderbim/test/bim/feature/search.feature b/src/blenderbim/test/bim/feature/search.feature index 7e1614b01a..80ec5a2c10 100644 --- a/src/blenderbim/test/bim/feature/search.feature +++ b/src/blenderbim/test/bim/feature/search.feature @@ -5,6 +5,7 @@ Scenario: Select all walls Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add a new item to "scene.IfcSelectorProperties.groups" diff --git a/src/blenderbim/test/bim/feature/sequence.feature b/src/blenderbim/test/bim/feature/sequence.feature index 4cd0e551cb..ecbaa64063 100644 --- a/src/blenderbim/test/bim/feature/sequence.feature +++ b/src/blenderbim/test/bim/feature/sequence.feature @@ -349,6 +349,8 @@ Scenario: Animate the construction of a wall And I press "bim.edit_task_time" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -385,6 +387,7 @@ Scenario: Animate the demolition of a wall And I press "bim.edit_task_time" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -424,6 +427,7 @@ Scenario: Animate the operation of a wall And I press "bim.edit_task_time" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -458,6 +462,7 @@ Scenario: Animate the movement of a wall And I add a cube And I rename the object "Cube" to "ToObject" And the object "ToObject" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/ToObject" is selected @@ -508,6 +513,7 @@ Scenario: Animate the consumption of a wall And I press "bim.edit_task_time" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -549,6 +555,7 @@ Scenario: Clear Previous Animation And I press "bim.edit_task_time" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected @@ -758,6 +765,7 @@ Scenario: Assign Product Output And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" And the object "IfcWall/Cube" is selected @@ -773,10 +781,11 @@ Scenario: Assign Product Input And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" And the object "IfcWall/Cube" is selected - And I press "bim.assign_process(task={task}, related_object_type='PRODUCT')" + And I press "bim.assign_process(task={task},related_object=0, related_object_type='PRODUCT')" Then nothing happens Scenario: Select Assigned Outputs @@ -788,9 +797,9 @@ Scenario: Select Assigned Outputs And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" - And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" - And I press "object.select_all(action='DESELECT')" + And I press "bim.assign_class()" And I press "bim.assign_product(task={task})" When I press "bim.select_task_related_products(task={task})" Then nothing happens @@ -804,9 +813,10 @@ Scenario: Select Assigned Inputs And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" - And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" - And I press "object.select_all(action='DESELECT')" + And I press "bim.assign_class()" + And the object "IfcWall/Cube" is selected And I press "bim.assign_process(task={task}, related_object_type='PRODUCT')" When I press "bim.select_task_related_products(task={task})" Then nothing happens @@ -849,6 +859,7 @@ Scenario: Add Animation Camera Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_animation_camera" diff --git a/src/blenderbim/test/bim/feature/spatial.feature b/src/blenderbim/test/bim/feature/spatial.feature index 4e0d84d88c..03e54ef479 100644 --- a/src/blenderbim/test/bim/feature/spatial.feature +++ b/src/blenderbim/test/bim/feature/spatial.feature @@ -21,7 +21,9 @@ Scenario: Assign container Given an empty IFC project And I add a cube And the object "Cube" is selected - And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()" @@ -32,7 +34,9 @@ Scenario: Copy to container Given an empty IFC project And I add a cube And the object "Cube" is selected - And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" @@ -43,7 +47,9 @@ Scenario: Reference structure Given an empty IFC project And I add a cube And the object "Cube" is selected - And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" @@ -54,7 +60,9 @@ Scenario: Dereference structure Given an empty IFC project And I add a cube And the object "Cube" is selected - And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" @@ -66,7 +74,9 @@ Scenario: Select container Given an empty IFC project And I add a cube And the object "Cube" is selected - And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()" @@ -78,7 +88,9 @@ Scenario: Select similar container Given an empty IFC project And I add a cube And the object "Cube" is selected - And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()" diff --git a/src/blenderbim/test/bim/feature/type.feature b/src/blenderbim/test/bim/feature/type.feature index 1e02167b6b..2e0fd06206 100644 --- a/src/blenderbim/test/bim/feature/type.feature +++ b/src/blenderbim/test/bim/feature/type.feature @@ -24,6 +24,7 @@ Scenario: Enable editing type Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When I press "bim.enable_editing_type" @@ -33,6 +34,7 @@ Scenario: Disable editing type Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.enable_editing_type" @@ -43,6 +45,7 @@ Scenario: Assign type - assign to an empty type Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -58,6 +61,7 @@ Scenario: Assign type - assign to a type with representation maps Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add a cube @@ -73,6 +77,7 @@ Scenario: Assign type - assign to a type with a material layer set, which automa Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -94,6 +99,7 @@ Scenario: Assign type - assign to a different type with a material layer set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -132,6 +138,7 @@ Scenario: Assign type - assign to a type with a material profile set Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -155,6 +162,7 @@ Scenario: Select type objects Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -172,10 +180,12 @@ Scenario: Select similar type Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty diff --git a/src/blenderbim/test/bim/feature/void.feature b/src/blenderbim/test/bim/feature/void.feature index 44691a35d1..53231ec61e 100644 --- a/src/blenderbim/test/bim/feature/void.feature +++ b/src/blenderbim/test/bim/feature/void.feature @@ -6,6 +6,7 @@ Scenario: Add an opening Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add a cube @@ -19,6 +20,7 @@ Scenario: Add an opening using the BIM tool Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_potential_opening" @@ -32,6 +34,7 @@ Scenario: Show openings Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_potential_opening" @@ -47,6 +50,7 @@ Scenario: Hide openings Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_potential_opening" @@ -62,6 +66,7 @@ Scenario: Edit openings Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_potential_opening" @@ -79,6 +84,7 @@ Scenario: Add an opening to Element B with a void that already voids Element A Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" @@ -105,6 +111,7 @@ Scenario: Remove opening Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_potential_opening" @@ -123,6 +130,7 @@ Scenario: Remove opening - using deletion Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_potential_opening" @@ -140,6 +148,7 @@ Scenario: Remove opening - indirectly by deleting its building element Given an empty IFC project And I add a cube And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.add_potential_opening" diff --git a/src/blenderbim/test/core/test_misc.py b/src/blenderbim/test/core/test_misc.py index 5f65c62867..9e420c0c23 100644 --- a/src/blenderbim/test/core/test_misc.py +++ b/src/blenderbim/test/core/test_misc.py @@ -39,11 +39,3 @@ class TestResizeToStorey: misc.get_object_storey("obj").should_be_called().will_return("storey") misc.get_storey_height_in_si("storey", 1).should_be_called().will_return(None) subject.resize_to_storey(misc, obj="obj", total_storeys=1) - - -class TestSplitAlongEdge: - def test_run(self, misc): - misc.split_objects_with_cutter(["obj"], "cutter").should_be_called().will_return(["new_obj"]) - misc.run_root_copy_class(obj="new_obj").should_be_called() - misc.mark_object_as_edited("obj").should_be_called() - subject.split_along_edge(misc, cutter="cutter", objs=["obj"]) diff --git a/src/blenderbim/test/tool/test_model.py b/src/blenderbim/test/tool/test_model.py index 68c91a142e..90546c0a16 100644 --- a/src/blenderbim/test/tool/test_model.py +++ b/src/blenderbim/test/tool/test_model.py @@ -20,6 +20,7 @@ import bpy import ifcopenshell import blenderbim.core.tool import blenderbim.tool as tool +import numpy as np from test.bim.bootstrap import NewFile from blenderbim.tool.model import Model as subject @@ -50,3 +51,31 @@ class TestGenerateOccurrenceName(NewFile): bpy.context.scene.BIMModelProperties.occurrence_name_style = "CUSTOM" bpy.context.scene.BIMModelProperties.occurrence_name_function = '"Foobar"' assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar" + +class TestGetManualBooleans(NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.Model) + + def test_len_returned_boolean(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + length = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT") + ifcopenshell.api.run("unit.assign_unit", ifc, units=[length]) + ifcopenshell.api.run("unit.assign_unit", ifc) + element = ifc.createIfcColumn() + hea100 = ifc.create_entity( + "IfcIShapeProfileDef", ProfileName="HEA100", ProfileType="AREA", + OverallWidth=100, OverallDepth=96, WebThickness=5, FlangeThickness=8, FilletRadius=12, + ) + model3d = ifcopenshell.api.run("context.add_context", ifc, context_type="Model") + body = ifcopenshell.api.run("context.add_context", ifc,context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d) + representation = ifcopenshell.api.run("geometry.add_profile_representation", ifc, context=body, profile=hea100, depth=5) + ifcopenshell.api.run("geometry.assign_representation", ifc, product=element, representation=representation) + matrix = np.eye(4) + matrix = ifcopenshell.util.placement.rotation(45,"X") @ matrix + matrix[:,3][0:3] = (0, 0, 3) + matrix = matrix.tolist() + ifcopenshell.api.run("geometry.add_boolean", ifc, representation = representation, type = "IfcHalfSpaceSolid", matrix = matrix) + assert len(subject.get_manual_booleans(element)) == 1 + diff --git a/src/bsdd/README.md b/src/bsdd/README.md index 215845a321..5d92dbe9d5 100644 --- a/src/bsdd/README.md +++ b/src/bsdd/README.md @@ -1,21 +1,3 @@ # bsdd -An experimental work in progress library to interact with the buildingSMART Data Dictionary (bSDD) API. - -More reading: - - * [Swagger API docs](https://bs-dd-api-prototype.azurewebsites.net/swagger/index.html) - * [bSDD Github Repository](https://github.com/buildingSMART/bSDD) - -# Demo - -Let's replicate the SketchUp example: - -``` -client = Client() -pprint(client.Domain()) -pprint(client.SearchListOpen("http://identifier.buildingsmart.org/uri/nlsfb/nlsfb2005-2.2", RelatedIfcEntity="IfcWall")) -data = client.Classification("http://identifier.buildingsmart.org/uri/nlsfb/nlsfb2005-2.2/class/21.21") -pprint(data) -apply_ifc_classification_properties(ifc_file, element, data["classificationProperties"]) -``` +A library to interact with the buildingSMART Data Dictionary (bSDD) API. diff --git a/src/bsdd/bsdd.py b/src/bsdd/bsdd.py index 0e0f704cc5..637e36912c 100644 --- a/src/bsdd/bsdd.py +++ b/src/bsdd/bsdd.py @@ -52,7 +52,7 @@ class Client: headers = {} if is_auth_required: headers = {"Authorization": "Bearer " + self.get_access_token()} - return requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None).json() + return requests.get(f"{self.baseurl}{endpoint}", timeout=10, headers=headers, params=params or None).json() def post(self): pass # TODO @@ -121,6 +121,20 @@ class Client: }, ) + def ClassificationSearchOpen(self, SearchText, version="v1", DomainNamespaceUris=None, RelatedIfcEntities=None): + if DomainNamespaceUris is None: + DomainNamespaceUris = [] + if RelatedIfcEntities is None: + RelatedIfcEntities = [] + return self.get( + f"api/ClassificationSearchOpen/{version}", + { + "SearchText": SearchText, + "DomainNamespaceUris": DomainNamespaceUris, + "RelatedIfcEntities": RelatedIfcEntities, + }, + ) + def Country(self, version="v1"): return self.get(f"api/Country/{version}") diff --git a/src/ifc4d/ifc4d/csv2ifc.py b/src/ifc4d/ifc4d/csv2ifc.py index 7ef6a8b6d8..e60d45dd4a 100644 --- a/src/ifc4d/ifc4d/csv2ifc.py +++ b/src/ifc4d/ifc4d/csv2ifc.py @@ -20,6 +20,7 @@ import csv import ifcopenshell import ifcopenshell.api import ifcopenshell.util.unit +import datetime class Csv2Ifc: @@ -28,6 +29,14 @@ class Csv2Ifc: self.file = None self.resources = [] self.units = {} + self.resource_map = { + "CREW": "IfcCrewResource", + "LABOR": "IfcLaborResource", + "EQUIPMENT": "IfcConstructionEquipmentResource", + "SUBCONTRACTOR": "IfcSubContractResource", + "MATERIAL": "IfcConstructionMaterialResource", + "PRODUCT": "IfcConstructionProductResource", + } def execute(self): self.parse_csv() @@ -41,43 +50,46 @@ class Csv2Ifc: for row in reader: if not row[0]: continue - if row[0] == "Hierarchy": + if row[0] == "HIERARCHY": for i, col in enumerate(row): if not col: continue self.headers[col] = i continue - cost_data = self.get_row_resource_data(row) + resource_data = self.get_row_resource_data(row) hierarchy_key = int(row[0]) if hierarchy_key == 1: - self.resources.append(cost_data) + self.resources.append(resource_data) else: - self.parents[hierarchy_key - 1]["children"].append(cost_data) - self.parents[hierarchy_key] = cost_data + self.parents[hierarchy_key - 1]["children"].append(resource_data) + self.parents[hierarchy_key] = resource_data def get_row_resource_data(self, row): - name = row[self.headers["Name"]] - identification = row[self.headers["Identification"]] if "Identification" in self.headers else None + name = row[self.headers["ACTIVITY/RESOURCE NAME"]] + resource_class = self.resource_map[row[self.headers["TYPE"]]] + base_cost_value = row[self.headers["COST"]] + productivity = {} - type = row[self.headers["Type"]] - base_cost_value = row[self.headers["BaseCostValue"]] - base_cost_quantity = row[self.headers["BaseCostQuantity"]] - base_cost_unit = row[self.headers["QuantityUnit"]] + if resource_class in ["IfcConstructionEquipmentResource", "IfcLaborResource"]: + output_ratio = row[self.headers["LABOR OUTPUT"]] + if not output_ratio: + output_ratio = row[self.headers["EQUIPMENT OUTPUT"]] + if output_ratio: + time_consumed = datetime.timedelta(minutes=float(output_ratio) * 60) + time_consumed = ifcopenshell.util.date.datetime2ifc(time_consumed, "IfcDuration") - productivity = { - "BaseQuantityConsumed": row[self.headers["BaseQuantityConsumed"]], - "BaseQuantityProducedName": row[self.headers["BaseQuantityProducedName"]], - "BaseQuantityProducedValue": row[self.headers["BaseQuantityProducedValue"]], - } + productivity = { + "BaseQuantityConsumed": time_consumed, + "BaseQuantityProducedName": row[self.headers["QUANTITY NAME"]], + "BaseQuantityProducedValue": 1, + } return { - "Identification": str(identification).strip() if identification else None, "Name": str(name).strip() if name else None, - "Type": type, + "Description": row[self.headers["DESCRIPTION"]], + "class": resource_class, "BaseCostValue": float(base_cost_value) if base_cost_value else None, - "BaseCostQuantity": float(base_cost_quantity) if base_cost_quantity else None, - "Unit": str(base_cost_unit).strip() if base_cost_unit else None, - "Productivity": productivity if productivity["BaseQuantityProducedName"] else None, + "Productivity": productivity, "children": [], } @@ -92,45 +104,25 @@ class Csv2Ifc: def create_resource(self, resource, parent): if parent is None: - resource["ifc"] = ifcopenshell.api.run("resource.add_resource", self.file, ifc_class=resource["Type"]) + resource["ifc"] = ifcopenshell.api.run("resource.add_resource", self.file, ifc_class=resource["class"]) else: resource["ifc"] = ifcopenshell.api.run( - "resource.add_resource", self.file, parent_resource=parent, ifc_class=resource["Type"] + "resource.add_resource", self.file, parent_resource=parent, ifc_class=resource["class"] ) resource["ifc"].Name = resource["Name"] - resource["ifc"].Identification = resource["Identification"] - productivity = resource["Productivity"] - if productivity: + if resource.get("Description", None): + resource["ifc"].Description = resource.get("Description") + if resource["Productivity"]: pset = ifcopenshell.api.run("pset.add_pset", self.file, product=resource["ifc"], name="EPset_Productivity") ifcopenshell.api.run( "pset.edit_pset", self.file, pset=pset, - properties=productivity, + properties=resource["Productivity"], ) if resource["BaseCostValue"]: cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=resource["ifc"]) cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(resource["BaseCostValue"]) - if resource["Unit"]: - measure_class = ifcopenshell.util.unit.get_symbol_measure_class(resource["Unit"]) - print(measure_class) - value_component = self.file.create_entity(measure_class, resource["BaseCostQuantity"]) - print(value_component) - unit_component = None - if measure_class == "IfcNumericMeasure": - unit_component = self.create_unit(resource["Unit"]) - else: - unit_type = ifcopenshell.util.unit.get_measure_unit_type(measure_class) - print(unit_type) - unit_assignment = ifcopenshell.util.unit.get_unit_assignment(self.file) - if unit_assignment: - units = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == unit_type] - if units: - unit_component = units[0] - if not unit_component: - unit_component = self.create_unit(resource["Unit"], unit_type) - print(unit_component) - cost_value.UnitBasis = self.file.createIfcMeasureWithUnit(value_component, unit_component) self.create_resources(resource["children"], resource["ifc"]) def create_unit(self, symbol, unit_type): diff --git a/src/ifc4d/ifc4d/msp2ifc.py b/src/ifc4d/ifc4d/msp2ifc.py index 470bcc435b..159d8279e9 100644 --- a/src/ifc4d/ifc4d/msp2ifc.py +++ b/src/ifc4d/ifc4d/msp2ifc.py @@ -17,7 +17,7 @@ # along with Ifc4D. If not, see . import datetime -from datetime import timedelta +from datetime import timedelta, date import ifcopenshell import ifcopenshell.api import ifcopenshell.util.date @@ -107,34 +107,79 @@ class MSP2Ifc: } def parse_calendar_xml(self, project): + def parse_working_times(day): + working_times = [] + if day.find("pr:WorkingTimes", self.ns): + for working_time in day.find("pr:WorkingTimes", self.ns).findall("pr:WorkingTime", self.ns): + if working_time.find("pr:FromTime", self.ns) is None: + continue + working_times.append( + { + "Start": datetime.time.fromisoformat(working_time.find("pr:FromTime", self.ns).text), + "Finish": datetime.time.fromisoformat(working_time.find("pr:ToTime", self.ns).text), + } + ) + return working_times + + def parse_exception(exception): + work_times = parse_working_times(exception) + time_period = exception.find("pr:TimePeriod", self.ns) + data = { + "Name": exception.find("pr:Name", self.ns).text + if exception.find("pr:Name", self.ns) is not None + else None, + "FromDate": datetime.datetime.fromisoformat(time_period.find("pr:FromDate", self.ns).text) + if time_period is not None + else None, + "ToDate": datetime.datetime.fromisoformat(time_period.find("pr:ToDate", self.ns).text) + if time_period is not None + else None, + "Occurrences": int(exception.find("pr:Occurrences", self.ns).text) + if exception.find("pr:Occurrences", self.ns) is not None + else None, + "Month": exception.find("pr:Month", self.ns).text + if exception.find("pr:Month", self.ns) is not None + else None, + "MonthDay": exception.find("pr:MonthDay", self.ns).text + if exception.find("pr:MonthDay", self.ns) is not None + else None, + "Type": exception.find("pr:Type", self.ns).text + if exception.find("pr:Type", self.ns) is not None + else None, + "WorkingTimes": work_times, + "ifc": None, + } + return data + for calendar in project.find("pr:Calendars", self.ns).findall("pr:Calendar", self.ns): calendar_id = calendar.find("pr:UID", self.ns).text week_days = [] + exceptions = [] week_days_element = calendar.find("pr:WeekDays", self.ns) week_day_elements = week_days_element.findall("pr:WeekDay", self.ns) if week_days_element else [] for week_day in week_day_elements: - working_times = [] if week_day.find("pr:WorkingTimes", self.ns): - for working_time in week_day.find("pr:WorkingTimes", self.ns).findall("pr:WorkingTime", self.ns): - if working_time.find("pr:FromTime", self.ns) is None: - continue - working_times.append( + if week_day.find("pr:DayType", self.ns).text == "0": + data = parse_exception(week_day) + data["Type"] = "2" + exceptions.append(data) + else: + week_days.append( { - "Start": datetime.time.fromisoformat(working_time.find("pr:FromTime", self.ns).text), - "Finish": datetime.time.fromisoformat(working_time.find("pr:ToTime", self.ns).text), + "DayType": week_day.find("pr:DayType", self.ns).text, + "WorkingTimes": parse_working_times(week_day), + "ifc": None, } ) - week_days.append( - { - "DayType": week_day.find("pr:DayType", self.ns).text, - "WorkingTimes": working_times, - "ifc": None, - } - ) - exceptions = {} + exceptions_element = calendar.find("pr:Exceptions", self.ns) + for exception in exceptions_element.findall("pr:Exception", self.ns) if exceptions_element else []: + data = parse_exception(exception) + exceptions.append(data) + self.calendars[calendar_id] = { "Name": calendar.find("pr:Name", self.ns).text, "StandardWorkWeek": week_days, + "HolidayOrExceptions": exceptions, } def create_ifc(self): @@ -163,9 +208,15 @@ class MSP2Ifc: ) def create_calendars(self): + def has_work_or_exceptions(calendar): + return calendar["StandardWorkWeek"] or calendar["HolidayOrExceptions"] + for calendar in self.calendars.values(): + if not has_work_or_exceptions(calendar): + continue calendar["ifc"] = ifcopenshell.api.run("sequence.add_work_calendar", self.file, name=calendar["Name"]) self.process_working_week(calendar["StandardWorkWeek"], calendar["ifc"]) + self.process_exceptions(calendar["HolidayOrExceptions"], calendar["ifc"]) def create_task(self, task, work_schedule=None, parent_task=None): task["ifc"] = ifcopenshell.api.run( @@ -218,13 +269,14 @@ class MSP2Ifc: def process_working_week(self, week, calendar): day_map = { - "1": 7, # Sunday - "2": 1, # Monday - "3": 2, # Tuesday - "4": 3, # Wednesday - "5": 4, # Thursday - "6": 5, # Friday - "7": 6, # Saturday + "1": 7, # Sunday + "2": 1, # Monday + "3": 2, # Tuesday + "4": 3, # Wednesday + "5": 4, # Thursday + "6": 5, # Friday + "7": 6, # Saturday + "0": 0, # Exception } for day in week: if day["ifc"]: @@ -297,14 +349,12 @@ class MSP2Ifc: def parse_resources_xml(self, project): resources_lst = project.find("pr:Resources", self.ns) resources = resources_lst.findall("pr:Resource", self.ns) - # print("Resource text", resources[4].find("pr:Name", self.ns).text) for resource in resources: name = resource.find("pr:Name", self.ns) id = resource.find("pr:ID", self.ns).text if name is not None: name = name.text else: - # print("- No Name") name = None self.resources[id] = { "Name": name, @@ -314,4 +364,62 @@ class MSP2Ifc: "ifc": None, "rel": None, } - print("Resource found", self.resources) + + def process_exceptions(self, exceptions, calendar): + for exception in exceptions or []: + self.process_exception(exception, calendar) + + def process_exception(self, exception, calendar): + if exception["ifc"] or not exception["FromDate"]: + return + exception["ifc"] = ifcopenshell.api.run( + "sequence.add_work_time", self.file, work_calendar=calendar, time_type="ExceptionTimes" + ) + ifcopenshell.api.run( + "sequence.edit_work_time", + self.file, + work_time=exception["ifc"], + attributes={ + "Name": exception["Name"], + "Start": ifcopenshell.util.date.datetime2ifc(exception["FromDate"], "IfcDate"), + "Finish": ifcopenshell.util.date.datetime2ifc(exception["ToDate"], "IfcDate"), + }, + ) + # BIG assumptions due to missing types enumeration in docs https://learn.microsoft.com/en-us/office-project/xml-data-interchange/exception-element?view=project-client-2016 + recurrence_type = None + if exception["Type"] == "1": + recurrence_type = "DAILY" + attributes = { + "Occurrences": int(exception["Occurrences"]) if exception["Occurrences"] else None, + } + elif exception["Type"] == "2": + recurrence_type = "YEARLY_BY_DAY_OF_MONTH" + month_component = [int(exception["Month"]) + 1] if exception["Month"] else None + day_component = [int(exception["MonthDay"])] if exception["MonthDay"] else None + if month_component is None and (exception["FromDate"].date().day == exception["ToDate"].date().day): + month_component = [exception["FromDate"].date().month] + day_component = [exception["FromDate"].date().day] + attributes = { + "MonthComponent": month_component, + "DayComponent": day_component, + "Occurrences": int(exception["Occurrences"]) if exception["Occurrences"] else None, + } + else: + return + recurrence = ifcopenshell.api.run( + "sequence.assign_recurrence_pattern", + self.file, + parent=exception["ifc"], + recurrence_type=recurrence_type, + ) + ifcopenshell.api.run( + "sequence.edit_recurrence_pattern", self.file, recurrence_pattern=recurrence, attributes=attributes + ) + for work_time in exception["WorkingTimes"] or []: + ifcopenshell.api.run( + "sequence.add_time_period", + self.file, + recurrence_pattern=recurrence, + start_time=work_time["Start"], + end_time=work_time["Finish"], + ) diff --git a/src/ifc4d/ifc4d/resource_spreadsheet.csv b/src/ifc4d/ifc4d/resource_spreadsheet.csv new file mode 100644 index 0000000000..47fea5d101 --- /dev/null +++ b/src/ifc4d/ifc4d/resource_spreadsheet.csv @@ -0,0 +1,63 @@ +HIERARCHY,TYPE,ACTIVITY/RESOURCE NAME,DESCRIPTION,COST,USAGE,UNIT,QUANTITY NAME,LABOR OUTPUT,EQUIPMENT OUTPUT,Productivity Unit, +1,CREW,CONCRETE WORKS,,,,,,,,, +2,LABOR,BEAMS,,,,,,,,, +3,LABOR,"Beams, 745 kg/m ",3 m span,,,Cubic Meters ,,27.3,8.61,Hr / Meter, +4,LABOR,Foreman,,25,1,Cubic Meters ,Length,27.3,,Hr / Meter, +4,LABOR, Mason,,25,4,Cubic Meters ,Length,109.2,,Hr / Meter, +4,LABOR,Carpenter &Steelman,,25,10,Cubic Meters ,Length,273,,Hr / Meter, +4,LABOR,Laborer,,25,0.25,Cubic Meters ,Length,6.825,,Hr / Meter, +4,EQUIPMENT,Crane,,25,0.125,Cubic Meters ,Length,,1.07625,Hr / Meter, +,,,,,,,,,,, +3,LABOR,"Beams, 745 kg/m ",7.5 m span,,,Cubic Meters ,,22.02,6.94,Hr / Meter, +4,LABOR,Foreman,,25,1,Cubic Meters ,Length,22.02,,Hr / Meter, +4,LABOR, Mason,,25,4,Cubic Meters ,Length,88.08,,Hr / Meter, +4,LABOR,Carpenter &Steelman,,25,10,Cubic Meters ,Length,220.2,,Hr / Meter, +4,LABOR,Laborer,,25,0.25,Cubic Meters ,Length,5.505,,Hr / Meter, +4,EQUIPMENT,Crane,,25,0.125,Cubic Meters ,Length,,0.8675,Hr / Meter, +,,,,,,,,,,, +2,LABOR,SLABS,,,,,,,,, +3,LABOR,In-situ,200 mm thick (concreting & finish only),,,Square Meters ,,0.22,0.2,Hr / Square Meters, +4,LABOR,Foreman,,25,0.5,Square Meters ,GrossArea,0.11,,Hr / Square Meters, +4,LABOR,Laborer,,25,3,Square Meters ,GrossArea,0.66,,Hr / Square Meters, +4,LABOR,Carpenter,,25,4,Square Meters ,GrossArea,0.88,,Hr / Square Meters, +4,LABOR,Steelfixers,,25,2.5,Square Meters ,GrossArea,0.55,,Hr / Square Meters, +4,EQUIPMENT,Crane,,25,0.4,Square Meters ,GrossArea,,0.08,Hr / Square Meters, +,,,,,,,,,,, +2,LABOR,FOUNDATIONS,,,,,,,,, +3,LABOR,Drainage,,25,,Meters,Length,3.5,,Hr / Meter, +,,,,,,,,,,, +,,,,,,,,,,, +2,LABOR,WATERPROOFING,,,,,,,,, +3,LABOR,Drainage,,25,,Square Meters ,GrossArea,0.5,,Hr / Square Meters, +,,,,,,,,,,, +,,,,,,,,,,, +2,LABOR,STAIRS,,,,,,,,, +3,LABOR,STAIRS,"300 mm wide (Incld. forms, rebar, & finish)",,,Cubic Meters ,,102.16,36.89,Hr / Cubic Meters, +4,LABOR,Foreman,,25,0.5,Cubic Meters ,GrossVolume,51.08,,Hr / Cubic Meters, +4,LABOR,Carpenter,,25,6,Cubic Meters ,GrossVolume,612.96,,Hr / Cubic Meters, +4,LABOR,Steelman,,25,2,Cubic Meters ,GrossVolume,204.32,,Hr / Cubic Meters, +4,LABOR,Mason,,25,2,Cubic Meters ,GrossVolume,204.32,,Hr / Cubic Meters, +4,LABOR,Laborer,,25,2.5,Cubic Meters ,GrossVolume,255.4,,Hr / Cubic Meters, +4,EQUIPMENT,Crane,,25,0.375,Cubic Meters ,GrossVolume,,13.83375,Hr / Cubic Meters, +3,LABOR,STAIRS LANDING,"Incld. forms, rebar, & finish",,,Cubic Meters ,,12.76,4.02,Hr / Cubic Meters, +4,LABOR,Foreman,,25,1,Cubic Meters ,GrossVolume,12.76,,Hr / Cubic Meters, +4,LABOR,Carpenter & SteelFixers,,25,10,Cubic Meters ,GrossVolume,127.6,,Hr / Cubic Meters, +4,LABOR,Laborer,,25,4,Cubic Meters ,GrossVolume,51.04,,Hr / Cubic Meters, +4,LABOR,Mason,,25,0.125,Cubic Meters ,GrossVolume,1.595,,Hr / Cubic Meters, +4,EQUIPMENT,Crane,,25,0.125,Cubic Meters ,GrossVolume,,0.5025,Hr / Cubic Meters, +,,,,,,,,,,, +1,CREW,MASONRY,,,,,,,,, +2,LABOR,CONCRETE BLOCKS,,,,,,,,, +3,LABOR,Hollow Blocks 100mm,100mm thick,,,Square Meters ,,1.17,0.29,Hr / Cubic Meters, +4,LABOR,Foreman,,25,0.25,Square Meters ,GrossSideArea,0.2925,,Hr / Cubic Meters, +4,LABOR,Mason,,25,4,Square Meters ,GrossSideArea,4.68,,Hr / Cubic Meters, +4,LABOR,Laborer,,25,2,Square Meters ,GrossSideArea,2.34,,Hr / Cubic Meters, +4,EQUIPMENT,Crane,,25,0.125,Square Meters ,GrossSideArea,,0.03625,Hr / Cubic Meters, +4,EQUIPMENT,Fork Lift,,25,0.125,Square Meters ,GrossSideArea,,0.03625,Hr / Cubic Meters, +4,EQUIPMENT,Truck,,25,0.125,Square Meters ,GrossSideArea,,0.03625,Hr / Cubic Meters, +3,LABOR,Hollow Blocks 200mm,100mm thick,,,Square Meters ,,1.32,0.33,Hr / Cubic Meters, +4,LABOR,Foreman,,25,0.25,Square Meters ,GrossSideArea,0.33,,Hr / Cubic Meters, +4,LABOR,Mason,,25,4,Square Meters ,GrossSideArea,5.28,,Hr / Cubic Meters, +4,LABOR,Laborer,,25,2,Square Meters ,GrossSideArea,2.64,,Hr / Cubic Meters, +4,EQUIPMENT,Crane,,25,0.125,Square Meters ,GrossSideArea,,0.5025,Hr / Cubic Meters, +4,EQUIPMENT,Fork Lift,,25,0.125,Square Meters ,GrossSideArea,,0.5025,Hr / Cubic Meters, diff --git a/src/ifc4d/ifc4d/resource_spreadsheet.ods b/src/ifc4d/ifc4d/resource_spreadsheet.ods new file mode 100644 index 0000000000..d2deaf274f Binary files /dev/null and b/src/ifc4d/ifc4d/resource_spreadsheet.ods differ diff --git a/src/ifccobie/COPYING b/src/ifccobie/COPYING deleted file mode 100644 index 810fce6e9b..0000000000 --- a/src/ifccobie/COPYING +++ /dev/null @@ -1,621 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS diff --git a/src/ifccobie/COPYING.LESSER b/src/ifccobie/COPYING.LESSER deleted file mode 100644 index 0a041280bd..0000000000 --- a/src/ifccobie/COPYING.LESSER +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/src/ifccobie/cobie.py b/src/ifccobie/cobie.py deleted file mode 100755 index c5cde601eb..0000000000 --- a/src/ifccobie/cobie.py +++ /dev/null @@ -1,1667 +0,0 @@ -#!/usr/bin/env python3 - -# IfcCOBie - Extract COBie data from IFC to spreadsheets -# Copyright (C) 2019, 2020, 2021 Dion Moult -# -# This file is part of IfcCOBie. -# -# IfcCOBie is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcCOBie is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcCOBie. If not, see . - -# This can be packaged with `pyinstaller --onefile --clean --icon=icon.ico bimtester.py` - -import os -import time -import argparse -import datetime -import logging -import ifcopenshell -import ifcopenshell.util.selector -import ifcopenshell.util.placement - - -class IfcCobieParser: - def __init__(self, logger, selector): - self.selector = selector - self.logger = logger - self.file = None - self.sheets = [ - "contacts", - "facilities", - "floors", - "spaces", - "zones", - "types", - "components", - "systems", - "assemblies", - "connections", - "spares", - "resources", - "jobs", - "impacts", - "documents", - "attributes", - "coordinates", - "issues", - ] - for sheet in self.sheets: - setattr(self, sheet, {}) - self.picklists = { - "Category-Role": [], - "Category-Facility": [], - "FloorType": [], - "Category-Space": [], - "ZoneType": [], - "Category-Product": [], - "AssetType": [], - "DurationUnit": ["day"], # See note about hardcoded day below - "Category-Element": [], - "SpareType": [], - "ApprovalBy": [], - "StageType": [], - "objType": [], - } - self.default_date = (datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=-2177452801)).isoformat() - - def parse(self, file, type_query=".COBieType", component_query=".COBie", custom_data={}): - self.custom_data = custom_data - for sheet in self.sheets: - if sheet not in self.custom_data: - self.custom_data[sheet] = {} - - if isinstance(file, str): - self.file = ifcopenshell.open(file) - else: - self.file = file - - self.type_assets = self.selector.parse(self.file, type_query) - self.component_assets = self.selector.parse(self.file, component_query) - self.get_contacts() - self.get_facilities() - self.get_floors() - self.get_spaces() - self.get_zones() - self.get_types() - self.get_components() - self.get_systems() - self.get_assemblies() - self.get_connections() - self.get_spares() - self.get_resources() - self.get_jobs() - self.get_impacts() - self.get_documents() - self.get_attributes() - self.get_coordinates() - self.get_issues() - - def get_contacts(self): - histories = self.file.by_type("IfcOwnerHistory") - for history in histories: - email = self.get_email_from_history(history) - if not email: - continue - postal_address = self.get_postal_address_from_history(history) - self.contacts[email] = { - "CreatedBy": email, - "CreatedOn": datetime.datetime.fromtimestamp(history.CreationDate).isoformat() - if history.CreationDate - else datetime.datetime.now().isoformat(), - "Category": self.get_category_from_history(history), - "Company": history.OwningUser.TheOrganization.Name or "n/a", - "Phone": self.get_phone_from_history(history), - "ExtSystem": self.get_ext_system_from_history(history), - "ExtObject": self.get_ext_object_from_history(history), - "ExtIdentifier": history.OwningUser.ThePerson.Id - if self.file.schema == "IFC2X3" - else history.OwningUser.ThePerson.Identification, - "Department": self.get_department_from_history(history), - "OrganizationCode": (history.OwningUser.TheOrganization.Id or "n/a") - if self.file.schema == "IFC2X3" - else (history.OwningUser.TheOrganization.Identification or "n/a"), - "GivenName": self.get_name_from_person(history.OwningUser.ThePerson, "GivenName"), - "FamilyName": self.get_name_from_person(history.OwningUser.ThePerson, "FamilyName"), - "Street": self.get_lines_from_address(postal_address), - "PostalBox": self.get_attribute_from_address(postal_address, "PostalBox"), - "Town": self.get_attribute_from_address(postal_address, "Town"), - "StateRegion": self.get_attribute_from_address(postal_address, "Region"), - "PostalCode": self.get_attribute_from_address(postal_address, "PostalCode"), - "Country": self.get_attribute_from_address(postal_address, "Country"), - } - for field, key in self.custom_data["contacts"].items(): - self.contacts[email][field] = self.get_element_value(history, key) - - def get_facilities(self): - buildings = self.file.by_type("IfcBuilding") - for building in buildings: - building_name = self.get_object_name(building) - units = self.get_units_from_building(building) - self.facilities[building_name] = { - "CreatedBy": self.get_email_from_history(building.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(building.OwnerHistory), - "Category": self.get_category_from_object(building, "Category-Facility"), - "ProjectName": self.get_project_name_from_building(building), - "SiteName": self.get_site_name_from_building(building), - "LinearUnits": self.get_unit_type_from_units(units, "LENGTHUNIT"), - "AreaUnits": self.get_unit_type_from_units(units, "AREAUNIT"), - "VolumeUnits": self.get_unit_type_from_units(units, "VOLUMEUNIT"), - "CostUnit": self.get_monetary_unit_from_units(units), - "AreaMeasurement": self.get_area_measurement_from_building(building), - "ExternalSystem": self.get_ext_system_from_history(building.OwnerHistory), - "ExternalProjectObject": self.get_ext_project_object(), - "ExternalProjectIdentifier": self.get_project_globalid_from_building(building), - "ExternalSiteObject": self.get_ext_site_object(), - "ExternalSiteIdentifier": self.get_site_globalid_from_building(building), - "ExternalFacilityObject": self.get_ext_object(building), - "ExternalFacilityIdentifier": building.GlobalId, - "Description": self.get_object_attribute(building, "Description", default="n/a"), - "ProjectDescription": self.get_object_attribute( - self.get_parent_spatial_element(building, "IfcProject"), "Description", default="n/a" - ), - "SiteDescription": self.get_object_attribute( - self.get_parent_spatial_element(building, "IfcSite"), "Description", default="n/a" - ), - "Phase": self.get_object_attribute( - self.get_parent_spatial_element(building, "IfcProject"), "Phase", default="n/a" - ), - } - for field, key in self.custom_data["facilities"].items(): - self.facilities[building_name][field] = self.get_element_value(building, key) - - def get_floors(self): - storeys = self.file.by_type("IfcBuildingStorey") - for storey in storeys: - storey_name = self.get_object_name(storey) - self.floors[storey_name] = { - "CreatedBy": self.get_email_from_history(storey.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(storey.OwnerHistory), - "Category": self.get_category_from_object(storey, "FloorType"), - "ExtSystem": self.get_ext_system_from_history(storey.OwnerHistory), - "ExtObject": self.get_ext_object(storey), - "ExtIdentifier": storey.GlobalId, - "Description": self.get_object_attribute(storey, "Description", default="n/a"), - "Elevation": self.get_object_attribute(storey, "Elevation", default="n/a"), - "Height": self.get_height_from_storey(storey), - } - for field, key in self.custom_data["floors"].items(): - self.floors[storey_name][field] = self.get_element_value(storey, key) - - def get_spaces(self): - spaces = self.file.by_type("IfcSpace") - for space in spaces: - space_name = self.get_object_name(space) - self.spaces[space_name] = { - "CreatedBy": self.get_email_from_history(space.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(space.OwnerHistory), - "Category": self.get_category_from_object(space, "Category-Space"), - "FloorName": self.get_object_attribute( - self.get_parent_spatial_element(space, "IfcBuildingStorey"), - "Name", - is_primary_key=True, - default="n/a", - ), - "Description": self.get_object_attribute(space, "Description", default="n/a"), - "ExtSystem": self.get_ext_system_from_history(space.OwnerHistory), - "ExtObject": self.get_ext_object(space), - "ExtIdentifier": space.GlobalId, - "RoomTag": self.get_pset_value_from_object(space, "COBie_Space", "RoomTag", "n/a"), - "UsableHeight": self.get_usable_height_from_space(space), - "GrossArea": self.get_gross_area_from_space(space), - "NetArea": self.get_net_area_from_space(space), - } - for field, key in self.custom_data["spaces"].items(): - self.spaces[space_name][field] = self.get_element_value(space, key) - - def get_zones(self): - zones = self.file.by_type("IfcZone") - for zone in zones: - zone_name = self.get_object_name(zone) - self.zones[zone_name] = { - "CreatedBy": self.get_email_from_history(zone.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(zone.OwnerHistory), - "Category": self.get_category_from_object(zone, "ZoneType"), - "SpaceNames": self.get_grouped_product_names_from_object(zone, "IfcSpace"), - "ExtSystem": self.get_ext_system_from_history(zone.OwnerHistory), - "ExtObject": self.get_ext_object(zone), - "ExtIdentifier": zone.GlobalId, - "Description": self.get_object_attribute(zone, "Description", default="n/a"), - } - for field, key in self.custom_data["zones"].items(): - self.zones[zone_name][field] = self.get_element_value(zone, key) - - def get_types(self): - types = self.file.by_type("IfcTypeObject") - for type in self.type_assets: - # The responsibility matrix states to parse IfcMaterial and - # IfcMaterialLayerSet too, but it doesn't make much sense, so I - # don't parse it. - type_name = self.get_object_name(type) - self.types[type_name] = { - "CreatedBy": self.get_email_from_history(type.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(type.OwnerHistory), - "Category": self.get_category_from_object(type, "Category-Product"), - # The responsibility matrix states two possible fallbacks. I - # choose the 'n/a' option as opposed to repeating the name. - "Description": self.get_object_attribute(type, "Description", default="n/a"), - "AssetType": self.get_pset_value_from_object(type, "COBie_Asset", "AssetType", "n/a", "AssetType"), - "Manufacturer": self.get_contact_pset_value_from_object( - type, "Pset_ManufacturerTypeInformation", "Manufacturer" - ), - "ModelNumber": self.get_pset_value_from_object( - type, "Pset_ManufacturerTypeInformation", "ModelLabel", "n/a" - ), - # The responsibility matrix talks about using the Pset_Warranty - # values, but Pset_Warranty only applies to objects, not types, - # and so they are ignored. - "WarrantyGuarantorParts": self.get_contact_pset_value_from_object( - type, "COBie_Warranty", "WarrantyGuarantorParts" - ), - "WarrantyDurationParts": self.get_pset_value_from_object( - type, "COBie_Warranty", "WarrantyDurationParts", 0 - ), - "WarrantyGuarantorLabor": self.get_contact_pset_value_from_object( - type, "COBie_Warranty", "WarrantyGuarantorLabor" - ), - "WarrantyDurationLabor": self.get_pset_value_from_object( - type, "COBie_Warranty", "WarrantyDurationLabor", 0 - ), - # TODO: this may be derived from the duration values above, but - # until it is clarified, it will be hardcoded as 'day' - "WarrantyDurationUnit": "day", - "ExtSystem": self.get_ext_system_from_history(type.OwnerHistory), - "ExtObject": self.get_ext_object(type), - "ExtIdentifier": type.GlobalId, - "ReplacementCost": self.get_pset_value_from_object( - type, "COBie_EconomicImpactValues", "ReplacementCost", "n/a" - ), - "ExpectedLife": self.get_expected_life_from_type(type), - # See note about WarrantyDurationUnit above - "DurationUnit": "day", - "NominalLength": self.get_pset_value_from_object(type, "COBie_Specification", "NominalLength", 0), - "NominalWidth": self.get_pset_value_from_object(type, "COBie_Specification", "NominalWidth", 0), - "NominalHeight": self.get_pset_value_from_object(type, "COBie_Specification", "NominalHeight", 0), - "ModelReference": self.get_pset_value_from_object( - type, "Pset_ManufacturerTypeInformation", "ModelReference", "n/a" - ), - "Shape": self.get_pset_value_from_object(type, "COBie_Specification", "Shape", "n/a"), - "Size": self.get_pset_value_from_object(type, "COBie_Specification", "Size", "n/a"), - # The responsbility matrix allows the British spelling of - # "colour". I, however, do not. - "Color": self.get_pset_value_from_object(type, "COBie_Specification", "Color", "n/a"), - "Finish": self.get_pset_value_from_object(type, "COBie_Specification", "Finish", "n/a"), - "Grade": self.get_pset_value_from_object(type, "COBie_Specification", "Grade", "n/a"), - "Material": self.get_pset_value_from_object(type, "COBie_Specification", "Material", "n/a"), - "Constituents": self.get_pset_value_from_object(type, "COBie_Specification", "Constituents", "n/a"), - "Features": self.get_pset_value_from_object(type, "COBie_Specification", "Features", "n/a"), - "AccessibilityPerformance": self.get_pset_value_from_object( - type, "COBie_Specification", "AccessibilityPerformance", "n/a" - ), - "CodePerformance": self.get_pset_value_from_object( - type, "COBie_Specification", "CodePerformance", "n/a" - ), - "SustainabilityPerformance": self.get_pset_value_from_object( - type, "COBie_Specification", "SustainabilityPerformance", "n/a" - ), - } - for field, key in self.custom_data["types"].items(): - self.types[type_name][field] = self.get_element_value(type, key) - - def get_components(self): - components = self.file.by_type("IfcElement") - for component in components: - if not self.is_object_a_component_asset(component): - self.logger.warning("A component which is not an asset was found for %s", component) - continue - component_name = self.get_object_name(component) - self.components[component_name] = { - "CreatedBy": self.get_email_from_history(component.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(component.OwnerHistory), - "TypeName": self.get_type_name_from_object(component), - "Space": self.get_space_name_from_component(component), - "Description": self.get_object_attribute(component, "Description", default="n/a"), - "ExtSystem": self.get_ext_system_from_history(component.OwnerHistory), - "ExtObject": self.get_ext_object(component), - "ExtIdentifier": component.GlobalId, - "SerialNumber": self.get_pset_value_from_object( - component, "Pset_ManufacturerOccurence", "SerialNumber", "n/a" - ), - "InstallationDate": self.get_pset_value_from_object( - component, "COBie_Component", "InstallationDate", self.default_date - ), - "WarrantyStartDate": self.get_pset_value_from_object( - component, "COBie_Component", "WarrantyStartDate", self.default_date - ), - "TagNumber": self.get_pset_value_from_object(component, "COBie_Component", "TagNumber", "n/a"), - "BarCode": self.get_pset_value_from_object(component, "Pset_ManufacturerOccurence", "BarCode", "n/a"), - "AssetIdentifier": self.get_pset_value_from_object( - component, "COBie_Component", "AssetIdentifier", "n/a" - ), - } - for field, key in self.custom_data["components"].items(): - self.components[component_name][field] = self.get_element_value(component, key) - - def get_systems(self): - systems = self.file.by_type("IfcSystem") - for system in systems: - system_name = self.get_object_name(system) - self.systems[system_name] = { - "CreatedBy": self.get_email_from_history(system.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(system.OwnerHistory), - "Category": self.get_category_from_object(system, "Category-Element"), - "ComponentNames": self.get_grouped_product_names_from_object(system, "IfcProduct"), - "ExtSystem": self.get_ext_system_from_history(system.OwnerHistory), - "ExtObject": self.get_ext_object(system), - "ExtIdentifier": system.GlobalId, - "Description": self.get_object_attribute(system, "Description", default="n/a"), - } - for field, key in self.custom_data["systems"].items(): - self.systems[system_name][field] = self.get_element_value(system, key) - - def get_assemblies(self): - assemblies = self.file.by_type("IfcRelAggregates") - for assembly in assemblies: - assembly_name = self.get_object_name(assembly) - if not self.is_object_a_component_asset(assembly.RelatingObject): - continue - self.assemblies[assembly_name] = { - "CreatedBy": self.get_email_from_history(assembly.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(assembly.OwnerHistory), - "SheetName": "Assembly", - "ParentName": self.get_object_name(assembly.RelatingObject), - "ChildNames": ",".join([o.Name if o.Name else "" for o in assembly.RelatedObjects]), - "AssemblyType": "n/a", # I don't understand this field - "ExtSystem": self.get_ext_system_from_history(assembly.OwnerHistory), - "ExtObject": self.get_ext_object(assembly), - "ExtIdentifier": assembly.GlobalId, - "Description": self.get_object_attribute(assembly, "Description", default="n/a"), - } - for field, key in self.custom_data["assemblies"].items(): - self.assemblies[assembly_name][field] = self.get_element_value(assembly, key) - - def get_connections(self): - connections = self.file.by_type("IfcRelConnects") - for connection in connections: - connection_name = self.get_object_name(connection) - self.connections[connection_name] = { - "CreatedBy": self.get_email_from_history(connection.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(connection.OwnerHistory), - # There is ambiguity for what the ConnectionType mapping should be - "ConnectionType": self.get_object_attribute(connection, "Description", default="n/a"), - "SheetName": "Connections", - "RowName1": self.get_row_name_from_connection(connection, "RelatingElement"), - "RowName2": self.get_row_name_from_connection(connection, "RelatedElement"), - "RealizingElement": self.get_port_name_from_connection(connection, "RealizingElement"), - "PortName1": self.get_port_name_from_connection(connection, "RelatingPort"), - "PortName2": self.get_port_name_from_connection(connection, "RelatedPort"), - "ExtSystem": self.get_ext_system_from_history(connection.OwnerHistory), - "ExtObject": self.get_ext_object(connection), - "ExtIdentifier": connection.GlobalId, - "Description": self.get_object_attribute(connection, "Description", default="n/a"), - } - for field, key in self.custom_data["connections"].items(): - self.connections[connection_name][field] = self.get_element_value(connection, key) - - def get_spares(self): - spares = self.file.by_type("IfcConstructionProductResource") - for spare in spares: - spare_name = self.get_object_name(spare) - self.spares[spare_name] = { - "CreatedBy": self.get_email_from_history(spare.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(spare.OwnerHistory), - "Category": self.get_category_from_object(spare, "SpareType"), - "TypeName": self.get_type_name_from_object(spare), - "Suppliers": self.get_contact_pset_value_from_object(spare, "COBie_Spare", "Suppliers"), - "ExtSystem": self.get_ext_system_from_history(spare.OwnerHistory), - "ExtObject": self.get_ext_object(spare), - "ExtIdentifier": spare.GlobalId, - "Description": self.get_object_attribute(spare, "Description", default="n/a"), - "SetNumber": self.get_contact_pset_value_from_object(spare, "COBie_Spare", "SetNumber"), - "PartNumber": self.get_contact_pset_value_from_object(spare, "COBie_Spare", "PartNumber"), - } - for field, key in self.custom_data["spares"].items(): - self.spares[spare_name][field] = self.get_element_value(spare, key) - - def get_resources(self): - resources = self.file.by_type("IfcConstructionProductResource") - for resource in resources: - resource_name = self.get_object_name(resource) - self.resources[resource_name] = { - "CreatedBy": self.get_email_from_history(resource.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(resource.OwnerHistory), - "Category": self.get_object_attribute(resource, "ObjectType", picklist="ResourceType", default="n/a"), - "ExtSystem": self.get_ext_system_from_history(resource.OwnerHistory), - "ExtObject": self.get_ext_object(resource), - "ExtIdentifier": resource.GlobalId, - "Description": self.get_object_attribute(resource, "Description", default="n/a"), - } - for field, key in self.custom_data["resources"].items(): - self.resources[resource_name][field] = self.get_element_value(resource, key) - - def get_jobs(self): - jobs = self.file.by_type("IfcTask") - for job in jobs: - job_name = self.get_object_name(job) - task_time = job.TaskTime - self.jobs[job_name] = { - "CreatedBy": self.get_email_from_history(job.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(job.OwnerHistory), - "Category": self.get_object_attribute(job, "ObjectType", picklist="JobType", default="n/a"), - "Status": self.get_object_attribute(job, "Status", picklist="JobStatusType", default="n/a"), - "TypeName": self.get_type_name_from_object(job), - "Description": self.get_object_attribute(job, "Description", default="n/a"), - "Duration": self.get_object_attribute(task_time, "ScheduleDuration", default=0), - "DurationUnit": "day", - "Start": self.get_object_attribute(task_time, "ScheduleStart", default=0), - "TaskStartUnit": "day", - "Frequency": self.get_object_attribute(task_time.Recurrence, "Occurrences", default=0) - if hasattr(task_time, "Recurrence") - else 0, - "FrequencyUnit": "day", - "ExtSystem": self.get_ext_system_from_history(job.OwnerHistory), - "ExtObject": self.get_ext_object(job), - "ExtIdentifier": job.GlobalId, - "TaskNumber": self.get_object_attribute(job, "Identification"), - "Priors": self.get_priors_from_job(job), - "ResourceNames": self.get_resource_names_from_job(job), - } - for field, key in self.custom_data["jobs"].items(): - self.jobs[job_name][field] = self.get_element_value(job, key) - - # Impacts is not explicitly defined as a mapping in the responsibliity - # matrix. This is my best guess. This data should not be relied upon until - # this is clarified. - def get_impacts(self): - impacts = self.file.by_type("IfcPropertySet") - for impact in impacts: - if impact.Name != "Pset_EnvironmentalImpactValues" or not impact.HasProperties: - continue - for property in impact.HasProperties: - property_name = "{}-{}".format(property.id(), self.get_object_name(property)) - self.impacts[property_name] = { - "CreatedBy": self.get_email_from_history(impact.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(impact.OwnerHistory), - "ImpactType": None, - "ImpactStage": None, - "SheetName": "Impacts", - "RowName": "n/a", - "Value": self.get_property_value(property), - "Unit": "{}{}".format(property.Unit.Prefix, property.Unit.Name) - if hasattr(property, "Unit") and property.Unit - else "n/a", - "LeadInTime": self.get_property_value(property, name="LeadInTime"), - "Duration": self.get_property_value(property, name="Duration"), - "LeadOutTime": self.get_property_value(property, name="LeadOutTime"), - "ExtSystem": self.get_ext_system_from_history(impact.OwnerHistory), - "ExtObject": self.get_ext_object(impact), - "ExtIdentifier": impact.GlobalId, - "Description": self.get_object_attribute(impact, "Description", default="n/a"), - } - for field, key in self.custom_data["impacts"].items(): - self.impacts[impact_name][field] = self.get_element_value(impact, key) - - def get_documents(self): - documents = self.file.by_type("IfcDocumentInformation") - for document in documents: - document_name = self.get_object_name(document) - self.documents[document_name] = { - "CreatedBy": self.get_email_from_history(document.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(document.OwnerHistory), - "Category": "n/a", # I am not sure what this mapping is meant to be - "ApprovalBy": self.get_object_attribute( - document, "IntendedUse", picklist="ApprovalBy", default="Information Only" - ), - "Stage": self.get_object_attribute(document, "Scope", picklist="StageType", default="Required"), - "SheetName": "Documents", - "RowName": "n/a", - "Directory": self.get_directory_from_document(document), - "File": self.get_file_from_document(document), - "ExtSystem": self.get_ext_system_from_history(document.OwnerHistory), - "ExtObject": self.get_ext_object(document), - "ExtIdentifier": document.GlobalId, - "Description": self.get_object_attribute(document, "Description", default=document_name), - "Reference": document_name, - } - for field, key in self.custom_data["documents"].items(): - self.documents[document_name][field] = self.get_element_value(document, key) - - # Attributes is not explicitly defined as a mapping in the responsibliity - # matrix. This is my best guess. This data should not be relied upon until - # this is clarified. - def get_attributes(self): - attributes = self.file.by_type("IfcPropertySet") - for attribute in attributes: - if not attribute.HasProperties: - continue - for property in attribute.HasProperties: - property_name = "{}-{}".format(property.id(), self.get_object_name(property)) - self.attributes[property_name] = { - "CreatedBy": self.get_email_from_history(attribute.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(attribute.OwnerHistory), - "Category": "n/a", # I am not sure what this mapping is meant to be - "SheetName": "Attributes", - "RowName": "n/a", # I am not sure what this mapping is meant to be - "Value": self.get_property_value(property), - "Unit": "{}{}".format( - property.Unit.Prefix if hasattr(property.Unit, "Prefix") else "", - property.Unit.Name if hasattr(property.Unit, "Name") else "n/a", - ) - if hasattr(property, "Unit") and property.Unit - else "n/a", - "ExtSystem": self.get_ext_system_from_history(attribute.OwnerHistory), - "ExtObject": self.get_ext_object(attribute), - "ExtIdentifier": attribute.GlobalId, - "Description": self.get_object_attribute(attribute, "Description", default="n/a"), - # I'm holding off implementing this until I understand a bit - # more about attributes - "AllowedValues": "n/a", - } - - def get_coordinates(self): - coordinates = ( - self.file.by_type("IfcBuildingStorey") + self.file.by_type("IfcSpace") + self.file.by_type("IfcProduct") - ) - for coordinate in coordinates: - coordinate_name = "{}/{}".format(coordinate.is_a(), self.get_object_name(coordinate)) - mat = ifcopenshell.util.placement.get_local_placement(coordinate.ObjectPlacement) - x, y, z = mat[:,3][:3] - self.coordinates[coordinate_name] = { - "CreatedBy": self.get_email_from_history(coordinate.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(coordinate.OwnerHistory), - "Category": "Location", # I am not sure what this mapping is meant to be - "SheetName": "Coordinates", - "RowName": "n/a", - "CoordinateXAxis": x, - "CoordinateYAxis": y, - "CoordinateZAxis": z, - "ExtSystem": self.get_ext_system_from_history(coordinate.OwnerHistory), - "ExtObject": self.get_ext_object(coordinate), - "ExtIdentifier": coordinate.GlobalId, - # Holding off implementing this, see Bug #688: - # https://github.com/IfcOpenShell/IfcOpenShell/issues/688 - "ClockwiseRotation": "n/a", # X axis - "ElevationalRotation": "n/a", # Y axis - "YawRotation": "n/a", # Z axis - } - - # I don't fully understand this worksheet. Don't trust this data. - def get_issues(self): - issues = self.file.by_type("IfcApproval") - for issue in issues: - issue_name = self.get_object_name(issue) - self.issues[issue_name] = { - "CreatedBy": self.get_email_from_history(issue.OwnerHistory), - "CreatedOn": self.get_created_on_from_history(issue.OwnerHistory), - "Type": "n/a", # How do we get to the Pset_Risk from the IfcApproval? - "Risk": "n/a", # How do we get to the Pset_Risk from the IfcApproval? - "Chance": "n/a", # How do we get to the Pset_Risk from the IfcApproval? - "Impact": "n/a", # How do we get to the Pset_Risk from the IfcApproval? - "SheetName1": "n/a", - "RowName1": "n/a", - "SheetName2": "n/a", - "RowName2": "n/a", - "Description": self.get_object_attribute(issue, "Description", default="n/a"), - "Owner": self.get_email_from_history(issue.RequestingApproval), # Is this correct? - "Mitigation": "n/a", - "ExtSystem": self.get_ext_system_from_history(issue.OwnerHistory), - "ExtObject": self.get_ext_object(issue), - "ExtIdentifier": issue.GlobalId, - } - - def get_directory_from_document(self, document): - if self.file.schema == "IFC2X3": - if document.HasDocumentReferences: - for reference in document.HasDocumentReferences: - return self.get_object_attribute(reference, "Location", default="n/a") - else: - return self.get_object_attribute(document, "Location", default="n/a") - - def get_file_from_document(self, document): - if self.file.schema == "IFC2X3": - if document.HasDocumentReferences: - for reference in document.HasDocumentReferences: - return self.get_object_attribute(reference, "Name", default="n/a") - else: - self.get_object_attribute(document, "Identification", default="n/a") - - def get_resource_names_from_job(self, job): - names = [] - if job.OperatesOn and job.OperatesOn.RelatedObjects: - for object in job.OperatesOn.RelatedObjects: - names.append(object.Name) - return ",".join(names) - - def get_priors_from_job(self, job): - # The responsibility matrix is vague as to whether it expects a task - # name or a task identification. I chose task name. - if ( - job.IsSuccessorFrom - and job.IsSuccessorFrom.RelatingProcess - and job.IsSuccessorFrom.RelatingProcess.is_a("IfcTask") - ): - return self.get_object_name(job.IsSuccessorFrom.RelatingProcess) - - def get_row_name_from_connection(self, connection, key): - if not connection.is_a("IfcRelConnectsElements"): - return None - object = getattr(connection, key) - if self.is_object_a_component_asset(object): - return object.Name - self.logger.error("The connected object relationship %s is not a component asset for %s", key, connection) - - def get_port_name_from_connection(self, connection, key): - if not connection.is_a("IfcRelConnectsPorts"): - return None - object = getattr(connection, key) - if object and self.is_object_a_component_asset(object): - return object.Name - self.logger.error("The connected object relationship %s is not a component asset for %s", key, connection) - - def is_object_a_component_asset(self, obj): - return obj in self.component_assets - - def get_space_name_from_component(self, component): - for relationship in component.ContainedInStructure: - if relationship.RelatingStructure.is_a("IfcSpace") and relationship.RelatingStructure.Name: - return relationship.RelatingStructure.Name - self.logger.error("A related space name could not be determined for %s", component) - - def get_type_name_from_object(self, object): - if self.file.schema == "IFC2X3": - for relationship in object.IsDefinedBy: - if relationship.is_a("IfcRelDefinesByType") and relationship.RelatingType.Name: - return relationship.RelatingType.Name - else: - for relationship in object.IsTypedBy: - if relationship.RelatingType.Name: - return relationship.RelatingType.Name - self.logger.error("A related type name could not be determined for %s", object) - - def get_expected_life_from_type(self, type): - if self.file.schema == "IFC2X3": - return self.get_pset_value_from_object(type, "COBie_ServiceLife", "ServiceLifeDuration", "n/a") - return self.get_pset_value_from_object(type, "Pset_ServiceLife", "ServiceLifeDuration", "n/a") - - def get_contact_pset_value_from_object(self, object, pset_name, property_name): - result = self.get_pset_value_from_object(object, pset_name, property_name) - if not result: - self.logger.error("No property %s in %s was found for %s", property_name, pset_name, object) - if result not in self.contacts: - self.logger.error("A coresponding %s contact in %s was not found for %s", property_name, pset_name, object) - return result - - def get_pset_value_from_object(self, object, pset_name, property_name, default=None, picklist=None): - pset = self.get_pset_from_object(object, pset_name) - if not pset: - if picklist: - self.picklists[picklist].append(default) - return default - prop = self.get_property_from_pset(pset, property_name, default) - if picklist: - self.picklists[picklist].append(prop) - return prop - - def get_grouped_product_names_from_object(self, object, type): - names = [] - for relationship in object.IsGroupedBy: - for related_object in relationship.RelatedObjects: - if related_object.is_a(type): - names.append(related_object.Name) - if names: - return ",".join(names) - self.logger.error("No related %s were found for %s", type, object) - - def get_net_area_from_space(self, space): - qto = self.get_qto_from_object(space, "Qto_SpaceBaseQuantities") - if not qto: - return "n/a" - return self.get_property_from_qto(qto, "NetFloorArea", "AreaValue") - - def get_gross_area_from_space(self, space): - qto = self.get_qto_from_object(space, "Qto_SpaceBaseQuantities") - if not qto: - return "n/a" - return self.get_property_from_qto(qto, "GrossFloorArea", "AreaValue") - - def get_usable_height_from_space(self, space): - qto = self.get_qto_from_object(space, "Qto_SpaceBaseQuantities") - if not qto: - return "n/a" - return self.get_property_from_qto(qto, "FinishCeilingHeight", "LengthValue") - - def get_qto_from_object(self, object, name): - for relationship in object.IsDefinedBy: - if ( - relationship.is_a("IfcRelDefinesByProperties") - and relationship.RelatingPropertyDefinition.is_a("IfcQuantitySet") - and relationship.RelatingPropertyDefinition.Name == name - ): - return relationship.RelatingPropertyDefinition - self.logger.warning("The qto %s was not found for %s", name, object) - - def get_property_from_qto(self, qto, name, attribute): - for property in qto.Quantities: - if property.Name == name: - return getattr(property, attribute) - self.logger.warning("The quantity value %s was not found for %s", name, qto) - return "n/a" - - def get_property_from_pset(self, pset, name, default=None): - for prop in pset.HasProperties: - if prop.Name == name: - return prop.NominalValue.wrappedValue - self.logger.warning("The property %s was not found for %s", name, pset) - return default - - def get_property_value(self, prop, name=None): - if not prop.is_a("IfcPropertySingleValue"): - return "n/a" - if name is not None and prop.Name != name: - return "n/a" - value = self.get_object_attribute(prop, "NominalValue", default=None) - if value: - return value.wrappedValue - return "n/a" - - def get_pset_from_object(self, object, name): - if object.is_a("IfcTypeObject"): - if object.HasPropertySets: - for pset in object.HasPropertySets: - if pset.is_a("IfcPropertySet") and pset.Name == name: - return pset - else: - for relationship in object.IsDefinedBy: - if ( - relationship.is_a("IfcRelDefinesByProperties") - and relationship.RelatingPropertyDefinition.is_a("IfcPropertySet") - and relationship.RelatingPropertyDefinition.Name == name - ): - return relationship.RelatingPropertyDefinition - self.logger.warning("The pset %s was not found for %s", name, object) - - def get_height_from_storey(self, storey): - for relationship in storey.IsDefinedBy: - if not relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity"): - continue - for quantity in relationship.RelatingPropertyDefinition.Quantities: - if quantity.is_a("IfcQuantityLength") and quantity.LengthValue: - return quantity.LengthValue - self.logger.warning("A height length value was not found for %s", storey) - return "n/a" - - def get_created_on_from_history(self, history): - if history.CreationDate: - return datetime.datetime.fromtimestamp(history.CreationDate).isoformat() - self.logger.warning("A created on date was not found for %s", history) - return self.default_date - - def get_object_attribute(self, object, attribute, is_primary_key=False, picklist=None, default=None): - result = getattr(object, attribute) - if result: - if picklist: - self.picklists[picklist].append(result) - return result - if is_primary_key: - self.logger.error("The primary key attribute %s was not found for %s", attribute, object) - else: - self.logger.warning("The attribute %s was not found for %s", attribute, object) - return default - - def get_ext_project_object(self): - self.picklists["objType"].append("IfcProject") - return "IfcProject" - - def get_ext_site_object(self): - self.picklists["objType"].append("IfcSite") - return "IfcSite" - - def get_ext_object(self, object): - self.picklists["objType"].append(object.is_a()) - return object.is_a() - - def get_ext_system_from_history(self, history): - return history.OwningApplication.ApplicationFullName - - def get_object_name(self, object): - if not object.Name: - self.logger.error("A primary key name was not found for %s", object) - return "Object{}".format(object.id()) - return object.Name - - def get_area_measurement_from_building(self, building): - for relationship in building.IsDefinedBy: - if ( - relationship.RelatingPropertyDefinition.is_a("IfcElementQuantity") - and relationship.RelatingPropertyDefinition.MethodOfMeasurement - ): - return relationship.RelatingPropertyDefinition.MethodOfMeasurement - self.logger.warning("A method of measurement was not defined for %s", building) - - def get_unit_type_from_units(self, units, type): - for unit in units: - if unit.UnitType == type: - if unit.is_a("IfcSIUnit") and unit.Prefix: - return "{}{}".format(unit.Prefix, unit.Name) - return unit.Name - self.logger.error("A unit %s was not defined in this project for %s", type, units) - - def get_monetary_unit_from_units(self, units): - for unit in units: - if unit.is_a("IfcMonetaryUnit"): - return unit.Currency - self.logger.error("A monetary unit could not be found for %s", units) - - def get_project_globalid_from_building(self, building): - return self.get_parent_spatial_element(building, "IfcProject").GlobalId - - def get_site_globalid_from_building(self, building): - return self.get_parent_spatial_element(building, "IfcSite").GlobalId - - def get_project_name_from_building(self, building): - project = self.get_parent_spatial_element(building, "IfcProject") - if project.Name: - return project.Name - self.logger.error("The project name is empty for %s", project) - return "n/a" - - def get_site_name_from_building(self, building): - site = self.get_parent_spatial_element(building, "IfcSite") - if site.Name: - return site.Name - self.logger.error("The site name is empty for %s", site) - return "n/a" - - def get_units_from_building(self, building): - return self.get_parent_spatial_element(building, "IfcProject").UnitsInContext.Units - - def get_parent_spatial_element(self, child, name): - for relationship in child.Decomposes: - if relationship.RelatingObject.is_a(name): - return relationship.RelatingObject - return self.get_parent_spatial_element(relationship.RelatingObject, name) - return None - - def get_category_from_object(self, object, picklist): - class_identification = None - class_name = None - for association in object.HasAssociations: - if not association.is_a("IfcRelAssociatesClassification"): - continue - if not association.RelatingClassification.is_a("IfcClassificationReference"): - continue - if self.file.schema == "IFC2X3": - class_identification = association.RelatingClassification.ItemReference - else: - class_identification = association.RelatingClassification.Identification - class_name = association.RelatingClassification.Name - break - if not class_identification or class_name: - self.logger.error("The classification has invalid identification and name for %s", object) - result = "{}:{}".format(class_identification, class_name) - self.picklists[picklist].append(result) - return result - # The responsibility matrix lists a fallback, but it is a very - # cumbersome check, and so it is not implemented here. - - def get_name_from_person(self, person, attribute): - name = getattr(person, attribute) - if not name or not name.isalpha(): - self.logger.warning('The person\'s %s seems to be badly formatted ("%s") for %s', attribute, name, person) - return name if name else "n/a" - - def get_lines_from_address(self, address): - result = self.get_attribute_from_address(address, "AddressLines") - if isinstance(result, tuple): - return ", ".join(result) - return result - - def get_attribute_from_address(self, address, attribute): - result = getattr(address, attribute) - if not result: - self.logger.warning("The address %s seems to not exist for %s", attribute, address) - return "n/a" - return result - - def get_email_from_history(self, history): - person = history.OwningUser.ThePerson - organisation = history.OwningUser.TheOrganization - email = self.get_email_from_person_or_organisation(person) - if email: - return email - - email = self.get_email_from_person_or_organisation(organisation) - if email: - return email - - given_name = person.GivenName if person.GivenName else "unknown" - family_name = person.FamilyName if person.FamilyName else "unknown" - organisation_name = organisation.Name if organisation.Name else "unknown" - - if given_name == "unknown" and family_name == "unknown" and organisation_name == "unknown": - self.logger.error("No primary key could be determined from %s", history) - - return "{}{}@{}".format(given_name, family_name, organisation_name) - - def get_postal_address_from_history(self, history): - for address in history.OwningUser.ThePerson.Addresses or []: - if address.is_a("IfcPostalAddress"): - return address - for address in history.OwningUser.TheOrganization.Addresses or []: - if address.is_a("IfcPostalAddress"): - return address - return self.file.createIfcPostalAddress() - - def get_category_from_history(self, history): - roles = [] - both = history.OwningUser - person = both.ThePerson - organisation = both.TheOrganization - both_roles = list(both.Roles) if both.Roles else [] - person_roles = list(person.Roles) if person.Roles else [] - organisation_roles = list(organisation.Roles) if organisation.Roles else [] - for role in both_roles + person_roles + organisation_roles: - roles.append(self.get_role(role)) - result = ",".join(set(roles)) - if not result: - self.logger.error("No roles could be found for %s", history) - return - self.picklists["Category-Role"].append(result) - return result - - def get_phone_from_history(self, history): - person = history.OwningUser.ThePerson - organisation = history.OwningUser.TheOrganization - phone = self.get_phone_from_person_or_organisation(person) - if phone: - return phone - phone = self.get_phone_from_person_or_organisation(organisation) - if phone: - return phone - return "n/a" - - def get_ext_object_from_history(self, history): - result = history.OwningUser.is_a() - self.picklists["objType"].append(result) - return result - - def get_department_from_history(self, history): - organisation = history.OwningUser.TheOrganization - department = self.get_internal_location_from_organisation(organisation) - if department: - return department - last_department = None - for relationship in organisation.Relates: - for related_organisation in relationship.RelatedOrganizations: - department = self.get_internal_location_from_organisation(related_organisation) - if department: - last_department = department - if last_department: - return last_department - return history.OwningUser.TheOrganization.Name or "n/a" - - def get_internal_location_from_organisation(self, organisation): - for address in organisation.Addresses or []: - if address.is_a("IfcPostalAddress"): - return address.InternalLocation - self.logger.warning("An internal location was not found for %s", organisation) - - def get_role(self, role): - if role.Role == "USERDEFINED": - return role.UserDefinedRole - return role.Role - - def get_phone_from_person_or_organisation(self, person_or_org): - for address in person_or_org.Addresses or []: - if address.is_a("IfcTelecomAddress"): - return address.TelephoneNumbers[0] - self.logger.warning("A phone was not found for {}", person_or_org) - - def get_email_from_person_or_organisation(self, person_or_org): - for address in person_or_org.Addresses or []: - if address.is_a("IfcTelecomAddress"): - return address.ElectronicMailAddresses[0] - self.logger.warning("An email address was not found for {}", person_or_org) - - def get_element_value(self, element, key): - value = self.selector.get_element_value(element, key) - if hasattr(value, "wrappedValue"): - return value.wrappedValue - return value - - -class CobieWriter: - def __init__(self, parser, filename=None): - self.filename = filename - self.parser = parser - self.sheets = [] - self.sheet_data = {} - self.colours = { - "r": "fdff8e", # Required - "i": "fdcd94", # Internal reference - "e": "cd95ff", # External reference - "o": "cdffc8", # Optional - "s": "c0c0c0", # Secondary information - "p": "9ccaff", # Project specific - "n": "000000", # Not used - } - - def write(self): - self.sheets = [ - "Contact", - "Facility", - "Floor", - "Space", - "Zone", - "Type", - "Component", - "System", - "Assembly", - "Connection", - "Spare", - "Resource", - "Job", - "Impact", - "Document", - "Attribute", - "Coordinate", - "Issue", - ] - self.write_data( - "Contact", - self.parser.contacts, - "Email", - [ - "Email", - "CreatedBy", - "CreatedOn", - "Category", - "Company", - "Phone", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Department", - "OrganizationCode", - "GivenName", - "FamilyName", - "Street", - "PostalBox", - "Town", - "StateRegion", - "PostalCode", - "Country", - ], - "ririrreeeoooooooooo", - self.parser.custom_data["contacts"], - ) - self.write_data( - "Facility", - self.parser.facilities, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "ProjectName", - "SiteName", - "LinearUnits", - "AreaUnits", - "VolumeUnits", - "CostUnit", - "AreaMeasurement", - "ExternalSystem", - "ExternalProjectObject", - "ExternalProjectIdentifier", - "ExternalSiteObject", - "ExternalSiteIdentifier", - "ExternalFacilityObject", - "ExternalFacilityIdentifier", - "Description", - "ProjectDescription", - "SiteDescription", - "Phase", - ], - "ririrriiiireeeeeeeoooo", - self.parser.custom_data["facilities"], - ) - self.write_data( - "Floor", - self.parser.floors, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - "Elevation", - "Height", - ], - "ririeeeooo", - self.parser.custom_data["floors"], - ) - self.write_data( - "Space", - self.parser.spaces, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "FloorName", - "Description", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "RoomTag", - "UsableHeight", - "GrossArea", - "NetArea", - ], - "ririireeeoooo", - self.parser.custom_data["spaces"], - ) - self.write_data( - "Zone", - self.parser.zones, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "SpaceNames", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - ], - "ririieeeo", - self.parser.custom_data["zones"], - ) - self.write_data( - "Type", - self.parser.types, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "Description", - "AssetType", - "Manufacturer", - "ModelNumber", - "WarrantyGuarantorParts", - "WarrantyDurationParts", - "WarrantyGuarantorLabor", - "WarrantyDurationLabor", - "WarrantyDurationUnit", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "ReplacementCost", - "ExpectedLife", - "DurationUnit", - "NominalLength", - "NominalWidth", - "NominalHeight", - "ModelReference", - "Shape", - "Size", - "Color", - "Finish", - "Grade", - "Material", - "Constituents", - "Features", - "AccessibilityPerformance", - "CodePerformance", - "SustainabilityPerformance", - ], - "riririoooooooeeeooooooooooooooooooo", - self.parser.custom_data["types"], - ) - self.write_data( - "Component", - self.parser.components, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "TypeName", - "Space", - "Description", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "SerialNumber", - "InstallationDate", - "WarrantyStartDate", - "TagNumber", - "BarCode", - "AssetIdentifier", - ], - "ririireeeoooooo", - self.parser.custom_data["components"], - ) - self.write_data( - "System", - self.parser.systems, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "ComponentNames", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - ], - "ririieeeo", - self.parser.custom_data["systems"], - ) - self.write_data( - "Assembly", - self.parser.assemblies, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "SheetName", - "ParentName", - "ChildNames", - "AssemblyType", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - ], - "rirrrrreeeo", - self.parser.custom_data["assemblies"], - ) - self.write_data( - "Connection", - self.parser.connections, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "ConnectionType", - "SheetName", - "RowName1", - "RowName2", - "RealizingElement", - "PortName1", - "PortName2", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - ], - "ririiiiiiieeeo", - self.parser.custom_data["connections"], - ) - self.write_data( - "Spare", - self.parser.spares, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "TypeName", - "Suppliers", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - "SetNumber", - "PartNumber", - ], - "ririiieeeooo", - self.parser.custom_data["spares"], - ) - self.write_data( - "Resource", - self.parser.resources, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - ], - "ririeeeo", - self.parser.custom_data["resources"], - ) - self.write_data( - "Job", - self.parser.jobs, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "Status", - "TypeName", - "Description", - "Duration", - "DurationUnit", - "Start", - "TaskStartUnit", - "Frequency", - "FrequencyUnit", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "TaskNumber", - "Priors", - "ResourceNames", - ], - "ririiirriririeeeoii", - self.parser.custom_data["jobs"], - ) - self.write_data( - "Impact", - self.parser.impacts, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "ImpactType", - "ImpactStage", - "SheetName", - "RowName", - "Value", - "Unit", - "LeadInTime", - "Duration", - "LeadOutTime", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - ], - "ririiiirioooeeeo", - self.parser.custom_data["impacts"], - ) - self.write_data( - "Document", - self.parser.documents, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "ApprovalBy", - "Stage", - "SheetName", - "RowName", - "Directory", - "File", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - "Reference", - ], - "ririiiiirreeeoo", - self.parser.custom_data["documents"], - ) - self.write_data( - "Attribute", - self.parser.attributes, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "SheetName", - "RowName", - "Value", - "Unit", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "Description", - "AllowedValues", - ], - "ririiirreeeoo", - ) - self.write_data( - "Coordinate", - self.parser.coordinates, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Category", - "SheetName", - "RowName", - "CoordinateXAxis", - "CoordinateYAxis", - "CoordinateZAxis", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - "ClockwiseRotation", - "ElevationalRotation", - "YawRotation", - ], - "ririiooooeeeooo", - ) - self.write_data( - "Issue", - self.parser.issues, - "Name", - [ - "Name", - "CreatedBy", - "CreatedOn", - "Type", - "Risk", - "Chance", - "Impact", - "SheetName1", - "RowName1", - "SheetName2", - "RowName2", - "Description", - "Owner", - "Mitigation", - "ExtSystem", - "ExtObject", - "ExtIdentifier", - ], - "ririooooooooooeee", - ) - - def write_data(self, sheet, data, primary_key, fieldnames, colours, custom_data={}): - self.sheet_data[sheet] = {"headers": fieldnames + list(custom_data.keys()), "colours": colours, "rows": []} - for name, row in data.items(): - row[primary_key] = name - values = [] - for fieldname in fieldnames: - values.append(row[fieldname]) - for fieldname in custom_data.keys(): - values.append(row[fieldname]) - self.sheet_data[sheet]["rows"].append(values) - - -class CobieCsvWriter(CobieWriter): - def write(self): - import csv - - super().write() - for sheet, data in self.sheet_data.items(): - with open(os.path.join(self.filename, "{}.csv".format(sheet)), "w", newline="", encoding="utf-8") as file: - writer = csv.writer(file) - writer.writerow(data["headers"]) - for row in data["rows"]: - writer.writerow(row) - - -class CobieXlsWriter(CobieWriter): - def write(self): - from xlsxwriter import Workbook - - super().write() - self.workbook = Workbook(self.filename + ".xlsx") - - self.cell_formats = {} - for key, value in self.colours.items(): - self.cell_formats[key] = self.workbook.add_format() - self.cell_formats[key].set_bg_color(value) - - for sheet in self.sheets: - self.write_worksheet(sheet) - self.workbook.close() - - def write_worksheet(self, name): - worksheet = self.workbook.add_worksheet(name) - r = 0 - c = 0 - for header in self.sheet_data[name]["headers"]: - cell = worksheet.write(r, c, header, self.cell_formats["s"]) - c += 1 - c = 0 - r += 1 - for row in self.sheet_data[name]["rows"]: - c = 0 - for col in row: - if c >= len(self.sheet_data[name]["colours"]): - cell_format = "p" - else: - cell_format = self.sheet_data[name]["colours"][c] - cell = worksheet.write(r, c, col, self.cell_formats[cell_format]) - c += 1 - r += 1 - - -class CobieOdsWriter(CobieWriter): - def write(self): - from odf.opendocument import OpenDocumentSpreadsheet - from odf.style import Style, TableCellProperties - - super().write() - self.doc = OpenDocumentSpreadsheet() - - self.cell_formats = {} - for key, value in self.colours.items(): - style = Style(name=key, family="table-cell") - style.addElement(TableCellProperties(backgroundcolor="#" + value)) - self.doc.automaticstyles.addElement(style) - self.cell_formats[key] = style - - for sheet in self.sheets: - self.write_table(sheet) - self.doc.save(self.filename, True) - - def write_table(self, name): - from odf.table import Table, TableRow, TableCell - from odf.text import P - - table = Table(name=name) - tr = TableRow() - for header in self.sheet_data[name]["headers"]: - tc = TableCell(valuetype="string", stylename="s") - tc.addElement(P(text=header)) - tr.addElement(tc) - table.addElement(tr) - for row in self.sheet_data[name]["rows"]: - tr = TableRow() - c = 0 - for col in row: - if c >= len(self.sheet_data[name]["colours"]): - cell_format = "p" - else: - cell_format = self.sheet_data[name]["colours"][c] - tc = TableCell(valuetype="string", stylename=cell_format) - tc.addElement(P(text=col)) - tr.addElement(tc) - c += 1 - table.addElement(tr) - self.doc.spreadsheet.addElement(table) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Converts COBie IFC MVD into its spreadsheet equivalent") - parser.add_argument("input", type=str, help="Specify an IFC file to process") - parser.add_argument("output", type=str, help="The output directory for CSV or filename for other formats") - parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log") - parser.add_argument( - "-f", "--format", type=str, help="Choose which format to export in (csv/ods/xlsx)", default="csv" - ) - parser.add_argument( - "-c", "--components", type=str, help="A custom selector for components. Defaults to COBie", default=".COBie" - ) - parser.add_argument( - "-t", "--types", type=str, help="A custom selector for types. Defaults to COBieType", default=".COBieType" - ) - parser.add_argument( - "-d", - "--data", - type=str, - help="JSON file containing custom data to be appended to the COBie spreadsheet template", - default="", - ) - args = vars(parser.parse_args()) - - print("Processing IFC file ...") - - start = time.time() - logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG) - logger = logging.getLogger("IFCtoCOBie") - logger.info("Starting conversion") - selector = ifcopenshell.util.selector.Selector() - parser = IfcCobieParser(logger, selector) - if args["data"]: - with open(bpy.context.scene.BIMProperties.cobie_json_file, "r") as f: - custom_data = json.load(f) - else: - custom_data = {} - parser.parse(args["input"], args["types"], args["components"], custom_data) - - print("Generating reports ...") - - if args["format"] == "xlsx": - writer = CobieXlsWriter(parser, args["output"]) - elif args["format"] == "ods": - writer = CobieOdsWriter(parser, args["output"]) - else: - writer = CobieCsvWriter(parser, args["output"]) - writer.write() - - logger.info("Finished conversion in %ss", time.time() - start) - print("# All reports are complete :-)") diff --git a/src/ifccobie/get_maintainable_assets.py b/src/ifccobie/get_maintainable_assets.py deleted file mode 100644 index 591a080088..0000000000 --- a/src/ifccobie/get_maintainable_assets.py +++ /dev/null @@ -1,79 +0,0 @@ - -# IfcCOBie - Extract COBie data from IFC to spreadsheets -# Copyright (C) 2019, 2020, 2021 Dion Moult -# -# This file is part of IfcCOBie. -# -# IfcCOBie is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# IfcCOBie is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with IfcCOBie. If not, see . - -import json -import ifcopenshell -import ifcopenshell.util.selector - -with open("../blenderbim/blenderbim/bim/schema/entity_descriptions.json") as f: - entity_descriptions = json.load(f) -with open("../blenderbim/blenderbim/bim/schema/enum_descriptions.json") as f: - enum_descriptions = json.load(f) - -print('{|class="wikitable"') -print("! IFC Class") -print("! Predefined Type") - - -def print_entity(entity): - print("|-") - print("| " + entity.name()) - print("| ") - if entity.name() in entity_descriptions: - print( - "{} ... [https://standards.buildingsmart.org/IFC/DEV/IFC4_3/RC1/HTML/link/{}.htm read more]".format( - entity_descriptions[entity.name()], entity.name().lower() - ) - ) - else: - print( - "No description provided ... [https://standards.buildingsmart.org/IFC/DEV/IFC4_3/RC1/HTML/link/{}.htm read more]".format( - entity_descriptions[entity.name()], entity.name().lower() - ) - ) - for attribute in entity.attributes(): - if attribute.name() == "PredefinedType": - enum = attribute.type_of_attribute().declared_type() - print("\nThe following predefined types are defined:\n") - for item in enum.enumeration_items(): - # print('|-') - # print('| ' + entity.name()) - # print('| ' + item) - if enum.name() in enum_descriptions and item in enum_descriptions[enum.name()]: - print( - "* '''{}''' - {} ... [https://standards.buildingsmart.org/IFC/DEV/IFC4_3/RC1/HTML/link/{}.htm read more]".format( - item, enum_descriptions[enum.name()][item], enum.name().lower() - ) - ) - else: - print( - "* '''{}''' - No description provided ... [https://standards.buildingsmart.org/IFC/DEV/IFC4_3/RC1/HTML/link/{}.htm read more]".format( - item, enum.name().lower() - ) - ) - - for subtype in entity.subtypes(): - print_entity(subtype) - - -for asset in ifcopenshell.util.selector.cobie_component_assets: - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name("IFC4") - print_entity(schema.declaration_by_name(asset)) - -print("|}") diff --git a/src/ifccobie/icon.ico b/src/ifccobie/icon.ico deleted file mode 100644 index b9eee9acb9..0000000000 Binary files a/src/ifccobie/icon.ico and /dev/null differ diff --git a/src/ifcfm/ifcfm/__init__.py b/src/ifcfm/ifcfm/__init__.py index 8eaefb279b..5b81f02367 100644 --- a/src/ifcfm/ifcfm/__init__.py +++ b/src/ifcfm/ifcfm/__init__.py @@ -41,27 +41,21 @@ class Parser: self.file = None self.preset = preset self.categories = {} - self.get_category_elements = {} - self.get_element_data = {} + self.config = None self.get_custom_element_data = {} self.duplicate_keys = [] if isinstance(preset, str): module = importlib.import_module(f"ifcfm.{preset}") - self.get_category_elements = getattr(module, "get_category_elements") - self.get_element_data = getattr(module, "get_element_data") - elif isinstance(preset, dict): - self.get_category_elements = preset["get_category_elements"] - self.get_element_data = preset["get_element_data"] + self.config = getattr(module, "config") else: - self.get_category_elements = getattr(preset, "get_category_elements") - self.get_element_data = getattr(preset, "get_element_data") + self.config = preset def parse(self, ifc_file): - for category_name, get_category_elements in self.get_category_elements.items(): + for category_name, category_config in self.config["categories"].items(): self.categories.setdefault(category_name, {}) - for element in get_category_elements(ifc_file): - get_element_data = self.get_element_data[category_name] + for element in category_config["get_category_elements"](ifc_file): + get_element_data = category_config["get_element_data"] if isinstance(get_element_data, dict): data = {} @@ -71,7 +65,7 @@ class Parser: data = get_element_data(ifc_file, element) or {} get_custom_element_data = self.get_custom_element_data.get(category_name, lambda x, y: None) - if isinstance(get_element_data, dict): + if isinstance(get_custom_element_data, dict): custom_data = {} for key, query in get_custom_element_data.items(): custom_data[key] = ifcopenshell.util.selector.get_element_value(element, query) @@ -87,6 +81,15 @@ class Parser: self.duplicate_keys.append((self.categories[category_name][key], data)) self.categories[category_name][key] = data + def exclude_categories(self, names): + for name in names: + if name in self.config["categories"]: + del self.config["categories"][name] + + def exclude_element_data(self, category, names): + headers = self.config["categories"][category]["headers"] + self.config["categories"][category]["headers"] = [h for h in headers if h not in names] + class Writer: def __init__(self, parser): @@ -105,8 +108,9 @@ class Writer: empty = self.config.get("empty", empty) bool_true = self.config.get("bool_true", bool_true) bool_false = self.config.get("bool_false", bool_false) - for category, data in self.parser.categories.items(): - headers = self.config.get("categories", {}).get(category, {}).get("headers", []) + for category, config in self.config["categories"].items(): + data = self.parser.categories.get(category, None) + headers = config["headers"] if not data: self.categories[category] = {"headers": headers, "rows": []} diff --git a/src/ifcfm/ifcfm/basic.py b/src/ifcfm/ifcfm/basic.py index 5e3d81d3dd..f4ccb81ffd 100644 --- a/src/ifcfm/ifcfm/basic.py +++ b/src/ifcfm/ifcfm/basic.py @@ -228,26 +228,6 @@ def get_property(psets, pset_name, prop_name, decimals=None): return round(result, decimals) -get_category_elements = { - "Facilities": get_facilities, - "Storeys": get_storeys, - "Spaces": get_spaces, - "Zones": get_zones, - "ElementTypes": get_element_types, - "Elements": get_elements, - "Systems": get_systems, -} - -get_element_data = { - "Facilities": get_facility_data, - "Storeys": get_storey_data, - "Spaces": get_space_data, - "Zones": get_zone_data, - "ElementTypes": get_element_type_data, - "Elements": get_element_data, - "Systems": get_system_data, -} - config = { "colours": { "h": "dddddd", # Header data @@ -278,6 +258,8 @@ config = { ], "colours": "ppppreeeeesss", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_facilities, + "get_element_data": get_facility_data, }, "Storeys": { "headers": [ @@ -292,6 +274,8 @@ config = { ], "colours": "ppreeees", "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_storeys, + "get_element_data": get_storey_data, }, "Spaces": { "headers": [ @@ -308,11 +292,15 @@ config = { ], "colours": "ppprreeess", "sort": [{"name": "LevelName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_spaces, + "get_element_data": get_space_data, }, "Zones": { "headers": ["Name", "SpaceName", "AuthorOrganizationName", "AuthorDate", "ModelSoftware", "ModelID"], "colours": "prreee", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_zones, + "get_element_data": get_zone_data, }, "ElementTypes": { "headers": [ @@ -333,6 +321,8 @@ config = { ], "colours": "pppreeeeesssss", "sort": [{"name": "ModelObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_element_types, + "get_element_data": get_element_type_data, }, "Elements": { "headers": [ @@ -356,6 +346,8 @@ config = { ], "colours": "prrrreeeeesssssss", "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_elements, + "get_element_data": get_element_data, }, "Systems": { "headers": [ @@ -369,6 +361,8 @@ config = { ], "colours": "pppreee", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_systems, + "get_element_data": get_system_data, }, }, } diff --git a/src/ifcfm/ifcfm/cobie.py b/src/ifcfm/ifcfm/cobie.py index 9eb6eb0d7c..6f385f1278 100644 --- a/src/ifcfm/ifcfm/cobie.py +++ b/src/ifcfm/ifcfm/cobie.py @@ -30,6 +30,8 @@ import ifcopenshell.util.classification # https://github.com/opensourceBIM/COBie-plugins/tree/master/COBieShared/src/org/bimserver/cobie/shared/serialization/util # Some settings are also defined here: # https://github.com/opensourceBIM/COBie-plugins/blob/master/COBiePlugins/lib/IfcToCobieConfig.xml +# Note that the following categories are not implemented in the BIMServer COBie-Plugins: +# Impact, Coordinate, Issue, Picklist def get_contacts(ifc_file): @@ -494,16 +496,16 @@ def get_type_data(ifc_file, element): pset_metadata = {} pset_mapping = { "manufacturer": {"Manufacturer"}, - "model_number": {"ModelNumber", "ArticleNumber", "ModelLabel"}, + "model_number": {"ModelNumber", "ArticleNumber", "ModelReference"}, "warranty_guarantor_parts": {"WarrantyGuarantorParts", "PointOfContact"}, "warranty_guarantor_labor": {"WarrantyGuarantorLabor", "PointOfContact"}, "warranty_description": {"WarrantyDescription", "WarrantyIdentifier"}, "replacement_cost": {"ReplacementCost", "Replacement Cost", "Replacement", "Cost"}, - "nominal_length": {"NominalLength", "OverallLength"}, - "nominal_width": {"NominalWidth", "Width"}, + "nominal_length": {"NominalLength", "OverallLength", "Length"}, + "nominal_width": {"NominalWidth", "OverallWidth", "Width"}, # https://github.com/opensourceBIM/COBie-plugins/blob/master/COBiePlugins/lib/IfcToCobieConfig.xml#L104 "nominal_height": {"NominalHeight", "Height"}, # Original has a typo "Heght" - "model_reference": {"ModelReference", "Reference"}, + "model_reference": {"ModelLabel"}, # I believe this is what the intention was, not "ModelReference". "shape": {"Shape"}, "size": {"Size"}, "color": {"Color", "Colour"}, @@ -1139,52 +1141,6 @@ def get_sheet_name(element): return "Resource" -get_category_elements = { - "Contact": get_contacts, - "Facility": get_facilities, - "Floor": get_floors, - "Space": get_spaces, - "Zone": get_zones, - "Type": get_types, - "Component": get_components, - "System": get_systems, - "Assembly": get_assemblies, - "Connection": get_connections, - "Spare": get_spares, - "Resource": get_resources, - "Job": get_jobs, - # "Impact": get_impacts, # Not implemented for some reason in BIMServer COBie-Plugins - "Document": get_documents, - "Attribute": get_attributes, - # Not implemented for some reason in BIMServer COBie-Plugins - # "Coordinate": get_coordinates, - # "Issue": get_issues, - # "Picklist": get_picklists, -} - -get_element_data = { - "Contact": get_contact_data, - "Facility": get_facility_data, - "Floor": get_floor_data, - "Space": get_space_data, - "Zone": get_zone_data, - "Type": get_type_data, - "Component": get_component_data, - "System": get_system_data, - "Assembly": get_assembly_data, - "Connection": get_connection_data, - "Spare": get_spare_data, - "Resource": get_resource_data, - "Job": get_job_data, - # "Impact": get_impact_data, - "Document": get_document_data, - "Attribute": get_attribute_data, - # "Coordinate": get_coordinate_data, - # "Issue": get_issue_data, - # "Picklist": get_picklist_data, -} - - config = { "colours": { "h": "c0c0c0", # Header data @@ -1225,6 +1181,8 @@ config = { ], "colours": "rrrrrreeeoooooooooo", "sort": [{"name": "Email", "order": "ASC"}], + "get_category_elements": get_contacts, + "get_element_data": get_contact_data, }, "Facility": { "headers": [ @@ -1253,6 +1211,8 @@ config = { ], "colours": "ririrriiiireeeeeeeoooo", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_facilities, + "get_element_data": get_facility_data, }, "Floor": { "headers": [ @@ -1269,6 +1229,8 @@ config = { ], "colours": "ririeeeooo", "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_floors, + "get_element_data": get_floor_data, }, "Space": { "headers": [ @@ -1288,6 +1250,8 @@ config = { ], "colours": "ririrreeeoooo", "sort": [{"name": "FloorName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_spaces, + "get_element_data": get_space_data, }, "Zone": { "headers": [ @@ -1303,6 +1267,8 @@ config = { ], "colours": "ririreeeo", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_zones, + "get_element_data": get_zone_data, }, "Type": { "headers": [ @@ -1344,6 +1310,8 @@ config = { ], "colours": "riririiriririeeeooiorrroooooooooooo", "sort": [{"name": "ExternalObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_types, + "get_element_data": get_type_data, }, "Component": { "headers": [ @@ -1369,6 +1337,8 @@ config = { {"name": "TypeName", "order": "ASC"}, {"name": "Name", "order": "ASC"}, ], + "get_category_elements": get_components, + "get_element_data": get_component_data, }, "System": { "headers": [ @@ -1384,8 +1354,10 @@ config = { ], "colours": "ririieeeo", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_systems, + "get_element_data": get_system_data, }, - "Assembly": { + "Assembly": { # Note that this is technically "not required" "headers": [ "Name", "CreatedBy", @@ -1401,8 +1373,10 @@ config = { ], "colours": "ririiiieeeo", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_assemblies, + "get_element_data": get_assembly_data, }, - "Connection": { + "Connection": { # Note that this is technically "not required" "headers": [ "Name", "CreatedBy", @@ -1421,6 +1395,8 @@ config = { ], "colours": "ririiiiiiieeeo", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_connections, + "get_element_data": get_connection_data, }, "Spare": { "headers": [ @@ -1439,6 +1415,8 @@ config = { ], "colours": "ririiieeeooo", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_spares, + "get_element_data": get_spare_data, }, "Resource": { "headers": [ @@ -1453,6 +1431,8 @@ config = { ], "colours": "ririeeeo", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_resources, + "get_element_data": get_resource_data, }, "Job": { "headers": [ @@ -1478,6 +1458,8 @@ config = { ], "colours": "ririiirriririeeeoii", "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "TaskNumber", "order": "ASC"}], + "get_category_elements": get_jobs, + "get_element_data": get_job_data, }, "Document": { "headers": [ @@ -1499,6 +1481,8 @@ config = { ], "colours": "ririiiiirreeeoo", "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_documents, + "get_element_data": get_document_data, }, "Attribute": { "headers": [ @@ -1518,6 +1502,8 @@ config = { ], "colours": "ririiirreeeoo", "sort": [{"name": "Category", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_attributes, + "get_element_data": get_attribute_data, }, - } + }, } diff --git a/src/ifcopenshell-python/docs/bcf.rst b/src/ifcopenshell-python/docs/bcf.rst new file mode 100644 index 0000000000..6e09d19de6 --- /dev/null +++ b/src/ifcopenshell-python/docs/bcf.rst @@ -0,0 +1,112 @@ +BCF +=== + +**BIM Collaboration Format** (BCF) is a standard by buildingSMART to manage and +exchange coordination topics between disciplines collaborating on a project. +For example, when there is an issue during the design, engineering, or +construction of a project, a topic may be created, assigned, prioritised, +commented, or linked to objects in a BIM model or camera location. + +There are two implementations of BCF: + +1. **BCF-XML**: an XML file-based exchange of collaboration topics. This is + useful for mass imports, exports, data migration across CDEs, or fully + offline implementations. +2. **BCF-API**: an online RESTful API-based management of collaboration topics. + When topics are managed by a CDE, if the CDE follows the OpenCDE + specification by buildingSMART, their topics may be accessed and manipulated + using BCF. + +The upstream documentation by buildingSMART for BCF is available here: + +1. `BCF-XML 2.1 upstream documentation + `__. +2. `BCF-XML 3.0 upstream documentation + `__. +3. `BCF-API 3.0 upstream documentation + `__. + +The IfcOpenShell **BCF** library supports BCF-XML version 2.1 and 3.0, and +BCF-API 3.0. + +BCF-XML +------- + +The ``bcfxml.load`` function lets you read a BCF-XML file. + +It takes care of using the right version based on the "bcf.version" file +contained in the BCF package. + +The BCF files are extracted and parsed on-demand, and edits are stored in +memory until you call the `save` method. + +.. code-block:: python + + from bcf.bcfxml import load + + # Load a project + with load("/path/to/file.bcf") as bcfxml: + project = bcfxml.project + print(project.name) + + # To edit a project, just modify the object directly + bcfxml.project.name = "New name" + + # Get a dictionary of topics + topics = bcfxml.topics + + for topic_guid, topic_handler in bcfxml.topics.items(): + topic = topic_handler.topic + print("Topic guid is", topic.guid) + print("Topic title is", topic.title) + + # Fetch extra data about a topic + header = topic_handler.header + comments = topic_handler.comments + viewpoints = topic_handler.viewpoints + + for comment in comments: + print(comment.guid) + print(comment.comment) + print(comment.author) + + # Get a particular topic + topic = bcfxml.get_topic(guid) + + # Modify a topic + topic.title = "New title" + + bcfxml.save() + +BCF-API +------- + +The ``bcfapi`` module lets you interact with the BCF-API standard. + +.. code-block:: python + + from bcf.v3.bcfapi import FoundationClient, BcfClient + + foundation_client = FoundationClient("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET", "OPENCDE_BASEURL") + auth_methods = foundation_client.get_auth_methods() + + # Our library currently only implements the authorization_code flow + if "authorization_code" in auth_methods: + foundation_client.login() + + bcf_client = BcfClient(foundation_client) + + versions = foundation_client.get_versions() + for version in versions: + if "3.0" in versions: + if version["api_id"] == "bcf" and version["version_id"] == "3.0": + bcf_client.set_version(version) + + data = bcf_client.get_projects() + print(data) + project_id = data[0]["project_id"] + print(project_id) + data = bcf_client.get_project(project_id) + print(data) + data = bcf_client.get_extensions(project_id) + print(data) diff --git a/src/ifcopenshell-python/docs/bimserver-plugin.rst b/src/ifcopenshell-python/docs/bimserver-plugin.rst index 432c499f87..b9d9beae07 100644 --- a/src/ifcopenshell-python/docs/bimserver-plugin.rst +++ b/src/ifcopenshell-python/docs/bimserver-plugin.rst @@ -1,16 +1,5 @@ BIMServer-Plugin ================ -This documentation is free software! You are free to contribute and help write -this document. - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - -Indices and tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +The BIMServer-Plugin is a plugin to the open source BIMServer CDE to allow you +to use IfcOpenShell to parse, view, and audit models. diff --git a/src/ifcopenshell-python/docs/bimtester.rst b/src/ifcopenshell-python/docs/bimtester.rst index 8640df8bab..bdecbc2131 100644 --- a/src/ifcopenshell-python/docs/bimtester.rst +++ b/src/ifcopenshell-python/docs/bimtester.rst @@ -1,16 +1,4 @@ BIMTester ========= -This documentation is free software! You are free to contribute and help write -this document. - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - -Indices and tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +BIMTester is a utility that allows you to write Gherkin-based tests for models. diff --git a/src/ifcopenshell-python/docs/bsdd.rst b/src/ifcopenshell-python/docs/bsdd.rst new file mode 100644 index 0000000000..d8f53b4f0f --- /dev/null +++ b/src/ifcopenshell-python/docs/bsdd.rst @@ -0,0 +1,56 @@ +bSDD +==== + +The **buildingSMART Data Dictionary** (bSDD) is an online RESTful centralised +API provided by buildingSMART that allows you to search for standardised +classifications and properties. + +For example, if you want to assign a Uniclass classification system (popular in +the UK) or an Omniclass classification system (popular in the US) to elements +in your model, instead of downloading the classification system from their +website, you can directly search the bSDD. This ensures that you are always up +to date, and that codes are entered correctly (without spelling mistakes, +correct formatting, etc). + +The bSDD search results may also be filtered based on IFC class. This will make +it quick to shortlist relevant classification codes and properties to a +particular object. + +The bSDD also stores information on whether or not classification systems +require additional standard properties to be filled out, and whether they +should be filled out in a particular way. For example, all countries need to +fill out a "Fire Rating" property for walls, but they have different ways to +fill it out. Local governments (or companies) may submit their standard to the +bSDD so that all bSDD-compatible BIM applications can look up the property and +fill it out in a standardised way (such as picking for a list of preset +possible values defined by the local government). + +More reading: + +1. `Swagger API docs `_ +2. `bSDD Github Repository `_ + +Examples +-------- + +Learning how to use the bSDD is best done by reading the official Swagger API docs. + +.. code-block:: python + + client = Client() + + # Get a list of "dictionary domains". For example, Uniclass (by the NBS organisation) might be one domain. + print(client.Domain()) + + # For example, search the Netherland's Nlsfb2005 classification standard for all codes that apply to an IfcWall. + print(client.SearchListOpen("http://identifier.buildingsmart.org/uri/nlsfb/nlsfb2005-2.2", RelatedIfcEntity="IfcWall")) + + # Alternatively, search up a particular classification code. + data = client.Classification("http://identifier.buildingsmart.org/uri/nlsfb/nlsfb2005-2.2/class/21.21") + print(data) + + # You may also apply default properties (if the classification system on + # the bSDD defines them) to your IFC element. For example, if a + # classification code is for a load bearing wall, it can automatically set + # the "LoadBearing" property to True for you. + apply_ifc_classification_properties(ifc_file, element, data["classificationProperties"]) diff --git a/src/ifcopenshell-python/docs/conf.py b/src/ifcopenshell-python/docs/conf.py index b766df476c..0d981bfc71 100644 --- a/src/ifcopenshell-python/docs/conf.py +++ b/src/ifcopenshell-python/docs/conf.py @@ -66,7 +66,8 @@ autoapi_add_toctree_entry = True autoapi_type = 'python' # autoapi works by reading source code instead of importing modules -autoapi_dirs = ['../ifcopenshell', '../../ifcdiff', '../../ifcpatch/ifcpatch'] +autoapi_dirs = ['../ifcopenshell', '../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester'] +# autoapi_dirs = ['../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester'] # These are auto-generated based on the IFC schema, so exclude them autoapi_ignore = ['*ifcopenshell/express/rules*'] diff --git a/src/ifcopenshell-python/docs/ifccobie.rst b/src/ifcopenshell-python/docs/ifccobie.rst deleted file mode 100644 index 35ba047267..0000000000 --- a/src/ifcopenshell-python/docs/ifccobie.rst +++ /dev/null @@ -1,16 +0,0 @@ -IfcCOBie -======== - -This documentation is free software! You are free to contribute and help write -this document. - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - -Indices and tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/src/ifcopenshell-python/docs/ifcfm.rst b/src/ifcopenshell-python/docs/ifcfm.rst new file mode 100644 index 0000000000..cefbe99a87 --- /dev/null +++ b/src/ifcopenshell-python/docs/ifcfm.rst @@ -0,0 +1,4 @@ +IfcFM +===== + +IfcFM is a utility to diff --git a/src/ifcopenshell-python/docs/ifcmax.rst b/src/ifcopenshell-python/docs/ifcmax.rst new file mode 100644 index 0000000000..a564e15b93 --- /dev/null +++ b/src/ifcopenshell-python/docs/ifcmax.rst @@ -0,0 +1,20 @@ +IfcMax +====== + +IfcMax is a 3ds Max importer plugin able to import the IFC file format. + +Community builds are available for 3ds Max by Josef Wienerroither (also known +as ``FrogsInSpace`` or ``spacefrog``). Builds are available for IfcOpenShell +v0.7.0 for 3ds Max version 2020-2024. Older builds are also available for +IfcOpenShell v0.6.0 for 3ds Max version 2015-2022. + +It is recommended to use the latest version of IfcOpenShell and 3ds Max. + +- `Visit FrogsInSpace official website for IfcMax `__. +- `Download IfcMax plugins `__. + +.. note:: + + This plugin is purely an importer and does not handle native IFC authoring + or exporting. For more information for native IFC authoring, we recommend + using the :doc:`BlenderBIM Add-on`. diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst index 5c0d1b045a..8dc7811f1d 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst @@ -11,9 +11,10 @@ packages**. If you aren't a programmer, go for the **BlenderBIM Add-on**. 4. **Docker** is recommended for developers using Docker. 5. **AWS Lambda** is recommended for developers using AWS Lambda functions. 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. +7. **Web Assembly** is recommended for developers experimenting with IfcOpenShell on the web. +8. **Using the BlenderBIM Add-on** is recommended for non-developers wanting a graphical interface. +9. **From source with precompiled binaries** is recommended for developers actively working with the Python code. +10. **Compiling from source** is recommended for developers actively working with the C++ core. Pre-built packages ------------------ @@ -197,6 +198,17 @@ local system. `__ to launch a simple notebook. +Web Assembly +------------ + +IfcOpenShell is available as technology preview to be run using WASM. This +allows you to run IfcOpenShell in a browser using pyodide. This implementation +is incredibly heavy and will incur a long load time, but once loaded, will give +you full access to the entire IfcOpenShell API. + +`Click here `__ to learn how to +use WASM. + Using the BlenderBIM Add-on --------------------------- diff --git a/src/ifcopenshell-python/docs/index.rst b/src/ifcopenshell-python/docs/index.rst index f5bd6086d0..4aca839ad3 100644 --- a/src/ifcopenshell-python/docs/index.rst +++ b/src/ifcopenshell-python/docs/index.rst @@ -22,16 +22,19 @@ IfcOpenShell is a suite of developer libraries and utilities to manipulate OpenB :maxdepth: 1 :caption: Utilities: + bcf bimserver-plugin bimtester + bsdd ifc2ca ifc4d ifc5d ifccityjson ifcclash - ifccobie ifccsv ifcdiff + ifcfm + ifcmax ifcpatch ifcsverchok ifctester diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py index 2787b0048c..e947654b3a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py @@ -18,6 +18,7 @@ import ifcopenshell.api import ifcopenshell.util.date +import ifcopenshell.util.resource class Usecase: @@ -105,8 +106,10 @@ class Usecase: total_cost = 0 for resource in resources: - cost = self.get_cost(resource) - quantity = self.get_quantity(resource) + cost = ifcopenshell.util.resource.get_cost(resource) + quantity = ifcopenshell.util.resource.get_quantity(resource) + if not cost: + cost = ifcopenshell.util.resource.get_parent_cost(resource) # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data. if not cost or not quantity: continue total_cost += cost * quantity @@ -114,20 +117,3 @@ class Usecase: if total_cost: cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=self.settings["cost_item"]) cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(total_cost) - - def get_cost(self, resource): - total = 0 - for cost_value in resource.BaseCosts or []: - total += cost_value.AppliedValue.wrappedValue if cost_value.AppliedValue else 0 - return total - - def get_quantity(self, resource): - total = 0 - if resource.BaseQuantity: - return resource.BaseQuantity[3] - if resource.Usage and resource.Usage.ScheduleWork: - # For now we assume either hourly or daily depending on how duration is stored - duration = ifcopenshell.util.date.ifc2datetime(resource.Usage.ScheduleWork) - if duration.days: - return duration.days - return duration.seconds / 60 / 60 diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py index 33414bc486..081f30a43d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell.api + class Usecase: def __init__(self, file, cost_schedule=None): @@ -42,4 +44,13 @@ class Usecase: def execute(self): # TODO: do a deep purge + for inverse in self.file.get_inverse(self.settings["cost_schedule"]): + if inverse.is_a("IfcRelAssignsToControl"): + [ + ifcopenshell.api.run( + "sequence.remove_cost_item", self.file, task=related_object + ) + for related_object in inverse.RelatedObjects + if related_object.is_a("IfcCostItem") + ] self.file.remove(self.settings["cost_schedule"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py index 322fb46356..44aa512ab3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py @@ -65,8 +65,13 @@ class Usecase: elif self.settings["pset"].is_a() in ("IfcMaterialProperties", "IfcProfileProperties"): properties = self.settings["pset"].Properties or [] for prop in properties: - if self.file.get_total_inverses(prop) == 1: - self.file.remove(prop) + if self.file.get_total_inverses(prop) != 1: + continue + if prop.is_a("IfcPropertyEnumeratedValue"): + enumeration = prop.EnumerationReference + if self.file.get_total_inverses(enumeration) == 1: + self.file.remove(enumeration) + self.file.remove(prop) self.file.remove(self.settings["pset"]) for element in to_purge: self.file.remove(element) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py index a3b4dd2a16..078f5b7f5c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py @@ -120,7 +120,7 @@ class Usecase: for rel in self.settings["related_process"].IsSuccessorFrom or []: if rel.RelatingProcess == self.settings["relating_process"]: return rel - return self.file.create_entity( + rel = self.file.create_entity( "IfcRelSequence", **{ "GlobalId": ifcopenshell.guid.new(), @@ -132,3 +132,7 @@ class Usecase: "SequenceType": self.settings["sequence_type"], } ) + ifcopenshell.api.run( + "sequence.cascade_schedule", self.file, task=self.settings["relating_process"] + ) + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py index d8fc4f4384..b541a81670 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py @@ -135,7 +135,9 @@ class Usecase: finishes = [] starts = [] - for rel in task.IsSuccessorFrom: + for rel in ifcopenshell.util.sequence.get_sequence_assignment( + task, "predecessor" + ): predecessor = rel.RelatingProcess predecessor_duration = ( ifcopenshell.util.date.ifc2datetime( @@ -314,6 +316,12 @@ class Usecase: for rel in task.IsPredecessorTo: self.cascade_task(rel.RelatedProcess, task_sequence=task_sequence + [task]) + for rel in task.IsNestedBy: + [ + self.cascade_task(nested_task, task_sequence=task_sequence + [task]) + for nested_task in rel.RelatedObjects or [] + ] + def get_lag_time_days(self, lag_time): return ifcopenshell.util.date.ifc2datetime(lag_time.LagValue.wrappedValue).days diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py index c7e911e2d9..4039c53c57 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py @@ -148,7 +148,7 @@ class Usecase: [ ( rel.RelatingProcess.id(), - rel.RelatedProcess.id(), + task.id(), { "lag_time": 0 if not rel.TimeLag @@ -158,11 +158,12 @@ class Usecase: "type": self.sequence_type_map[rel.SequenceType], }, ) - for rel in task.IsSuccessorFrom or [] + for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, sequence="predecessor") ] ) - predecessor_types = [rel.SequenceType for rel in task.IsSuccessorFrom] - successor_types = [rel.SequenceType for rel in task.IsPredecessorTo] + + predecessor_types = [rel.SequenceType for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, "predecessor")] + successor_types = [rel.SequenceType for rel in ifcopenshell.util.sequence.get_sequence_assignment(task, "successor")] if not predecessor_types: self.edges.append(("start", task.id(), {"lag_time": 0, "type": "FS"})) @@ -170,6 +171,7 @@ class Usecase: self.start_dates.append( ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleStart) ) + self.g.nodes[task.id()]["early_start"] = ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleStart) # we assume this task is constrained to start on this date if not successor_types: self.edges.append((task.id(), "finish", {"lag_time": 0, "type": "FF"})) @@ -223,6 +225,16 @@ class Usecase: else: finishes = [] starts = [] + if data.get("early_start") is not None: + data["early_finish"] = ifcopenshell.util.sequence.get_start_or_finish_date( + data["early_start"], + datetime.timedelta(days=data["duration"]), + data["duration_type"], + data["calendar"], + date_type="FINISH", + ) + return True # we're done! We assume this task is constrained and finish processing it + for predecessor in predecessors: predecessor_data = self.g.nodes[predecessor] edge = self.g[predecessor][node] diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py index 1ff6609ce1..eddfae5f59 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py @@ -70,8 +70,8 @@ class Usecase: } def execute(self): - rels = self.settings["port"].ConnectedTo or [] - rels += self.settings["port"].ConnectedFrom or [] + rels = self.settings["port"].ConnectedTo or () + rels += self.settings["port"].ConnectedFrom or () for rel in rels: rel.RelatingPort.FlowDirection = None diff --git a/src/ifcopenshell-python/ifcopenshell/util/date.py b/src/ifcopenshell-python/ifcopenshell/util/date.py index 27e6df4af0..f7f7c102f1 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/date.py +++ b/src/ifcopenshell-python/ifcopenshell/util/date.py @@ -35,8 +35,7 @@ def timedelta2duration(timedelta): } if components["seconds"]: components["hours"], components["minutes"], components["seconds"] = [ - int(i) - for i in str(datetime.timedelta(seconds=components["seconds"])).split(":") + int(i) for i in str(datetime.timedelta(seconds=components["seconds"])).split(":") ] return isodate.Duration(**components) @@ -122,9 +121,7 @@ def datetime2ifc(dt, ifc_type): if isinstance(dt, datetime.datetime): return dt.isoformat() elif isinstance(dt, datetime.date): - return datetime.datetime.combine( - dt, datetime.datetime.min.time() - ).isoformat() + return datetime.datetime.combine(dt, datetime.datetime.min.time()).isoformat() elif ifc_type == "IfcDate": if isinstance(dt, datetime.datetime): return dt.date().isoformat() @@ -180,9 +177,7 @@ def string_to_duration(duration_string): match = findall(r"(\d+\.?\d*)s", duration_string) if match: seconds = float(match[0]) - return isodate.duration_isoformat( - datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) - ) + return isodate.duration_isoformat(datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)) def parse_duration(value): @@ -192,9 +187,11 @@ def parse_duration(value): if "P" in value: try: return isodate.parse_duration(value) + except ModuleNotFoundError: + print("Duration parsing not supported: isodate module not found") except: - print("error parsing ISO string duration") - return None + print("Error parsing ISO string duration") + return None else: try: final_string = "P" @@ -204,11 +201,7 @@ def parse_duration(value): final_string += char elif char == "D": final_string += "D" - if ( - "H" in value_upper - or "S" in value_upper - or "MIN" in value_upper - ): + if "H" in value_upper or "S" in value_upper or "MIN" in value_upper: final_string += "T" elif char == "W": final_string += "W" @@ -218,9 +211,7 @@ def parse_duration(value): final_string += "Y" elif char == "H": final_string = ( - final_string[:1] + "T" + final_string[1:] - if "T" not in final_string - else final_string + final_string[:1] + "T" + final_string[1:] if "T" not in final_string else final_string ) final_string += "H" elif char == "M": @@ -229,9 +220,7 @@ def parse_duration(value): final_string += "M" elif char == "S": final_string = ( - final_string[:1] + "T" + final_string[1:] - if "T" not in final_string - else final_string + final_string[:1] + "T" + final_string[1:] if "T" not in final_string else final_string ) final_string += "S" return isodate.parse_duration(final_string) diff --git a/src/ifcopenshell-python/ifcopenshell/util/resource.py b/src/ifcopenshell-python/ifcopenshell/util/resource.py index f1e17dcc42..f3ce2082e0 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/resource.py +++ b/src/ifcopenshell-python/ifcopenshell/util/resource.py @@ -113,5 +113,32 @@ def get_resource_required_work(resource): iso_string = f"P{required_work}D" return iso_string + def get_nested_resources(resource): return [object for rel in resource.IsNestedBy or [] for object in rel.RelatedObjects] + + +def get_cost(resource): + total = 0 + for cost_value in resource.BaseCosts or []: + total += cost_value.AppliedValue.wrappedValue if cost_value.AppliedValue else 0 + return total + + +def get_quantity(resource): + total = 0 + if resource.BaseQuantity: + return resource.BaseQuantity[3] + if resource.Usage and resource.Usage.ScheduleWork: + # For now we assume either hourly or daily depending on how duration is stored + duration = ifcopenshell.util.date.ifc2datetime(resource.Usage.ScheduleWork) + if duration.days: + return duration.days + return duration.seconds / 60 / 60 + +def get_parent_cost(resource): + if not resource.Nests: + return + else: + cost = get_cost(resource.Nests[0].RelatingObject) + return cost diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index 02ec945a63..a97e9a58a1 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -62,6 +62,8 @@ def derive_calendar(task): def count_working_days(start, finish, calendar): result = 0 + if start == finish: + return 0 current_date = datetime.date(start.year, start.month, start.day) finish_date = datetime.date(finish.year, finish.month, finish.day) while current_date <= finish_date: @@ -426,3 +428,23 @@ def get_tasks_for_product(product, schedule=None): ] return inputs, outputs + + +def get_sequence_assignment(task, sequence="successor"): + if sequence == "successor": + relationship_attr = "IsPredecessorTo" + elif sequence == "predecessor": + relationship_attr = "IsSuccessorFrom" + else: + return [] + + relationship = getattr(task, relationship_attr, None) + if relationship: + return relationship + + for rel in task.Nests or []: + result = get_sequence_assignment(rel.RelatingObject, sequence) + if result: + return result + + return [] diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 43822dfc6c..6941e37b2f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -1075,7 +1075,7 @@ class ShapeBuilder: transition_items.append(self.extrude_face_set(first_profile_points, start_length, end_cap=False)) transition_items.append( - self.extrude_face_set(second_profile_points, end_length, end_extrusion_offset, start_cap=False) + self.extrude_face_set(second_profile_points, end_length, offset=end_extrusion_offset, start_cap=False) ) first_profile_points = [p + start_offset for p in first_profile_points] @@ -1105,7 +1105,7 @@ class ShapeBuilder: transition_items.append(self.extrude_face_set(start_points, start_length, end_cap=False)) transition_items.append( - self.extrude_face_set(end_points, end_length, end_extrusion_offset, start_cap=False) + self.extrude_face_set(end_points, end_length, offset=end_extrusion_offset, start_cap=False) ) # offset verts diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index 43eae4fd18..a96c424835 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -362,12 +362,18 @@ def get_property_unit(prop, ifc_file): if not unit_assignment: return entity = prop.wrapped_data.declaration().as_entity() + measure_class = None if prop.is_a("IfcPhysicalSimpleQuantity"): measure_class = entity.attribute_by_index(3).type_of_attribute().declared_type().name() elif prop.is_a("IfcPropertySingleValue") and prop.NominalValue: measure_class = prop.NominalValue.is_a() - elif prop.is_a("IfcPropertyEnumeratedValue") and prop.EnumerationValues: - measure_class = prop.EnumerationValues[0].is_a() + elif prop.is_a("IfcPropertyEnumeratedValue"): + if prop.EnumerationReference: + unit = getattr(prop.EnumerationReference, "Unit", None) + if unit: + return unit + if prop.EnumerationValues: + measure_class = prop.EnumerationValues[0].is_a() elif prop.is_a("IfcPropertyListValue") and prop.ListValues: measure_class = prop.ListValues[0].is_a() elif prop.is_a("IfcPropertyBoundedValue"): @@ -393,6 +399,8 @@ def get_property_unit(prop, ifc_file): else: table_units[f"{attribute}Unit"] = None return table_units + if measure_class is None: + return unit_type = get_measure_unit_type(measure_class) units = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == unit_type] if units: diff --git a/src/ifcopenshell-python/pyproject.toml b/src/ifcopenshell-python/pyproject.toml index dc8db46e66..cf65b9de30 100644 --- a/src/ifcopenshell-python/pyproject.toml +++ b/src/ifcopenshell-python/pyproject.toml @@ -15,6 +15,7 @@ classifiers = [ ] [project.optional-dependencies] geometry = ["mathutils"] +date = ["isodate"] [project.urls] "Homepage" = "http://ifcopenshell.org" "Bug Tracker" = "https://github.com/ifcopenshell/ifcopenshell/issues" diff --git a/src/ifcopenshell-python/test/api/pset/test_remove_pset.py b/src/ifcopenshell-python/test/api/pset/test_remove_pset.py index 3ce8d0127e..1685f158e8 100644 --- a/src/ifcopenshell-python/test/api/pset/test_remove_pset.py +++ b/src/ifcopenshell-python/test/api/pset/test_remove_pset.py @@ -83,3 +83,27 @@ class TestRemovePset(test.bootstrap.IFC4): pset2.HasProperties = pset.HasProperties ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset) assert pset2.HasProperties + + def test_removing_a_pset_with_enumeration(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Pset_WallCommon") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Status": ["NEW"]}) + ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset) + assert len(self.file.by_type("IfcPropertyEnumeration")) == 0 + + def test_removing_a_pset_with_shared_enumeration(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Pset_WallCommon") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Status": ["NEW"]}) + + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + pset2 = ifcopenshell.api.run("pset.add_pset", self.file, product=element2, name="Pset_WallCommon") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset2, properties={"Status": ["NEW"]}) + + enumeration1 = pset.HasProperties[0].EnumerationReference + enumeration2 = pset2.HasProperties[0].EnumerationReference + pset2.HasProperties[0].EnumerationReference = enumeration1 + self.file.remove(enumeration2) + + ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset) + assert len(self.file.by_type("IfcPropertyEnumeration")) == 1 diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index d5306a84b3..c5b69a8724 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -34,12 +34,13 @@ class TestFormat(): assert subject.format('title(\"fOo\")') == "Foo" assert subject.format('concat(\"fOo\", \"bar\")') == "fOobar" assert subject.format('upper(concat(\"fOo\", \"bar\"))') == "FOOBAR" + assert subject.format('substr(\"foobar\", 3)') == "bar" assert subject.format('substr(\"foobar\", 1, 2)') == "o" assert subject.format('substr(\"foobar\", 1, -1)') == "ooba" def test_number_formatting(self): - assert subject.format("round(123, 5)") == "125.0" - assert subject.format('round(\"123\", 5)') == "125.0" + assert subject.format("round(123, 5)") == "125" + assert subject.format('round(\"123\", 5)') == "125" assert subject.format('metric_length(123, 5, 2)') == "125.00" assert subject.format('metric_length(123.123, 0.1, 2)') == "123.10" assert subject.format('metric_length(\"123\", 5, 2)') == "125.00" diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 2e5efdf0ae..791223a11b 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -637,7 +637,7 @@ class Property(Facet): elif prop_entity.is_a("IfcPropertySingleValue"): data_type = prop_entity.NominalValue.is_a() - if data_type != self.datatype: + if data_type.lower() != self.datatype.lower(): is_pass = False reason = {"type": "DATATYPE", "actual": data_type} break @@ -656,7 +656,7 @@ class Property(Facet): prop_schema = prop_entity.wrapped_data.declaration().as_entity() data_type = prop_schema.attribute_by_index(3).type_of_attribute().declared_type().name() - if data_type != self.datatype: + if data_type.lower() != self.datatype.lower(): is_pass = False reason = {"type": "DATATYPE", "actual": data_type} break @@ -676,7 +676,7 @@ class Property(Facet): reason = {"type": "NOVALUE"} break data_type = prop_entity.EnumerationValues[0].is_a() - if data_type != self.datatype: + if data_type.lower() != self.datatype.lower(): is_pass = False reason = {"type": "DATATYPE", "actual": data_type} break @@ -686,7 +686,7 @@ class Property(Facet): reason = {"type": "NOVALUE"} break data_type = prop_entity.ListValues[0].is_a() - if data_type != self.datatype: + if data_type.lower() != self.datatype.lower(): is_pass = False reason = {"type": "DATATYPE", "actual": data_type} break @@ -709,7 +709,7 @@ class Property(Facet): if value is not None: data_type = value.is_a() values.append(value.wrappedValue) - if data_type != self.datatype: + if data_type.lower() != self.datatype.lower(): is_pass = False reason = {"type": "DATATYPE", "actual": data_type} break @@ -734,7 +734,7 @@ class Property(Facet): if not column_values: continue data_type = column_values[0].is_a() - if data_type == self.datatype: + if data_type.lower() == self.datatype.lower(): column_values = [v.wrappedValue for v in column_values] unit = units[f"{attribute}Unit"] if unit: diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py index c3ebfed16c..c4766259bb 100644 --- a/src/ifctester/ifctester/ids.py +++ b/src/ifctester/ifctester/ids.py @@ -173,8 +173,8 @@ class Specification: self.minOccurs = ids_dict["@minOccurs"] self.maxOccurs = ids_dict["@maxOccurs"] self.ifcVersion = ids_dict["@ifcVersion"] - self.applicability = self.parse_clause(ids_dict["applicability"]) if "applicability" in ids_dict else [] - self.requirements = self.parse_clause(ids_dict["requirements"]) if "requirements" in ids_dict else [] + self.applicability = self.parse_clause(ids_dict["applicability"]) if ids_dict.get("applicability") is not None else [] + self.requirements = self.parse_clause(ids_dict["requirements"]) if ids_dict.get("requirements") is not None else [] return self def parse_clause(self, clause): @@ -247,9 +247,7 @@ class Specification: if self.failed_entities: self.status = False elif self.maxOccurs == 0: - if (len(self.applicable_entities)) > 0 and len(self.requirements) == 0: - self.status = False - if (len(self.applicable_entities)) > 0 and (len(self.applicable_entities) - len(self.failed_entities)) > 0: + if (len(self.applicable_entities)) > 0: self.status = False def get_usage(self): diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index c982e1ec9b..e9b139b829 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -211,16 +211,19 @@ class Json(Reporter): requirements = [] for requirement in specification.requirements: total_fail = len(requirement.failed_entities) + total_pass = total_applicable - total_fail + percent_pass = math.floor((total_pass / total_applicable) * 100) if total_applicable else "N/A" total_checks += total_applicable - total_checks_pass += total_applicable - total_fail + total_checks_pass += total_pass requirements.append( { "description": requirement.to_string("requirement"), "status": requirement.status, "failed_entities": self.report_failed_entities(requirement), "total_applicable": total_applicable, - "total_pass": total_applicable - total_fail, + "total_pass": total_pass, "total_fail": total_fail, + "percent_pass": percent_pass, } ) total_applicable_pass = total_applicable - len(specification.failed_entities) @@ -433,13 +436,16 @@ class Bcf(Json): continue for failure in requirement["failed_entities"]: element = failure["element"] - title_components = [ + title_components = [] + for title_component in [ element.is_a(), - getattr(element, "Name", None) or "Unnamed", + getattr(element, "Name", "") or "Unnamed", failure.get("reason", "No reason"), getattr(element, "GlobalId", ""), getattr(element, "Tag", ""), - ] + ]: + if title_component: + title_components.append(title_component) title = " - ".join(title_components) description = f'{specification["name"]} - {requirement["description"]}' topic = bcfxml.add_topic(title, description, "IfcTester")