From 4f1e21dfb024fa2aea33eee150e2e6816b6c5bdd Mon Sep 17 00:00:00 2001 From: c4rlosdias Date: Sat, 5 Aug 2023 10:50:27 -0300 Subject: [PATCH 01/22] Add element guId in BCF report --- src/ifctester/ifctester/reporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index ae9e0cd479..0cac09d14d 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -342,7 +342,7 @@ class Bcf(Json): continue for failure in requirement["failed_entities"]: element = failure["element"] - title = f"{element.id()}/{element.is_a()}/" + title = f"ID:[{element.id()}]/GUID:[{element.GlobalId}]/{element.is_a()}/" title += getattr(element, "Name", None) or "Unnamed" title += " - " + failure.get("reason", "No reason") description = f'{specification["name"]} - {requirement["description"]}' From 11c8d479097eb39306c52ee9f8e78ddb9ee856e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 25 Sep 2023 22:55:56 -0300 Subject: [PATCH 02/22] fixed a bug where the refresh aggregate operator was not working after adding new objects to the parent aggregate --- .../bim/module/geometry/operator.py | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index dcf63827b9..c2259d8ac6 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -1202,9 +1202,64 @@ class RefreshAggregate(bpy.types.Operator): else: parts = ifcopenshell.util.element.get_parts(instance_entity) for part in parts: + pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data") + if part.is_a("IfcElementAssembly"): + if not pset: + data_children = { + "children": [], + "instance_of": [part.GlobalId], + } + data = [data_children] + + pset = ifcopenshell.api.run( + "pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data" + ) + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + pset=pset, + properties={"Parent": instance_entity.GlobalId, "Data": json.dumps(data)}, + ) + + part_obj = tool.Ifc.get_object(part) + new_part = duplicate_objects(part_obj) + + blenderbim.core.aggregate.assign_object( + tool.Ifc, + tool.Aggregate, + tool.Collector, + relating_obj=tool.Ifc.get_object(entity), + related_obj=tool.Ifc.get_object(new_part), + ) + duplicate_children(new_part) + + for part in parts: + pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data") if part.is_a("IfcElementAssembly"): pass + else: + if not pset: + pset = ifcopenshell.api.run( + "pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data" + ) + else: + pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data") + pset = tool.Ifc.get().by_id(pset["id"]) + + data_children = { + "children": [], + "instance_of": [], + } + data = [data_children] + + ifcopenshell.api.run( + "pset.edit_pset", + tool.Ifc.get(), + pset=pset, + properties={"Parent": instance_entity.GlobalId, "Data": json.dumps(data)}, + ) + part_obj = tool.Ifc.get_object(part) new_part = duplicate_objects(part_obj) blenderbim.core.aggregate.assign_object( @@ -1225,6 +1280,10 @@ class RefreshAggregate(bpy.types.Operator): collection.objects.link(new_obj) obj.select_set(False) new_obj.select_set(True) + + # This is needed to make sure the new object gets unlink from + # the old object assembly collection + new_obj.BIMObjectProperties.collection = None # Copy the actual class new_entity = blenderbim.core.root.copy_class( @@ -1233,7 +1292,9 @@ class RefreshAggregate(bpy.types.Operator): if new_entity: tool.Model.handle_array_on_copied_element(new_entity) - blenderbim.core.aggregate.unassign_object( + + if not new_entity.is_a("IfcElementAssembly"): + blenderbim.core.aggregate.unassign_object( tool.Ifc, tool.Aggregate, tool.Collector, From ddd4b0e2621f1abfcf23dbeb145b2297ae8e91ce Mon Sep 17 00:00:00 2001 From: c4rlosdias Date: Mon, 2 Oct 2023 13:49:27 -0300 Subject: [PATCH 03/22] Fix problem in getpsets(facet.py) and added descriptions to specifications (IfcTester) --- src/blenderbim/blenderbim/bim/module/tester/operator.py | 4 +++- src/blenderbim/blenderbim/bim/module/tester/prop.py | 1 + src/blenderbim/blenderbim/bim/module/tester/ui.py | 3 ++- src/ifctester/ifctester/facet.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/tester/operator.py b/src/blenderbim/blenderbim/bim/module/tester/operator.py index 57ed89429d..010314e325 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/operator.py +++ b/src/blenderbim/blenderbim/bim/module/tester/operator.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . -import os +import os import bpy import time import tempfile @@ -72,8 +72,10 @@ class ExecuteIfcTester(bpy.types.Operator, tool.Ifc.Operator): tool.Tester.report = report props.specifications.clear() for spec in report: + print(spec) new_spec = props.specifications.add() new_spec.name = spec["name"] + new_spec.description = spec["description"] new_spec.status = spec["status"] blenderbim.bim.handler.refresh_ui_data() diff --git a/src/blenderbim/blenderbim/bim/module/tester/prop.py b/src/blenderbim/blenderbim/bim/module/tester/prop.py index 7496d4ba44..ab4c175196 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/prop.py +++ b/src/blenderbim/blenderbim/bim/module/tester/prop.py @@ -37,6 +37,7 @@ def update_active_specification_index(self, context): class Specification(PropertyGroup): name: StringProperty(name="Name") + description: StringProperty(name="Description") status: BoolProperty(default=False, name="Status") diff --git a/src/blenderbim/blenderbim/bim/module/tester/ui.py b/src/blenderbim/blenderbim/bim/module/tester/ui.py index 31c45bc888..b33cdd5aff 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/ui.py +++ b/src/blenderbim/blenderbim/bim/module/tester/ui.py @@ -107,8 +107,9 @@ class BIM_PT_tester(Panel): class BIM_UL_tester_specifications(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: - row = layout.row(align=True) + row = layout.split(factor=0.3, align=True) row.label(text=item.name, icon="CHECKMARK" if item.status else "CANCEL") + row.label(text=item.description) class BIM_UL_tester_failed_entities(UIList): diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index d10664fee2..78e718a264 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -48,7 +48,7 @@ def get_pset(element, pset): @lru_cache -def get_psets(element, pset): +def get_psets(element): return ifcopenshell.util.element.get_psets(element) From 7ca24ede4c53172f48f6a54459a9ab2caf1c48b6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 3 Oct 2023 10:50:26 +0500 Subject: [PATCH 04/22] IfcSverchok docs update urls --- src/ifcopenshell-python/docs/ifcsverchok/installation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst index c1db44bdf3..775ab2bcb0 100644 --- a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst +++ b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst @@ -11,15 +11,15 @@ Packaged installation --------------------- IfcSverchok is packaged like a regular Blender add-on, so installation is the -same as any other Blender add-on. You can download the package for installation -at the `Get BlenderBIM `__ website. +same as any other Blender add-on. `Download IfcSverchok here +`__. Like all Blender add-ons, they can be installed using ``Edit > Preferences > Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox``. You can enable add-ons permanently by using ``Save User Settings`` from the Addons menu. Before installing, you will also need to `install the BlenderBIM Add-on -<../blenderbim/installation>`__ and `install Sverchok +`__ and `install Sverchok `__. If you downloaded Blender as a ``.zip`` file without running an installer, you From 3904162360361b89609dc1849390fca8e36e809b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 3 Oct 2023 21:19:11 +1100 Subject: [PATCH 05/22] New IfcPatch recipe to fix Revit bug with hardcoded Uniformat classification --- .../RemoveRevitUniformatClassification.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/ifcpatch/ifcpatch/recipes/RemoveRevitUniformatClassification.py diff --git a/src/ifcpatch/ifcpatch/recipes/RemoveRevitUniformatClassification.py b/src/ifcpatch/ifcpatch/recipes/RemoveRevitUniformatClassification.py new file mode 100644 index 0000000000..22cad79bba --- /dev/null +++ b/src/ifcpatch/ifcpatch/recipes/RemoveRevitUniformatClassification.py @@ -0,0 +1,60 @@ +# IfcPatch - IFC patching utiliy +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcPatch. +# +# IfcPatch 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. +# +# IfcPatch 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 IfcPatch. If not, see . + + +class Patcher: + def __init__(self, src, file, logger): + """Removes the built-in Revit Uniformat classification. + + Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/486) + where it always inserts a Uniformat classification regardless if your + project needs it or not. This patch removes it. + + Example: + + .. code:: python + + ifcpatch.execute({"input": model, "recipe": "RemoveRevitUniformatClassification"}) + """ + self.src = src + self.file = file + self.logger = logger + + def patch(self): + for classification in self.file.by_type("IfcClassification"): + if classification.Name != "Uniformat": + continue + references = self.get_references(classification) + for reference in references: + self.file.remove(reference) + self.file.remove(classification) + for rel in self.file.by_type("IfcRelAssociatesClassification"): + if not rel.RelatingClassification: + self.file.remove(rel) + for rel in self.file.by_type("IfcExternalReferenceRelationship"): + if not rel.RelatingReference: + self.file.remove(rel) + + def get_references(self, classification): + results = [] + if not classification.HasReferences: + return results + for reference in classification.HasReferences: + results.append(reference) + results.extend(self.get_references(reference)) + return results From 748055dcf197cfbb7c28109f431b7d97a88a5024 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 3 Oct 2023 21:19:28 +1100 Subject: [PATCH 06/22] New IfcPatch recipe to fix Revit bug with occurrence based classifications --- .../FixRevitClassificationCodeTypes.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/ifcpatch/ifcpatch/recipes/FixRevitClassificationCodeTypes.py diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevitClassificationCodeTypes.py b/src/ifcpatch/ifcpatch/recipes/FixRevitClassificationCodeTypes.py new file mode 100644 index 0000000000..d6bdde4b5a --- /dev/null +++ b/src/ifcpatch/ifcpatch/recipes/FixRevitClassificationCodeTypes.py @@ -0,0 +1,51 @@ +# IfcPatch - IFC patching utiliy +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcPatch. +# +# IfcPatch 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. +# +# IfcPatch 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 IfcPatch. If not, see . + +import ifcopenshell +import ifcopenshell.util.element + + +class Patcher: + def __init__(self, src, file, logger): + """Reassigns occurrence classifications to types + + Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/691) + where it assigns classification codes to occurrences instead of types. + Almost always, this is not what you want. This patch reassigns all + classifications to their respective types. + + Example: + + .. code:: python + + ifcpatch.execute({"input": model, "recipe": "FixRevitClassificationCodeTypes"}) + """ + self.src = src + self.file = file + self.logger = logger + + def patch(self): + for rel in self.file.by_type("IfcRelAssociatesClassification"): + related_types = set() + for related_object in rel.RelatedObjects: + relating_type = ifcopenshell.util.element.get_type(related_object) + if relating_type: + related_types.add(relating_type) + else: + related_types.add(related_object) + rel.RelatedObjects = list(related_types) From a24fab6d92b8b1c51697094e340b7e136ccb476a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 3 Oct 2023 21:20:16 +1100 Subject: [PATCH 07/22] You can now select and bulk process multiple models using IfcFM in Blender. --- .../blenderbim/bim/module/fm/operator.py | 47 ++++++++++++++----- .../blenderbim/bim/module/fm/prop.py | 2 + src/blenderbim/blenderbim/bim/module/fm/ui.py | 5 +- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/fm/operator.py b/src/blenderbim/blenderbim/bim/module/fm/operator.py index d48502790a..a59fe1dc9a 100644 --- a/src/blenderbim/blenderbim/bim/module/fm/operator.py +++ b/src/blenderbim/blenderbim/bim/module/fm/operator.py @@ -32,9 +32,16 @@ class SelectFMIfcFile(bpy.types.Operator): filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") + files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement) def execute(self, context): - context.scene.BIMFMProperties.ifc_file = self.filepath + props = context.scene.BIMFMProperties + props.ifc_files.clear() + dirname = os.path.dirname(self.filepath) + for f in self.files: + new = props.ifc_files.add() + new.name = os.path.join(dirname, f.name) + props.ifc_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -42,6 +49,7 @@ class SelectFMIfcFile(bpy.types.Operator): return {"RUNNING_MODAL"} + class ExecuteIfcFM(bpy.types.Operator): bl_idname = "bim.execute_ifcfm" bl_label = "Execute IfcFM" @@ -66,17 +74,32 @@ class ExecuteIfcFM(bpy.types.Operator): props = context.scene.BIMFMProperties ifc_file = tool.Ifc.get() + filepaths = [] if not (ifc_file and props.should_load_from_memory): - ifc_file = ifcopenshell.open(props.ifc_file) + if len(props.ifc_files): + ifc_files = [ifcopenshell.open(f.name) for f in props.ifc_files] + filepaths = [f.name for f in props.ifc_files] + else: + ifc_files = [ifcopenshell.open(props.ifc_file)] + else: + ifc_files = [ifc_file] - parser = ifcfm.Parser(preset=props.engine) - parser.parse(ifc_file) - writer = ifcfm.Writer(parser) - writer.write() - if props.format == "csv": - writer.write_csv('tmp/') - elif props.format == "ods": - writer.write_ods(self.filepath) - elif props.format == "xlsx": - writer.write_xlsx(self.filepath) + for i, ifc_file in enumerate(ifc_files): + if filepaths: + dirname = os.path.dirname(self.filepath) + prefix, _ = os.path.splitext(os.path.basename(filepaths[i])) + basename = os.path.basename(self.filepath) + filepath = os.path.join(dirname, f"{prefix}-{basename}") + else: + filepath = self.filepath + parser = ifcfm.Parser(preset=props.engine) + parser.parse(ifc_file) + writer = ifcfm.Writer(parser) + writer.write() + if props.format == "csv": + writer.write_csv('tmp/') + elif props.format == "ods": + writer.write_ods(filepath) + elif props.format == "xlsx": + writer.write_xlsx(filepath) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/fm/prop.py b/src/blenderbim/blenderbim/bim/module/fm/prop.py index f6a3074da3..c75ac52518 100644 --- a/src/blenderbim/blenderbim/bim/module/fm/prop.py +++ b/src/blenderbim/blenderbim/bim/module/fm/prop.py @@ -17,6 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy +from blenderbim.bim.prop import StrProperty from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -32,6 +33,7 @@ from bpy.props import ( class BIMFMProperties(PropertyGroup): ifc_file: StringProperty(default="", name="IFC File") + ifc_files: CollectionProperty(name="IFC Files", type=StrProperty) should_load_from_memory: BoolProperty(default=False, name="Load from Memory") engine: EnumProperty( items=[ diff --git a/src/blenderbim/blenderbim/bim/module/fm/ui.py b/src/blenderbim/blenderbim/bim/module/fm/ui.py index 8ef8a4dd43..e838d72a0f 100644 --- a/src/blenderbim/blenderbim/bim/module/fm/ui.py +++ b/src/blenderbim/blenderbim/bim/module/fm/ui.py @@ -42,7 +42,10 @@ class BIM_PT_fm(Panel): if not IfcStore.get_file() or not props.should_load_from_memory: row = layout.row(align=True) - row.prop(props, "ifc_file") + if props.ifc_files: + row.label(text=f"{len(props.ifc_files)} Files Selected") + else: + row.prop(props, "ifc_file") row.operator("bim.select_fm_ifc_file", icon="FILE_FOLDER", text="") row = layout.row() From 82d4829c83535f4587657784eb692161ad7d6fa0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 3 Oct 2023 21:21:16 +1100 Subject: [PATCH 08/22] Clean up IfcFM's Basic preset to be more true to IFC rather than follow naming schemes from stuff like COBie --- src/ifcfm/ifcfm/basic.py | 96 +++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/src/ifcfm/ifcfm/basic.py b/src/ifcfm/ifcfm/basic.py index 697f9c2bbe..76e1a8e856 100644 --- a/src/ifcfm/ifcfm/basic.py +++ b/src/ifcfm/ifcfm/basic.py @@ -64,9 +64,10 @@ def get_facility_data(ifc_file, element): "Name": element.Name, "ProjectName": ifc_file.by_type("IfcProject")[0].Name, "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None), - "Category": get_classification(element), - "AuthorOrganizationName": get_owner_name(element), - "AuthorDate": get_owner_creation_date(element), + "ClassificationIdentification": get_classification_identification(element), + "ClassificationName": get_classification_name(element), + "OrganizationName": get_owner_name(element), + "CreationDate": get_owner_creation_date(element), "ModelSoftware": get_owner_application(element), "ModelProjectID": ifc_file.by_type("IfcProject")[0].GlobalId, "ModelSiteID": getattr(get_facility_parent(element, "IfcSite"), "GlobalId", None), @@ -80,9 +81,10 @@ def get_facility_data(ifc_file, element): def get_storey_data(ifc_file, element): return { "Name": element.Name, - "Category": "Level", - "AuthorOrganizationName": get_owner_name(element), - "AuthorDate": get_owner_creation_date(element), + "ClassificationIdentification": "Level", + "ClassificationName": get_classification_name(element), + "OrganizationName": get_owner_name(element), + "CreationDate": get_owner_creation_date(element), "ModelSoftware": get_owner_application(element), "ModelObject": element.is_a(), "ModelID": element.GlobalId, @@ -95,10 +97,11 @@ def get_space_data(ifc_file, element): return { "Name": element.Name, "Description": element.LongName, - "Category": get_classification(element), + "ClassificationIdentification": get_classification_identification(element), + "ClassificationName": get_classification_name(element), "LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None), - "AuthorOrganizationName": get_owner_name(element), - "AuthorDate": get_owner_creation_date(element), + "OrganizationName": get_owner_name(element), + "CreationDate": get_owner_creation_date(element), "ModelSoftware": get_owner_application(element), "ModelID": element.GlobalId, "AreaGross": get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2), @@ -111,8 +114,8 @@ def get_zone_data(ifc_file, element): return { "Name": zone.Name, "SpaceName": space.Name, - "AuthorOrganizationName": get_owner_name(zone), - "AuthorDate": get_owner_creation_date(zone), + "OrganizationName": get_owner_name(zone), + "CreationDate": get_owner_creation_date(zone), "ModelSoftware": get_owner_application(zone), "ModelID": zone.GlobalId, } @@ -123,9 +126,10 @@ def get_element_type_data(ifc_file, element): return { "Name": element.Name, "Description": element.Description, - "Category": get_classification(element), - "AuthorOrganizationName": get_owner_name(element), - "AuthorDate": get_owner_creation_date(element), + "ClassificationIdentification": get_classification_identification(element), + "ClassificationName": get_classification_name(element), + "OrganizationName": get_owner_name(element), + "CreationDate": get_owner_creation_date(element), "ModelSoftware": get_owner_application(element), "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), "ModelID": element.GlobalId, @@ -149,8 +153,8 @@ def get_element_data(ifc_file, element): "TypeName": ifcopenshell.util.element.get_type(element).Name, "SpaceName": space_name, "SystemName": system, - "AuthorOrganizationName": get_owner_name(element), - "AuthorDate": get_owner_creation_date(element), + "OrganizationName": get_owner_name(element), + "CreationDate": get_owner_creation_date(element), "ModelSoftware": get_owner_application(element), "ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)), "ModelID": element.GlobalId, @@ -169,9 +173,10 @@ def get_system_data(ifc_file, element): return { "Name": element.Name, "Description": element.Description, - "Category": get_classification(element), - "AuthorOrganizationName": get_owner_name(element), - "AuthorDate": get_owner_creation_date(element), + "ClassificationIdentification": get_classification_identification(element), + "ClassificationName": get_classification_name(element), + "OrganizationName": get_owner_name(element), + "CreationDate": get_owner_creation_date(element), "ModelSoftware": get_owner_application(element), "ModelID": element.GlobalId, } @@ -205,12 +210,18 @@ def get_facility_parent(element, ifc_class): parent = ifcopenshell.util.element.get_aggregate(parent) -def get_classification(element): +def get_classification_identification(element): references = list(ifcopenshell.util.classification.get_references(element)) if references: if hasattr(references[0], "Identification"): - return "{}:{}".format(references[0].Identification, references[0].Name) - return "{}:{}".format(references[0].ItemReference, references[0].Name) + return references[0].Identification + return references[0].ItemReference + + +def get_classification_name(element): + references = list(ifcopenshell.util.classification.get_references(element)) + if references: + return references[0].Name def get_property(psets, pset_name, prop_name, decimals=None): @@ -239,9 +250,10 @@ config = { "Name", "ProjectName", "SiteName", - "Category", - "AuthorOrganizationName", - "AuthorDate", + "ClassificationIdentification", + "ClassificationName", + "OrganizationName", + "CreationDate", "ModelSoftware", "ModelProjectID", "ModelSiteID", @@ -259,9 +271,10 @@ config = { "keys": ["Name"], "headers": [ "Name", - "Category", - "AuthorOrganizationName", - "AuthorDate", + "ClassificationIdentification", + "ClassificationName", + "OrganizationName", + "CreationDate", "ModelSoftware", "ModelObject", "ModelID", @@ -277,10 +290,11 @@ config = { "headers": [ "Name", "Description", - "Category", + "ClassificationIdentification", + "ClassificationName", "LevelName", - "AuthorOrganizationName", - "AuthorDate", + "OrganizationName", + "CreationDate", "ModelSoftware", "ModelID", "AreaGross", @@ -293,7 +307,7 @@ config = { }, "Zones": { "keys": ["Name", "SpaceName"], - "headers": ["Name", "SpaceName", "AuthorOrganizationName", "AuthorDate", "ModelSoftware", "ModelID"], + "headers": ["Name", "SpaceName", "OrganizationName", "CreationDate", "ModelSoftware", "ModelID"], "colours": "prreee", "sort": [{"name": "Name", "order": "ASC"}], "get_category_elements": get_zones, @@ -304,9 +318,10 @@ config = { "headers": [ "Name", "Description", - "Category", - "AuthorOrganizationName", - "AuthorDate", + "ClassificationIdentification", + "ClassificationName", + "OrganizationName", + "CreationDate", "ModelSoftware", "ModelObject", "ModelID", @@ -329,8 +344,8 @@ config = { "TypeName", "SpaceName", "SystemName", - "AuthorOrganizationName", - "AuthorDate", + "OrganizationName", + "CreationDate", "ModelSoftware", "ModelObject", "ModelID", @@ -353,9 +368,10 @@ config = { "headers": [ "Name", "Description", - "Category", - "AuthorOrganizationName", - "AuthorDate", + "ClassificationIdentification", + "ClassificationName", + "OrganizationName", + "CreationDate", "ModelSoftware", "ModelID", ], From 0e5e1f787330f3455ee10cb58a83414cf37d7a9a Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 3 Oct 2023 21:26:00 +0200 Subject: [PATCH 09/22] Option to write Earth-Centered Earth-Fixed glTF --- cmake/CMakeLists.txt | 12 + src/ifcconvert/IfcConvert.cpp | 5 +- .../GeometrySerializer.h | 5 +- src/serializers/GltfSerializer.cpp | 301 +++++++++++++++++- src/serializers/GltfSerializer.h | 4 +- win/build-deps.cmd | 35 ++ 6 files changed, 350 insertions(+), 12 deletions(-) diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 05b9c9cfb5..32f3debde2 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -58,6 +58,7 @@ option(BUILD_PACKAGE "" OFF) option(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON) option(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF) option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON) +option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF) option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON) option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF) @@ -934,6 +935,17 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON) add_library(Serializers ${SERIALIZERS_FILES}) set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS" VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}") + message(STATUS "WITH_PROJ ${WITH_PROJ}") + + if (WITH_PROJ) + target_compile_definitions(Serializers PRIVATE "WITH_PROJ") + if (PROJ_STATIC) + target_compile_definitions(Serializers PRIVATE "PROJ_DLL=") + endif() + target_include_directories(Serializers PRIVATE ${PROJ_INCLUDE_DIR} ${SQLITE_INCLUDE_DIR}) + target_link_libraries(Serializers ${PROJ_LIBRARIES}) + endif() + target_link_libraries(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES} ${USD_LIBRARIES}) endif(BUILD_CONVERT OR BUILD_IFCPYTHON) diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp index 059aebd45c..f73b137921 100644 --- a/src/ifcconvert/IfcConvert.cpp +++ b/src/ifcconvert/IfcConvert.cpp @@ -461,6 +461,7 @@ int main(int argc, char** argv) { ("space-name-transform", po::value(), "Additional transform to the space labels in SVG") ("edge-arrows", "Adds arrow heads to edge segments to signify edge direction") + ("ecef", "Write glTF in Earth-Centered Earth-Fixed coordinates. Requires PROJ") ; po::options_description cmdline_options; @@ -527,6 +528,7 @@ int main(int argc, char** argv) { const bool generate_uvs = vmap.count("generate-uvs") != 0; const bool validate = vmap.count("validate") != 0; const bool edge_arrows = vmap.count("edge-arrows") != 0; + const bool write_gltf_ecef = vmap.count("ecef") != 0; const bool no_wire_intersection_check = vmap.count("no-wire-intersection-check") != 0; const bool no_wire_intersection_tolerance = vmap.count("no-wire-intersection-tolerance") != 0; const bool strict_tolerance = vmap.count("strict-tolerance") != 0; @@ -840,7 +842,8 @@ int main(int argc, char** argv) { settings.set(SerializerSettings::USE_MATERIAL_NAMES, use_material_names); settings.set(SerializerSettings::USE_ELEMENT_TYPES, use_element_types); settings.set(SerializerSettings::USE_ELEMENT_HIERARCHY, use_element_hierarchy); - settings.set_deflection_tolerance(deflection_tolerance); + settings.set(SerializerSettings::WRITE_GLTF_ECEF, write_gltf_ecef); + settings.set_deflection_tolerance(deflection_tolerance); settings.set_angular_tolerance(angular_tolerance); settings.precision = precision; diff --git a/src/ifcgeom_schema_agnostic/GeometrySerializer.h b/src/ifcgeom_schema_agnostic/GeometrySerializer.h index 58fb7c4d64..63b1507d97 100644 --- a/src/ifcgeom_schema_agnostic/GeometrySerializer.h +++ b/src/ifcgeom_schema_agnostic/GeometrySerializer.h @@ -49,8 +49,11 @@ public: /// Use Y UP . /// Applicable for OBJ output. USE_Y_UP = 1ULL << (IfcGeom::IteratorSettings::NUM_SETTINGS + 7ULL), + /// Write in ECEF coordinates. + /// Applicable to glTF output + WRITE_GLTF_ECEF = 1ULL << (IfcGeom::IteratorSettings::NUM_SETTINGS + 8ULL), /// Number of different setting flags. - NUM_SETTINGS = 7 + NUM_SETTINGS = 8 }; SerializerSettings() diff --git a/src/serializers/GltfSerializer.cpp b/src/serializers/GltfSerializer.cpp index 3c285b7c03..f0a4f62737 100644 --- a/src/serializers/GltfSerializer.cpp +++ b/src/serializers/GltfSerializer.cpp @@ -23,6 +23,10 @@ #include "../ifcparse/utils.h" +#ifdef WITH_PROJ +#include +#endif + #include static const uint32_t GLTF = 0x46546C67U; @@ -177,13 +181,23 @@ void GltfSerializer::write(const IfcGeom::TriangulationElement* o) { node_array_.push_back(json_["nodes"].size()); const std::vector& m = o->transformation().matrix().data(); - // nb: note that this contains the Y-UP transform as well. - const std::array matrix_flat = { - m[0], m[ 2], -m[ 1], 0, - m[3], m[ 5], -m[ 4], 0, - m[6], m[ 8], -m[ 7], 0, - m[9], m[11], -m[10], 1 - }; + std::array matrix_flat; + if (settings_.get(SerializerSettings::WRITE_GLTF_ECEF)) { + matrix_flat = { + m[0], m[1], m[2], 0, + m[3], m[4], m[5], 0, + m[6], m[7], m[8], 0, + m[9], m[10], m[11], 1 + }; + } else { + // nb: note that this contains the Y-UP transform as well. + matrix_flat = { + m[0], m[2], -m[1], 0, + m[3], m[5], -m[4], 0, + m[6], m[8], -m[7], 0, + m[9], m[11], -m[10], 1 + }; + } static const std::array identity_matrix = {1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1}; json node; @@ -317,6 +331,22 @@ void write_block(std::ostream& fs, It begin, It end) { } void GltfSerializer::finalize() { + if (north_rotation_) { + (*north_rotation_)["children"] = json::array(); + for (int i = 0; i < json_["nodes"].size(); ++i) { + (*north_rotation_)["children"].push_back(i); + } + json_["nodes"].push_back(*north_rotation_); + } + + if (ecef_transform_) { + (*ecef_transform_)["children"] = json::array(); + for (int i = 0; i < json_["nodes"].size(); ++i) { + (*ecef_transform_)["children"].push_back(i); + } + json_["nodes"].push_back(*ecef_transform_); + } + tmp_fstream1_.close(); tmp_fstream2_.close(); @@ -335,7 +365,11 @@ void GltfSerializer::finalize() { } json scene_0; - scene_0["nodes"] = node_array_; + if (north_rotation_ || ecef_transform_) { + scene_0["nodes"] = std::array{json_["nodes"].size() - 1}; + } else { + scene_0["nodes"] = node_array_; + } json_["scenes"].push_back(scene_0); //The generated glb file will contain the indices buffer followed by the vertices buffer. @@ -375,4 +409,255 @@ void GltfSerializer::finalize() { write_padding(fstream_, binary_length); } +namespace { + void normalize(std::array& v) { + auto l = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + + v[0] /= l; + v[1] /= l; + v[2] /= l; + } + + void cross(const std::array& v1, const std::array& v2, std::array& result) { + result[0] = v1[1] * v2[2] - v1[2] * v2[1]; + result[1] = v1[2] * v2[0] - v1[0] * v2[2]; + result[2] = v1[0] * v2[1] - v1[1] * v2[0]; + } + + void proj_log(void *, int, const char* c) { + Logger::Error("PROJ: " + std::string(c)); + } +} + +void GltfSerializer::setFile(IfcParse::IfcFile* f) { + if (!settings_.get(SerializerSettings::WRITE_GLTF_ECEF)) { + return; + } + + boost::optional crs_epsg; + boost::optional> crs_x_axis; + boost::optional> eastings_northings_elevation; + + aggregate_of_instance::ptr coordops; + try { + coordops = f->instances_by_type("IfcCoordinateOperation"); + } catch (IfcParse::IfcException&) { + // Ignored. Schema likely doesn't support IfcCoordinateOperation. + } + if (coordops) { + for (auto& coordop : *coordops) { + IfcUtil::IfcBaseClass* source_crs = *coordop->as()->get("SourceCRS"); + if (source_crs->declaration().is("IfcGeometricRepresentationContext")) { + IfcUtil::IfcBaseClass* target_crs = *coordop->as()->get("TargetCRS"); + auto name_attr = target_crs->as()->get("Name"); + if (coordop->declaration().is("IfcMapConversion")) { + + if (!name_attr->isNull()) { + std::string epsg_code = *name_attr; + crs_epsg = epsg_code; + + // @todo in which unit are these? + double eastings = *coordop->as()->get("Eastings"); + double northings = *coordop->as()->get("Northings"); + double height = *coordop->as()->get("OrthogonalHeight"); + height = 0.; + + eastings_northings_elevation = { { eastings, northings, height} }; + + auto xaxis_attr = coordop->as()->get("XAxisAbscissa"); + auto yaxis_attr = coordop->as()->get("XAxisOrdinate"); + if (!xaxis_attr->isNull() && !yaxis_attr->isNull()) { + double xaxis = *xaxis_attr; + double yaxis = *yaxis_attr; + + crs_x_axis = { { xaxis, yaxis, 0. } }; + } + } + } + } + } + } + + if (!crs_epsg) { + auto sites = f->instances_by_type("IfcSite"); + + if (sites && sites->size() == 1) { + auto lat_attr = (*sites->begin())->as()->get("RefLatitude"); + auto lon_attr = (*sites->begin())->as()->get("RefLongitude"); + + if (!lat_attr->isNull() && !lon_attr->isNull()) { + std::vector lat_dms = *lat_attr; + std::vector lon_dms = *lon_attr; + + auto to_decimal = [](const std::vector& dms) { + double val = dms[0] + dms[1] / 60. + dms[2] / 3600.; + if (dms.size() == 4) { + val += dms[3] / 3600.e6; + } + return val; + }; + + auto lat = to_decimal(lat_dms); + auto lon = to_decimal(lon_dms); + double elev = 0.; + + /* + auto elev_attr = (*sites->begin())->as()->get("RefElevation"); + if (!elev_attr->isNull()) { + elev = *elev_attr; + } + */ + + crs_epsg.reset("EPSG:4326"); + eastings_northings_elevation = { { lat, lon, elev } }; + } + } + } + + auto contexts = f->instances_by_type_excl_subtypes("IfcGeometricRepresentationContext"); + + if (contexts && contexts->size() > 0) { + auto context = (*contexts->begin())->as(); + auto north_attr = context->get("TrueNorth"); + if (!north_attr->isNull()) { + IfcUtil::IfcBaseClass* north = *north_attr; + if (north->declaration().is("IfcDirection")) { + std::vector ratios = *north->as()->get("DirectionRatios"); + crs_x_axis = { { ratios[1], -ratios[0], 0. } }; + } + } + } + +#ifdef WITH_PROJ + + if (crs_epsg) { + PJ_COORD wgs84_point; + + auto C = proj_context_create(); + proj_log_func(C, nullptr, proj_log); + + // @todo a bit ugly we assume a proj.db in current working directory. + // a very simplistic but at least portable solution. + proj_context_set_database_path(C, "proj.db", nullptr, nullptr); + + if (*crs_epsg == "EPSG:4326") { + wgs84_point = proj_coord( + (*eastings_northings_elevation)[0], + (*eastings_northings_elevation)[1], + (*eastings_northings_elevation)[2], + 0); + } else { + // @todo a bit ugly we assume a proj.db in current working directory. + // a very simplistic but at least portable solution. + proj_context_set_database_path(C, "proj.db", nullptr, nullptr); + + auto P = proj_create_crs_to_crs( + C, crs_epsg->c_str(), "EPSG:4326", + NULL); + + if (!P) { + Logger::Error("Failed to create PROJ transformation object"); + return; + } + + auto a = proj_coord( + (*eastings_northings_elevation)[0], + (*eastings_northings_elevation)[1], + (*eastings_northings_elevation)[2], + 0); + + wgs84_point = proj_trans(P, PJ_FWD, a); + + Logger::Notice("Calculated latitude: " + std::to_string(wgs84_point.lp.lam) + " longitude: " + std::to_string(wgs84_point.lp.phi)); + } + + std::swap(wgs84_point.lp.phi, wgs84_point.lp.lam); + + const char *input_crs = "+proj=latlong +datum=WGS84"; + const char *output_crs = "+proj=geocent +datum=WGS84 +units=m"; + + // Create a transformation object + PJ *transform = proj_create_crs_to_crs(C, input_crs, output_crs, NULL); + + // Perform the transformation + PJ_COORD output_point = proj_trans(transform, PJ_FWD, wgs84_point); + + // Extract the ECEF coordinates + double x = output_point.xyz.x; + double y = output_point.xyz.y; + double z = output_point.xyz.z; + + const char *ellipsoid_def = "WGS84"; + + // Create a CRS object representing the ellipsoid + PJ *ellipsoid_crs = proj_create(C, ellipsoid_def); + + if (!ellipsoid_crs) { + Logger::Error("Failed to create ellipsoid CRS"); + return; + } + + auto ellipse = proj_get_ellipsoid(C, ellipsoid_crs); + + + int _; + double semi_major, semi_minor, __; + proj_ellipsoid_get_parameters(C, ellipse, &semi_major, &semi_minor, &_, &__); + + std::array dxyz = { { + x * (1. / (semi_major * semi_major)), + y * (1. / (semi_major * semi_major)), + z * (1. / (semi_minor * semi_minor)) + } }; + normalize(dxyz); + + // Oblate spheroid, so X and Y axis are equal, so rotation around Z yields east axis. + std::array east_xyz = { { + -y, + x, + 0. + } }; + normalize(east_xyz); + + std::array north; + cross(dxyz, east_xyz, north); + + std::array matrix = { + east_xyz[0], east_xyz[1], east_xyz[2], 0, + north[0], north[1], north[2], 0., + dxyz[0], dxyz[1], dxyz[2], 0, + 0,0,0,1 + }; + + ecef_transform_ = json::object({ + {"matrix", matrix } + }); + + json_["extensions"]["CESIUM_RTC"]["center"] = std::array{ {x, y, z} }; + json_["extensionsUsed"].push_back("CESIUM_RTC"); + + // Clean up + proj_destroy(ellipsoid_crs); + proj_destroy(transform); + proj_context_destroy(C); + } + + if (crs_x_axis) { + normalize(*crs_x_axis); + + auto phi = std::atan2((*crs_x_axis)[1], (*crs_x_axis)[0]); + + north_rotation_ = json::object({ + {"matrix", std::array{ + +std::cos(-phi), -std::sin(-phi), 0., 0., + +std::sin(-phi), +std::cos(-phi), 0., 0., + 0., 0., 1., 0., + 0., 0., 0., 1. + }} + }); + } +#endif +} + + #endif diff --git a/src/serializers/GltfSerializer.h b/src/serializers/GltfSerializer.h index 7450fa2afb..5f7265730f 100644 --- a/src/serializers/GltfSerializer.h +++ b/src/serializers/GltfSerializer.h @@ -36,9 +36,9 @@ private: std::ofstream fstream_, tmp_fstream1_, tmp_fstream2_; std::map materials_, meshes_; json json_, node_array_; + boost::optional ecef_transform_, north_rotation_; int bufferViewId; - int writeMaterial(const IfcGeom::Material& style); public: GltfSerializer(const std::string& filename, const SerializerSettings& settings); @@ -50,7 +50,7 @@ public: void finalize(); bool isTesselated() const { return true; } void setUnitNameAndMagnitude(const std::string& /*name*/, float /*magnitude*/) {} - void setFile(IfcParse::IfcFile*) {} + void setFile(IfcParse::IfcFile*); }; #endif diff --git a/win/build-deps.cmd b/win/build-deps.cmd index 4c7d1aa591..238d3df0d1 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -193,6 +193,41 @@ IF "%IFCOS_INSTALL_PYTHON%"=="TRUE" ( echo PYTHONHOME=%PYTHONHOME%>>"%~dp0\%BUILD_DEPS_CACHE_PATH%" ) + +:proj + +set DEPENDENCY_NAME=sqlite3 +md %INSTALL_DIR%\sqlite3\lib %INSTALL_DIR%\sqlite3\bin %INSTALL_DIR%\sqlite3\include +call :DownloadFile https://www.sqlite.org/2023/sqlite-amalgamation-3430100.zip "%DEPS_DIR%" sqlite-amalgamation-3430100.zip +IF NOT %ERRORLEVEL%==0 GOTO :Error +call :ExtractArchive sqlite-amalgamation-3430100.zip "%DEPS_DIR%" "%DEPS_DIR%\sqlite-amalgamation-3430100" +IF NOT %ERRORLEVEL%==0 GOTO :Error +pushd "%DEPS_DIR%\sqlite-amalgamation-3430100" +cl /c sqlite3.c +lib /OUT:%INSTALL_DIR%\sqlite3\lib\sqlite3.lib sqlite3.obj +cl sqlite3.c shell.c /link /out:%INSTALL_DIR%\sqlite3\bin\sqlite3.exe +copy sqlite3.h %INSTALL_DIR%\sqlite3\include +popd + +set DEPENDENCY_NAME=proj +set DEPENDENCY_DIR=%DEPS_DIR%\proj-9.2.1 +call :DownloadFile https://download.osgeo.org/proj/proj-9.2.1.zip "%DEPS_DIR%" proj-9.2.1.zip +IF NOT %ERRORLEVEL%==0 GOTO :Error +call :ExtractArchive proj-9.2.1.zip "%DEPS_DIR%" "%DEPS_DIR%\proj-9.2.1" +IF NOT %ERRORLEVEL%==0 GOTO :Error +cd "%DEPENDENCY_DIR%" +call :RunCMake -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%\proj-9.2.1" ^ + -DSQLITE3_INCLUDE_DIR=%INSTALL_DIR%\sqlite3\include -DSQLITE3_LIBRARY=%INSTALL_DIR%\sqlite3\lib\sqlite3.lib ^ + -DENABLE_TIFF=Off -DENABLE_CURL=Off -DBUILD_PROJSYNC=Off ^ + -DBUILD_SHARED_LIBS=Off +IF NOT %ERRORLEVEL%==0 GOTO :Error +call :BuildSolution "%DEPENDENCY_DIR%\%BUILD_DIR%\PROJ.sln" %BUILD_CFG% +IF NOT %ERRORLEVEL%==0 GOTO :Error +call :InstallCMakeProject "%DEPENDENCY_DIR%\%BUILD_DIR%" %BUILD_CFG% +IF NOT %ERRORLEVEL%==0 GOTO :Error + +goto :Successful + :mpir set DEPENDENCY_NAME=mpir set DEPENDENCY_DIR=%DEPS_DIR%\mpir From 4fe459dab6993d04f38c2e75f5bebe98e59701eb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 3 Oct 2023 10:59:09 +0500 Subject: [PATCH 10/22] Fixed typo --- src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py index 7c6428a4eb..ab54ef8d24 100644 --- a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py +++ b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py @@ -99,7 +99,7 @@ class DeleteSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} def draw(self, context): - self.layout.label(text="WARNING. The graph will be removed permamently.") + self.layout.label(text="WARNING. The graph will be removed permanently.") def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) From 0d7967ff622eb88cdcb19b7b4288b9523f4ab057 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 4 Oct 2023 15:13:57 +0500 Subject: [PATCH 11/22] .reg file for #3778 Forgot to commit the .reg file... --- src/blenderbim/blenderbim/bim/operator.py | 2 +- .../blenderbim/libs/desktop/windows_bbim_association.reg | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.reg diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 7b491e58be..e65ad34b49 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -237,7 +237,7 @@ class FileAssociate(bpy.types.Operator): # 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]) + subprocess.run(["cmd", "/c", "call", 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 diff --git a/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.reg b/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.reg new file mode 100644 index 0000000000..33451e8dc7 --- /dev/null +++ b/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.reg @@ -0,0 +1,4 @@ +Windows Registry Editor Version 5.00 + +[-HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ifc] +[-] \ No newline at end of file From dff70f2608fd7af03280a2fa504d0c5df70dd192 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 4 Oct 2023 18:14:43 +0500 Subject: [PATCH 12/22] bim.flip_object #3803 Added new operator bim.flip_object that flips the object by flipping two of its axes and keeps the original placement. Currently it can be found either from F3 search or from Shift-F hotkey for IfcBeam and IfcColumn in BIM tools. --- .../bim/module/geometry/__init__.py | 1 + .../bim/module/geometry/operator.py | 16 +++++++++++++ .../blenderbim/bim/module/model/opening.py | 13 +--------- .../blenderbim/bim/module/model/workspace.py | 3 +++ src/blenderbim/blenderbim/tool/blender.py | 2 ++ src/blenderbim/blenderbim/tool/geometry.py | 24 ++++++++++++++++++- 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py index 29503856bc..1b64ef180e 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py @@ -23,6 +23,7 @@ classes = ( operator.AddRepresentation, operator.CopyRepresentation, operator.EditObjectPlacement, + operator.FlipObject, operator.GetRepresentationIfcParameters, operator.OverrideDelete, operator.OverrideDuplicateMove, diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 157366aeb7..ef8c3e865c 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -1723,3 +1723,19 @@ class OverrideModeSetObject(bpy.types.Operator): if self.edited_objs: return context.window_manager.invoke_props_dialog(self) return self.execute(context) + + +class FlipObject(bpy.types.Operator): + bl_idname = "bim.flip_object" + bl_label = "Flip Object" + bl_description = "Flip object's local axes, keep the position" + bl_options = {"REGISTER", "UNDO"} + + flip_local_axes: bpy.props.EnumProperty( + name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY" + ) + + def execute(self, context): + for obj in context.selected_objects: + tool.Geometry.flip_object(obj, self.flip_local_axes) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index 9b841cfe2b..32f42340e9 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -393,18 +393,7 @@ class FlipFill(bpy.types.Operator, tool.Ifc.Operator): element = tool.Ifc.get_entity(obj) if not element or not element.FillsVoids: continue - - flip_matrix = Matrix.Rotation(pi, 4, "Z") - - bottom_left = obj.matrix_world @ Vector(obj.bound_box[0]) - top_right = obj.matrix_world @ Vector(obj.bound_box[6]) - center = obj.matrix_world.translation.copy() - center_offset = center - bottom_left - flipped_center = top_right - center_offset - - obj.matrix_world = obj.matrix_world @ flip_matrix - obj.matrix_world.translation.xy = flipped_center.xy - bpy.context.view_layer.update() + tool.Geometry.flip_object(obj, "XY") return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 6af2600dee..0f8d59b683 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -358,6 +358,7 @@ class BimToolUI: cls.layout.operator("bim.mep_connect_elements") else: add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "") + add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__) 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__) @@ -668,6 +669,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.flip_wall() elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"): bpy.ops.bim.flip_fill() + elif self.active_class in ("IfcBeam", "IfcColumn"): + bpy.ops.bim.flip_object(flip_local_axes="XZ") elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"): bpy.ops.bim.fit_flow_segments() diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index d544a67787..73e245bdc9 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -343,6 +343,8 @@ class Blender: "max_y": bound_box[6][1], "min_z": bound_box[0][2], "max_z": bound_box[6][2], + "min_point": Vector(bound_box[0]), + "max_point": Vector(bound_box[6]), "center": (Vector(bound_box[6]) + Vector(bound_box[0])) / 2, } return bbox_dict diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 8e624d61f0..ce33fbcd7d 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -28,7 +28,7 @@ import blenderbim.core.style import blenderbim.core.spatial import blenderbim.tool as tool import blenderbim.bim.import_ifc -from math import radians +from math import radians, pi from mathutils import Vector, Matrix from blenderbim.bim.ifc import IfcStore @@ -641,3 +641,25 @@ class Geometry(blenderbim.core.tool.Geometry): @classmethod def get_model_representations(cls): return tool.Ifc.get().by_type("IfcShapeRepresentation") + + @classmethod + def flip_object(cls, obj, flip_local_axes): + assert len(flip_local_axes) == 2, "flip_local_axes must be two axes to flip" + rotation_axis = next(i for i in "XYZ" if i not in flip_local_axes) + rotation_axis_i = "XYZ".index(rotation_axis) + + bb_data = tool.Blender.get_object_bounding_box(obj) + # min max points of rotated plane of origin based bounding box + min_point = Vector([min(i, 0) for i in bb_data["min_point"]]) + max_point = Vector([max(i, 0) for i in bb_data["max_point"]]) + # keep it in rotated plane only + max_point[rotation_axis_i] = min_point[rotation_axis_i] + + # to compensate for flipped two axes + # we adjust new max point to match previous min point (or vice versa) + original_min_point = obj.matrix_world @ min_point + obj.matrix_world = obj.matrix_world @ Matrix.Rotation(pi, 4, rotation_axis) + new_max_point = obj.matrix_world @ max_point + obj.matrix_world.translation += original_min_point - new_max_point + + bpy.context.view_layer.update() From ef8f4df4ab67482027d4182cad55bd760dc42785 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 5 Oct 2023 09:56:51 +1100 Subject: [PATCH 13/22] Fix #3841. Bump IOS. --- src/blenderbim/Makefile | 2 +- src/ifcopenshell-python/Makefile | 2 +- .../docs/ifcconvert/installation.rst | 10 ++-- .../docs/ifcopenshell-python/installation.rst | 56 +++++++++---------- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 08f291afa1..f8ea60a639 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -169,7 +169,7 @@ endif cp -r blenderbim/* dist/blenderbim/ # Provides IfcOpenShell Python functionality - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9cc1f5f-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f0e03c7-$(PLATFORM)64.zip cd dist/working && unzip ifcopenshell-python* cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/ diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 05442dacbb..105f0faf28 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -103,7 +103,7 @@ endif mkdir -p dist/ifcopenshell cp -r ifcopenshell/* dist/ifcopenshell/ - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9cc1f5f-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f0e03c7-$(PLATFORM)64.zip cd dist/working && unzip ifcopenshell-python* cp -r dist/working/ifcopenshell/ifcopenshell_wrapper.py dist/ifcopenshell/ ifeq ($(PLATFORM), win) diff --git a/src/ifcopenshell-python/docs/ifcconvert/installation.rst b/src/ifcopenshell-python/docs/ifcconvert/installation.rst index da8fecf14d..3ff4cc9f0f 100644 --- a/src/ifcopenshell-python/docs/ifcconvert/installation.rst +++ b/src/ifcopenshell-python/docs/ifcconvert/installation.rst @@ -20,11 +20,11 @@ Pre-built packages | build-linux64_ | build-win32_ | build-win64_ | build-macos64_ | build-macosm164_ | +----------------+----------------+----------------+----------------+------------------+ -.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-linux64.zip -.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-win32.zip -.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-win64.zip -.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-macos64.zip -.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-macosm164.zip +.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-linux64.zip +.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-win32.zip +.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-win64.zip +.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-macos64.zip +.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-macosm164.zip 2. Unzip the downloaded file and run IfcConvert using the command line. diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst index 8dc7811f1d..f7ea78d3c6 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst @@ -41,34 +41,34 @@ changes in the IfcOpenShell C++ core. | Python 3.11 | py311-linux64_ | py311-win32_ | py311-win64_ | N/A | py311-macosm164_ | +-------------+----------------+----------------+----------------+----------------+------------------+ -.. _py36-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-9cc1f5f-linux64.zip -.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-linux64.zip -.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-linux64.zip -.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-linux64.zip -.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-linux64.zip -.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9cc1f5f-linux64.zip -.. _py36-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-9cc1f5f-win32.zip -.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-win32.zip -.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-win32.zip -.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-win32.zip -.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-win32.zip -.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9cc1f5f-win32.zip -.. _py36-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-9cc1f5f-win64.zip -.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-win64.zip -.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-win64.zip -.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-win64.zip -.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-win64.zip -.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9cc1f5f-win64.zip -.. _py36-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-9cc1f5f-macos64.zip -.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-macos64.zip -.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-macos64.zip -.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-macos64.zip -.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-macos64.zip -.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-macosm164.zip -.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-macosm164.zip -.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-macosm164.zip -.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-macosm164.zip -.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9cc1f5f-macosm164.zip +.. _py36-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-f0e03c7-linux64.zip +.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-linux64.zip +.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-linux64.zip +.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-linux64.zip +.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-linux64.zip +.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f0e03c7-linux64.zip +.. _py36-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-f0e03c7-win32.zip +.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-win32.zip +.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-win32.zip +.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-win32.zip +.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-win32.zip +.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f0e03c7-win32.zip +.. _py36-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-f0e03c7-win64.zip +.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-win64.zip +.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-win64.zip +.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-win64.zip +.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-win64.zip +.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f0e03c7-win64.zip +.. _py36-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-f0e03c7-macos64.zip +.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-macos64.zip +.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-macos64.zip +.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-macos64.zip +.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-macos64.zip +.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-macosm164.zip +.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-macosm164.zip +.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-macosm164.zip +.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-macosm164.zip +.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f0e03c7-macosm164.zip 2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your Python path. If you're not sure where your Python path is, run the following From 6660bf7618476d0366e7b461927329f51bf755a2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 5 Oct 2023 15:50:49 +1100 Subject: [PATCH 14/22] Document geolocation util and add support for ePSet IFC2X3 fallbacks. --- .../ifcopenshell/util/geolocation.py | 427 ++++++++++++++++-- 1 file changed, 399 insertions(+), 28 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py index d4737ac016..7128630c2f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py @@ -23,11 +23,34 @@ import ifcopenshell.util.unit def dms2dd(degrees, minutes, seconds, ms=0): + """Convert degrees, minutes, and (milli)seconds to decimal degrees + + :param degrees: The degrees component + :type degrees: int + :param minutes: The minutes component + :type minutes: int + :param seconds: The seconds component + :type seconds: int + :param ms: The milliseconds component + :type ms: int + :return: The angle in decimal degrees. + :rtype: float + """ dd = float(degrees) + float(minutes) / 60.0 + float(seconds) / (3600.0) + float(ms / 3600000000.0) return dd def dd2dms(dd, use_ms=False): + """Convert decimal degrees to degrees, minutes, and (milli)seconds format + + :param dd: The decimal degrees + :type dd: float + :param use_ms: True if to include milliseconds and false otherwise. Defaults to false. + :type use_ms: bool + :return: The angle in a tuple of either 3 or 4 values, being degrees, + minutes, seconds, and optionally milliseconds. + :rtype: tuple[float] + """ dd = float(dd) sign = 1 if dd >= 0 else -1 dd = abs(dd) @@ -43,6 +66,43 @@ def dd2dms(dd, use_ms=False): def xyz2enh(x, y, z, eastings, northings, orthogonal_height, x_axis_abscissa, x_axis_ordinate, scale=None): + """Manually convert local XYZ coordinates to map eastings, northings, and height + + This function is for advanced users as it allows you to specify your own + helmert transformation parameters (i.e. those typically stored in + IfcMapConversion). This manual approach is useful for tests or in case your + are setting your helmert transformations in non-standard locations, or if + you are applying your own temporary false origin (such as when federating + models for digital twins of large cities). + + No unit conversion is performed. + + For most scenarios you should use ``auto_xyz2enh`` instead. + + :param x: The X local engineering coordinate. + :type x: float + :param y: The Y local engineering coordinate. + :type y: float + :param z: The Z local engineering coordinate. + :type z: float + :param eastings: The eastings offset to apply. + :type eastings: float + :param northings: The northings offset to apply. + :type northings: float + :param orthogonal_height: The orthogonal height offset to apply. + :type orthogonal_height: float + :param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the + 2D vector that points to the local X axis when in map coordinates. + :type x_axis_abscissa: float + :param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the + 2D vector that points to the local X axis when in map coordinates. + :type x_axis_ordinate: float + :param scale: The combined scale factor to convert from local coordinates + to map coordinates. + :type scale: float + :return: A tuple of three ordinates representing the easting, northing and height. + :rtype: tuple[float] + """ if scale is None: scale = 1.0 rotation = math.atan2(x_axis_ordinate, x_axis_abscissa) @@ -76,20 +136,60 @@ def xyz2enh_ifc4x3( def auto_xyz2enh(ifc_file, x, y, z): + """Convert from local XYZ coordinates to global map coordinate eastings, northings, and heights + + The necessary georeferencing map conversion is automatically detected from + the IFC map conversion parameters present in the IFC model. If no map + conversion is present, then the Z coordinate is returned unchanged. + + For IFC2X3, the map conversion is detected from the IfcProject's + ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC": + https://www.buildingsmart.org/standards/bsi-standards/standards-library/ + + :param ifc_file: The IFC file + :type ifc_file: ifcopenshell.file.file + :param x: The X local engineering coordinate provided in project length units. + :type x: float + :param y: The Y local engineering coordinate provided in project length units. + :type y: float + :param z: The Z local engineering coordinate provided in project length units. + :type z: float + :return: The global map coordinate eastings, northings, and height in map units. + :rtype: tuple[float] + """ + conversion = None try: conversion = ifc_file.by_type("IfcMapConversion") except: - return (x, y, z) - if not conversion: - return (x, y, z) - conversion = conversion[0] - e = conversion.Eastings or 0 - n = conversion.Northings or 0 - h = conversion.OrthogonalHeight or 0 - xaa = conversion.XAxisAbscissa or 0 - xao = conversion.XAxisOrdinate or 0 - scale = conversion.Scale or 0 - map_unit = conversion.TargetCRS.MapUnit + pass + + if conversion: + conversion = conversion[0] + e = conversion.Eastings or 0 + n = conversion.Northings or 0 + h = conversion.OrthogonalHeight or 0 + xaa = conversion.XAxisAbscissa or 0 + xao = conversion.XAxisOrdinate or 0 + scale = conversion.Scale or 1 + map_unit = conversion.TargetCRS.MapUnit + else: + project = ifc_file.by_type("IfcProject")[0] + conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion") + if not conversion: + return (x, y, z) + + e = conversion.get("Eastings", None) or 0 + n = conversion.get("Northings", None) or 0 + h = conversion.get("OrthogonalHeight", None) or 0 + xaa = conversion.get("XAxisAbscissa", None) or 0 + xao = conversion.get("XAxisOrdinate", None) or 0 + scale = conversion.get("Scale", None) or 1 + map_unit = None + + if not xaa and not xao: + xaa = 1.0 + xao = 0.0 + if map_unit: # Warning! This definition has changed in IFC4X3 such that map_unit no # longer affects unit conversion, only the Scale attribute affects unit @@ -103,6 +203,74 @@ def auto_xyz2enh(ifc_file, x, y, z): return xyz2enh(x, y, z, e, n, h, xaa, xao, scale) +def auto_enh2xyz(ifc_file, easting, northing, height): + """Convert from global map coordinate eastings, northings, and heights to local XYZ coordinates + + The necessary georeferencing map conversion is automatically detected from + the IFC map conversion parameters present in the IFC model. If no map + conversion is present, then the Z coordinate is returned unchanged. + + For IFC2X3, the map conversion is detected from the IfcProject's + ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC": + https://www.buildingsmart.org/standards/bsi-standards/standards-library/ + + :param ifc_file: The IFC file + :type ifc_file: ifcopenshell.file.file + :param easting: The global easting map coordinate provided in map units. + :type easting: float + :param northing: The global northing map coordinate provided in map units. + :type northing: float + :param height: The global height map coordinate provided in map units. + :type height: float + :return: The local engineering XYZ coordinates in project length units. + :rtype: tuple[float] + """ + conversion = None + try: + conversion = ifc_file.by_type("IfcMapConversion") + except: + pass + + if conversion: + conversion = conversion[0] + e = conversion.Eastings or 0 + n = conversion.Northings or 0 + h = conversion.OrthogonalHeight or 0 + xaa = conversion.XAxisAbscissa or 0 + xao = conversion.XAxisOrdinate or 0 + scale = conversion.Scale or 1 + map_unit = conversion.TargetCRS.MapUnit + else: + project = ifc_file.by_type("IfcProject")[0] + conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion") + if not conversion: + return (easting, northing, height) + + e = conversion.get("Eastings", None) or 0 + n = conversion.get("Northings", None) or 0 + h = conversion.get("OrthogonalHeight", None) or 0 + xaa = conversion.get("XAxisAbscissa", None) or 0 + xao = conversion.get("XAxisOrdinate", None) or 0 + scale = conversion.get("Scale", None) or 1 + map_unit = None + + if not xaa and not xao: + xaa = 1.0 + xao = 0.0 + + if map_unit: + # Warning! This definition has changed in IFC4X3 such that map_unit no + # longer affects unit conversion, only the Scale attribute affects unit + # conversion. TODO: consolidate once IFC4X3 confirmed. + project_unit = ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT") + map_prefix = getattr(map_unit, "Prefix", None) + project_prefix = getattr(project_unit, "Prefix", None) + e = ifcopenshell.util.unit.convert(e, map_prefix, map_unit.Name, project_prefix, project_unit.Name) + n = ifcopenshell.util.unit.convert(n, map_prefix, map_unit.Name, project_prefix, project_unit.Name) + h = ifcopenshell.util.unit.convert(h, map_prefix, map_unit.Name, project_prefix, project_unit.Name) + return enh2xyz(easting, northing, height, e, n, h, xaa, xao, scale) + + def auto_z2e(ifc_file, z): """Convert a Z coordinate to an elevation using model georeferencing data @@ -110,6 +278,10 @@ def auto_z2e(ifc_file, z): the IFC map conversion parameters present in the IFC model. If no map conversion is present, then the Z coordinate is returned unchanged. + For IFC2X3, the map conversion is detected from the IfcProject's + ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC": + https://www.buildingsmart.org/standards/bsi-standards/standards-library/ + :param ifc_file: The IFC file :type ifc_file: ifcopenshell.file.file :param z: The Z local engineering coordinate provided in project length units. @@ -117,15 +289,25 @@ def auto_z2e(ifc_file, z): :return: The elevation in project length units. :rtype: float """ + conversion = None try: conversion = ifc_file.by_type("IfcMapConversion") except: - return z - if not conversion or not conversion[0].OrthogonalHeight: - return z - conversion = conversion[0] - h = conversion.OrthogonalHeight - map_unit = conversion.TargetCRS.MapUnit + pass + + if conversion and not conversion[0].OrthogonalHeight: + conversion = conversion[0] + h = conversion.OrthogonalHeight + map_unit = conversion.TargetCRS.MapUnit + else: + project = ifc_file.by_type("IfcProject")[0] + conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion") + if not conversion: + return z + + h = conversion.get("OrthogonalHeight", None) or 0 + map_unit = None + if map_unit: # Warning! This definition has changed in IFC4X3 such that map_unit no # longer affects unit conversion, only the Scale attribute affects unit @@ -142,10 +324,61 @@ def auto_z2e(ifc_file, z): def z2e(z, h): + """Manually convert a Z coordinate to an elevation + + This function is for advanced users as it allows you to specify your own + orthogonal height offset. + + For most scenarios you should use ``auto_z2e`` instead. + + :param z: The Z local engineering coordinate provided in project length units. + :type z: float + :param h: The orthogonal height offset in project length units. + :type h: float + :return: The elevation in project length units. + :rtype: float + """ return z + h def enh2xyz(e, n, h, eastings, northings, orthogonal_height, x_axis_abscissa, x_axis_ordinate, scale=None): + """Manually convert map eastings, northings, and height to local XYZ coordinates + + This function is for advanced users as it allows you to specify your own + helmert transformation parameters (i.e. those typically stored in + IfcMapConversion). This manual approach is useful for tests or in case your + are setting your helmert transformations in non-standard locations, or if + you are applying your own temporary false origin (such as when federating + models for digital twins of large cities). + + No unit conversion is performed. + + For most scenarios you should use ``auto_enh2xyz`` instead. + + :param e: The global easting map coordinate. + :type e: float + :param n: The global northing map coordinate. + :type n: float + :param h: The global height map coordinate. + :type h: float + :param eastings: The eastings offset to apply. + :type eastings: float + :param northings: The northings offset to apply. + :type northings: float + :param orthogonal_height: The orthogonal height offset to apply. + :type orthogonal_height: float + :param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the + 2D vector that points to the local X axis when in map coordinates. + :type x_axis_abscissa: float + :param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the + 2D vector that points to the local X axis when in map coordinates. + :type x_axis_ordinate: float + :param scale: The combined scale factor to convert from local coordinates + to map coordinates. + :type scale: float + :return: A tuple of three ordinates representing XYZ. + :rtype: tuple[float] + """ if scale is None: scale = 1.0 rotation = math.atan2(x_axis_ordinate, x_axis_abscissa) @@ -158,6 +391,37 @@ def enh2xyz(e, n, h, eastings, northings, orthogonal_height, x_axis_abscissa, x_ def local2global(matrix, eastings, northings, orthogonal_height, x_axis_abscissa, x_axis_ordinate, scale=None): + """Manually convert a 4x4 matrix from local to global coordinates + + This function is for advanced users as it allows you to specify your own + helmert transformation parameters (i.e. those typically stored in + IfcMapConversion). This manual approach is useful for tests or in case your + are setting your helmert transformations in non-standard locations, or if + you are applying your own temporary false origin (such as when federating + models for digital twins of large cities). + + No unit conversion is performed. + + :param matrix: A 4x4 numpy matrix representing local coordinates. + :type matrix: np.array + :param eastings: The eastings offset to apply. + :type eastings: float + :param northings: The northings offset to apply. + :type northings: float + :param orthogonal_height: The orthogonal height offset to apply. + :type orthogonal_height: float + :param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the + 2D vector that points to the local X axis when in map coordinates. + :type x_axis_abscissa: float + :param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the + 2D vector that points to the local X axis when in map coordinates. + :type x_axis_ordinate: float + :param scale: The combined scale factor to convert from local coordinates + to map coordinates. + :type scale: float + :return: A numpy 4x4 array matrix representing global coordinates. + :rtype: np.array + """ if scale is None: scale = 1.0 x = np.array([x_axis_abscissa, x_axis_ordinate, 0]) @@ -221,6 +485,37 @@ def local2global_ifc4x3( def global2local(matrix, eastings, northings, orthogonal_height, x_axis_abscissa, x_axis_ordinate, scale=None): + """Manually convert a 4x4 matrix from global to local coordinates + + This function is for advanced users as it allows you to specify your own + helmert transformation parameters (i.e. those typically stored in + IfcMapConversion). This manual approach is useful for tests or in case your + are setting your helmert transformations in non-standard locations, or if + you are applying your own temporary false origin (such as when federating + models for digital twins of large cities). + + No unit conversion is performed. + + :param matrix: A 4x4 numpy matrix representing global coordinates. + :type matrix: np.array + :param eastings: The eastings offset to apply. + :type eastings: float + :param northings: The northings offset to apply. + :type northings: float + :param orthogonal_height: The orthogonal height offset to apply. + :type orthogonal_height: float + :param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the + 2D vector that points to the local X axis when in map coordinates. + :type x_axis_abscissa: float + :param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the + 2D vector that points to the local X axis when in map coordinates. + :type x_axis_ordinate: float + :param scale: The combined scale factor to convert from local coordinates + to map coordinates. + :type scale: float + :return: A numpy 4x4 array matrix representing local coordinates. + :rtype: np.array + """ if scale is None: scale = 1.0 x = np.array([x_axis_abscissa, x_axis_ordinate, 0]) @@ -245,13 +540,31 @@ def global2local(matrix, eastings, northings, orthogonal_height, x_axis_abscissa ) -# Used for converting the X and Y vectors of the X Axis in IFC grid north geolocation def xaxis2angle(x, y): + """Converts X axis abscissa and ordinates to an angle in decimal degrees + + :param x: The X axis abscissa + :type x: float + :param y: The X axis ordinate + :type y: float + :return: The equivalent angle in decimal degrees from the X axis + :rtype: float + """ return math.degrees(math.atan2(y, x)) * -1 -# Used for converting the X and Y vectors of the Y Axis in IFC true north geolocation def yaxis2angle(x, y): + """Converts Y axis abscissa and ordinates to an angle in decimal degrees + + The Y axis abscissa and ordinate is how IFC stores true north. + + :param x: The Y axis abscissa + :type x: float + :param y: The Y axis ordinate + :type y: float + :return: The equivalent angle in decimal degrees from the Y axis + :rtype: float + """ angle = math.degrees(math.atan2(y, x)) - 90 if angle < -180: angle += 360 @@ -261,36 +574,94 @@ def yaxis2angle(x, y): def get_grid_north(ifc_file): + """Get an angle pointing to map grid north + + Anticlockwise is positive. + + The necessary georeferencing map conversion is automatically detected from + the IFC map conversion parameters present in the IFC model. If no map + conversion is present, then the Z coordinate is returned unchanged. + + For IFC2X3, the map conversion is detected from the IfcProject's + ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC": + https://www.buildingsmart.org/standards/bsi-standards/standards-library/ + + :param ifc_file: The IFC file + :type ifc_file: ifcopenshell.file.file + :return: An angle to grid north in decimal degrees + :rtype: float + """ + conversion = None try: conversion = ifc_file.by_type("IfcMapConversion")[0] except: - return 0 - if not conversion.XAxisAbscissa or not conversion.XAxisOrdinate: - return 0 - return xaxis2angle(conversion.XAxisAbscissa, conversion.XAxisOrdinate) + pass + if conversion: + if not conversion.XAxisAbscissa or not conversion.XAxisOrdinate: + return 0 + xaa = conversion.XAxisAbscissa + xao = conversion.XAxisOrdinate + else: + project = ifc_file.by_type("IfcProject")[0] + conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion") + if not conversion: + return 0 + xaa = conversion.get("XAxisAbscissa", None) or 0 + xao = conversion.get("XAxisOrdinate", None) or 0 + return xaxis2angle(xaa, xao) def get_true_north(ifc_file): + """Get an angle pointing to global true north + + Anticlockwise is positive. + + Always remember that true north is not a constant! (Unless you are working + in polar coordinates) This true north is only a reference value useful for + things like solar analysis on small sites (<1km). If you're after the north + that your surveyor is using, you're probably after ``get_grid_north`` + instead. + + :param ifc_file: The IFC file + :type ifc_file: ifcopenshell.file.file + :return: An angle to true north in decimal degrees + :rtype: float + """ try: for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): - if not context.TrueNorth: - continue - return yaxis2angle(*context.TrueNorth.DirectionRatios[0:2]) + if context.TrueNorth: + return yaxis2angle(*context.TrueNorth.DirectionRatios[0:2]) except: return 0 return 0 -# Used for converting an angle in degrees to return the X and Y vectors of the X Axis in IFC grid north geolocation: def angle2xaxis(angle): + """Converts an angle into an X axis abscissa and ordinate + + The inverse of ``xaxis2angle``. + + :param angle: The angle in decimal degrees where anticlockwise is positive. + :type angle: float + :return: A tuple of X axis abscissa and ordinate + :rtype: tuple[float] + """ angle_rad = math.radians(angle) x = math.cos(angle_rad) y = -math.sin(angle_rad) return x, y -# Used for converting True North angle as seen in CAD (relative to +Y) def angle2yaxis(angle): + """Converts an angle into an Y axis abscissa and ordinate + + The inverse of ``yaxis2angle``. + + :param angle: The angle in decimal degrees where anticlockwise is positive. + :type angle: float + :return: A tuple of Y axis abscissa and ordinate + :rtype: tuple[float] + """ angle_rad = math.radians(angle) x = -math.sin(angle_rad) y = math.cos(angle_rad) From 3c02da416ea386a7c1ee4f24a7c93ec3d046625f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 5 Oct 2023 16:15:16 +1100 Subject: [PATCH 15/22] Georeference module now supports IFC2X3 georeferencing / conversion utilities. --- .../bim/module/georeference/data.py | 18 ++++-- .../blenderbim/bim/module/georeference/ui.py | 13 ++-- .../blenderbim/core/georeference.py | 6 +- src/blenderbim/blenderbim/core/spatial.py | 19 ++++-- src/blenderbim/blenderbim/core/tool.py | 5 +- .../blenderbim/tool/georeference.py | 59 ++----------------- src/blenderbim/test/core/test_georeference.py | 6 +- src/blenderbim/test/tool/test_georeference.py | 38 ++++++------ 8 files changed, 68 insertions(+), 96 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py index 66df12b93b..d362027939 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/data.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py @@ -56,7 +56,12 @@ class GeoreferenceData: @classmethod def map_conversion(cls): if tool.Ifc.get_schema() == "IFC2X3": - return {} + project = tool.Ifc.get().by_type("IfcProject")[0] + map_conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion") + if not map_conversion: + return {} + del map_conversion["id"] + return map_conversion for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): if context.HasCoordinateOperation: @@ -72,8 +77,8 @@ class GeoreferenceData: def map_derived_angle(cls): if ( cls.data["map_conversion"] - and cls.data["map_conversion"]["XAxisAbscissa"] is not None - and cls.data["map_conversion"]["XAxisOrdinate"] is not None + and cls.data["map_conversion"].get("XAxisAbscissa", None) is not None + and cls.data["map_conversion"].get("XAxisOrdinate", None) is not None ): return str( round( @@ -88,7 +93,12 @@ class GeoreferenceData: @classmethod def projected_crs(cls): if tool.Ifc.get_schema() == "IFC2X3": - return {} + project = tool.Ifc.get().by_type("IfcProject")[0] + projected_crs = ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS") + if not projected_crs: + return {} + del projected_crs["id"] + return projected_crs for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): if context.HasCoordinateOperation: diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index bb75f06f5e..f401376bbe 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -78,10 +78,14 @@ class BIM_PT_gis(Panel): def draw_ui(self, context): props = context.scene.BIMGeoreferenceProperties + if tool.Ifc.get_schema() == "IFC2X3": + row = self.layout.row() + row.label(text="IFC2X3 Fallback In Use", icon="ERROR") + if not GeoreferenceData.data["projected_crs"]: row = self.layout.row(align=True) row.label(text="Not Georeferenced") - if tool.Ifc.get_schema != "IFC2X3": + if tool.Ifc.get_schema() != "IFC2X3": row.operator("bim.add_georeferencing", icon="ADD", text="") if props.has_blender_offset: @@ -110,8 +114,9 @@ class BIM_PT_gis(Panel): if GeoreferenceData.data["projected_crs"]: row = self.layout.row(align=True) row.label(text="Projected CRS", icon="WORLD") - row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="") - row.operator("bim.remove_georeferencing", icon="X", text="") + if tool.Ifc.get_schema() != "IFC2X3": + row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="") + row.operator("bim.remove_georeferencing", icon="X", text="") for key, value in GeoreferenceData.data["projected_crs"].items(): if not value: @@ -184,4 +189,4 @@ class BIM_PT_gis_utilities(Panel): row = self.layout.row(align=True) row.prop(props, "y_axis_abscissa_output", text="North Abscissa") row = self.layout.row(align=True) - row.prop(props, "y_axis_ordinate_output", text="North Ordinate") \ No newline at end of file + row.prop(props, "y_axis_ordinate_output", text="North Ordinate") diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py index dcc9823904..9c6e6de118 100644 --- a/src/blenderbim/blenderbim/core/georeference.py +++ b/src/blenderbim/blenderbim/core/georeference.py @@ -71,13 +71,13 @@ def set_cursor_location(georeference): def convert_local_to_global(georeference): - coordinates = georeference.xyz2enh(georeference.get_coordinates("input"), georeference.get_map_conversion()) + coordinates = georeference.xyz2enh(georeference.get_coordinates("input")) georeference.set_coordinates("output", coordinates) georeference.set_cursor_location(coordinates) def convert_global_to_local(georeference): - coordinates = georeference.enh2xyz(georeference.get_coordinates("input"), georeference.get_map_conversion()) + coordinates = georeference.enh2xyz(georeference.get_coordinates("input")) georeference.set_coordinates("output", coordinates) georeference.set_cursor_location(coordinates) @@ -86,4 +86,4 @@ def convert_angle_to_coord(georeference, type): georeference.set_vector_coordinates(vector_coordinates,type) def import_plot(georeference, filepath): - georeference.import_plot(filepath, georeference.get_map_conversion()) + georeference.import_plot(filepath) diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index 109168847b..0e75438933 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -17,7 +17,6 @@ # along with BlenderBIM Add-on. If not, see . import blenderbim.core -import bpy def reference_structure(ifc, spatial, structure=None, element=None): @@ -98,33 +97,43 @@ def select_similar_container(ifc, spatial, obj=None): def select_product(spatial, product): spatial.select_products([product]) + def load_container_manager(spatial): spatial.load_container_manager() + def edit_container_attributes(spatial, entity=None): spatial.edit_container_attributes(entity) spatial.load_container_manager() + def contract_container(spatial, container=None): spatial.contract_container(container) spatial.load_container_manager() + def expand_container(spatial, container=None): spatial.expand_container(container) spatial.load_container_manager() + def delete_container(ifc, spatial, geometry, container=None): geometry.delete_ifc_object(ifc.get_object(container)) spatial.load_container_manager() + def select_decomposed_elements(spatial): container = spatial.get_active_container() if container: spatial.select_products(spatial.get_decomposed_elements(container)) -#HERE STARTS SPATIAL TOOL + +# HERE STARTS SPATIAL TOOL + def generate_spaces_from_walls(ifc, spatial, collector): + import bpy + active_obj = bpy.context.active_object element = ifc.get_entity(active_obj) container = spatial.get_container(element) @@ -172,9 +181,8 @@ def generate_spaces_from_walls(ifc, spatial, collector): bpy.context.view_layer.active_layer_collection.collection.objects.link(obj) bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace") container_obj = ifc.get_object(container) - blenderbim.core.spatial.assign_container( - ifc, collector, spatial, structure_obj=container_obj, element_obj=obj - ) + blenderbim.core.spatial.assign_container(ifc, collector, spatial, structure_obj=container_obj, element_obj=obj) + def toggle_space_visibility(ifc, spatial): model = ifc.get() @@ -182,4 +190,3 @@ def toggle_space_visibility(ifc, spatial): if not spaces: return spatial.toggle_spaces_visibility_wired_and_textured(spaces) - diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index de5d5f31cb..afa3a6810c 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -384,12 +384,11 @@ class Georeference: def angle2coords(cls, angle, type): pass def disable_editing(cls): pass def enable_editing(cls): pass - def enh2xyz(cls, map_conversion, coordinates): pass + def enh2xyz(cls, coordinates): pass def get_angle(cls, type): pass def get_coordinates(cls, io): pass def get_cursor_location(cls): pass def get_map_conversion_attributes(cls): pass - def get_map_conversion(cls): pass def get_projected_crs_attributes(cls): pass def get_true_north_attributes(cls): pass def import_map_conversion(cls): pass @@ -402,7 +401,7 @@ class Georeference: def set_ifc_grid_north(cls): pass def set_ifc_true_north(cls): pass def set_vector_coordinates(cls, vector_coordinates, type): pass - def xyz2enh(cls, map_conversion, coordinates): pass + def xyz2enh(cls, coordinates): pass @interface diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py index 52c31de1a2..037b5c6645 100644 --- a/src/blenderbim/blenderbim/tool/georeference.py +++ b/src/blenderbim/blenderbim/tool/georeference.py @@ -174,15 +174,7 @@ class Georeference(blenderbim.core.tool.Georeference): ) @classmethod - def get_map_conversion(cls): - if tool.Ifc.get_schema() == "IFC2X3": - return - for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): - if context.HasCoordinateOperation: - return context.HasCoordinateOperation[0] - - @classmethod - def xyz2enh(cls, coordinates, map_conversion): + def xyz2enh(cls, coordinates): props = bpy.context.scene.BIMGeoreferenceProperties if props.has_blender_offset: coordinates = ifcopenshell.util.geolocation.xyz2enh( @@ -196,53 +188,12 @@ class Georeference(blenderbim.core.tool.Georeference): float(props.blender_x_axis_ordinate), 1.0, ) - if map_conversion: - unit = map_conversion.TargetCRS.MapUnit - e = map_conversion.Eastings - n = map_conversion.Northings - h = map_conversion.OrthogonalHeight - if unit: - scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - e = ifcopenshell.util.unit.convert(e, getattr(unit, "Prefix", None), unit.Name, None, None) / scale - n = ifcopenshell.util.unit.convert(n, getattr(unit, "Prefix", None), unit.Name, None, None) / scale - h = ifcopenshell.util.unit.convert(h, getattr(unit, "Prefix", None), unit.Name, None, None) / scale - coordinates = ifcopenshell.util.geolocation.xyz2enh( - coordinates[0], - coordinates[1], - coordinates[2], - e, - n, - h, - map_conversion.XAxisAbscissa or 1.0, - map_conversion.XAxisOrdinate or 0.0, - map_conversion.Scale or 1.0, - ) - return coordinates + return ifcopenshell.util.geolocation.auto_xyz2enh(tool.Ifc.get(), *coordinates) @classmethod - def enh2xyz(cls, coordinates, map_conversion): + def enh2xyz(cls, coordinates): + coordinates = ifcopenshell.util.geolocation.auto_enh2xyz(tool.Ifc.get(), *coordinates) props = bpy.context.scene.BIMGeoreferenceProperties - if map_conversion: - unit = map_conversion.TargetCRS.MapUnit - e = map_conversion.Eastings - n = map_conversion.Northings - h = map_conversion.OrthogonalHeight - if unit: - scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - e = ifcopenshell.util.unit.convert(e, getattr(unit, "Prefix", None), unit.Name, None, None) / scale - n = ifcopenshell.util.unit.convert(n, getattr(unit, "Prefix", None), unit.Name, None, None) / scale - h = ifcopenshell.util.unit.convert(h, getattr(unit, "Prefix", None), unit.Name, None, None) / scale - coordinates = ifcopenshell.util.geolocation.enh2xyz( - coordinates[0], - coordinates[1], - coordinates[2], - e, - n, - h, - map_conversion.XAxisAbscissa or 1.0, - map_conversion.XAxisOrdinate or 0.0, - map_conversion.Scale or 1.0, - ) if props.has_blender_offset: coordinates = ifcopenshell.util.geolocation.enh2xyz( coordinates[0], @@ -316,7 +267,7 @@ class Georeference(blenderbim.core.tool.Georeference): rows = parse_csv(filepath) vertices = [] for row in rows: - coordinates = cls.enh2xyz([float(row[0]), float(row[1]), float(row[2])], map_conversion) + coordinates = cls.enh2xyz([float(row[0]), float(row[1]), float(row[2])]) vertices.append(coordinates) mesh = bpy.data.meshes.new("mesh") diff --git a/src/blenderbim/test/core/test_georeference.py b/src/blenderbim/test/core/test_georeference.py index 957e43b232..13aee6060c 100644 --- a/src/blenderbim/test/core/test_georeference.py +++ b/src/blenderbim/test/core/test_georeference.py @@ -97,8 +97,7 @@ class TestSetCursorLocation: class TestConvertLocalToGlobal: def test_run(self, georeference): georeference.get_coordinates("input").should_be_called().will_return("coordinates") - georeference.get_map_conversion().should_be_called().will_return("map_conversion") - georeference.xyz2enh("coordinates", "map_conversion").should_be_called().will_return("enh") + georeference.xyz2enh("coordinates").should_be_called().will_return("enh") georeference.set_coordinates("output", "enh").should_be_called() georeference.set_cursor_location("enh").should_be_called() subject.convert_local_to_global(georeference) @@ -107,8 +106,7 @@ class TestConvertLocalToGlobal: class TestConvertGlobalToLocal: def test_run(self, georeference): georeference.get_coordinates("input").should_be_called().will_return("coordinates") - georeference.get_map_conversion().should_be_called().will_return("map_conversion") - georeference.enh2xyz("coordinates", "map_conversion").should_be_called().will_return("xyz") + georeference.enh2xyz("coordinates").should_be_called().will_return("xyz") georeference.set_coordinates("output", "xyz").should_be_called() georeference.set_cursor_location("xyz").should_be_called() subject.convert_global_to_local(georeference) diff --git a/src/blenderbim/test/tool/test_georeference.py b/src/blenderbim/test/tool/test_georeference.py index 8540fed870..177d9aeb6b 100644 --- a/src/blenderbim/test/tool/test_georeference.py +++ b/src/blenderbim/test/tool/test_georeference.py @@ -247,25 +247,21 @@ class TestSetBlenderTrueNorth(NewFile): assert round(math.degrees(bpy.context.scene.sun_pos_properties.north_offset)) == 45 -class TestGetMapConversion(NewFile): - def test_run(self): - ifc = ifcopenshell.file() - tool.Ifc.set(ifc) - ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") - ifcopenshell.api.run("context.add_context", ifc, context_type="Model") - ifcopenshell.api.run("georeference.add_georeferencing", ifc) - assert subject.get_map_conversion().is_a("IfcMapConversion") - - class TestXyz2Enh(NewFile): def test_run(self): - assert subject.xyz2enh([0.0, 0.0, 0.0], None) == [0.0, 0.0, 0.0] + ifc = ifcopenshell.file() + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + tool.Ifc.set(ifc) + assert subject.xyz2enh([0.0, 0.0, 0.0]) == (0.0, 0.0, 0.0) def test_using_the_blender_offset(self): + ifc = ifcopenshell.file() + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + tool.Ifc.set(ifc) props = bpy.context.scene.BIMGeoreferenceProperties props.has_blender_offset = True props.blender_eastings = "1.0" - assert subject.xyz2enh([0.0, 0.0, 0.0], None) == (1.0, 0.0, 0.0) + assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0) def test_using_the_map_conversion(self): ifc = ifcopenshell.file() @@ -275,7 +271,7 @@ class TestXyz2Enh(NewFile): ifcopenshell.api.run("georeference.add_georeferencing", ifc) map_conversion = ifc.by_type("IfcMapConversion")[0] map_conversion.Eastings = 1.0 - assert subject.xyz2enh([0.0, 0.0, 0.0], map_conversion) == (1.0, 0.0, 0.0) + assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0) def test_applying_both_blender_offset_and_map_conversion(self): props = bpy.context.scene.BIMGeoreferenceProperties @@ -288,18 +284,24 @@ class TestXyz2Enh(NewFile): ifcopenshell.api.run("georeference.add_georeferencing", ifc) map_conversion = ifc.by_type("IfcMapConversion")[0] map_conversion.Northings = 1.0 - assert subject.xyz2enh([0.0, 0.0, 0.0], map_conversion) == (1.0, 1.0, 0.0) + assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 1.0, 0.0) class TestEnh2Xyz(NewFile): def test_run(self): - assert subject.enh2xyz([0.0, 0.0, 0.0], None) == [0.0, 0.0, 0.0] + ifc = ifcopenshell.file() + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + tool.Ifc.set(ifc) + assert subject.enh2xyz([0.0, 0.0, 0.0]) == (0.0, 0.0, 0.0) def test_using_the_blender_offset(self): + ifc = ifcopenshell.file() + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + tool.Ifc.set(ifc) props = bpy.context.scene.BIMGeoreferenceProperties props.has_blender_offset = True props.blender_eastings = "1.0" - assert subject.enh2xyz([0.0, 0.0, 0.0], None) == (-1.0, 0.0, 0.0) + assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0) def test_using_the_map_conversion(self): ifc = ifcopenshell.file() @@ -309,7 +311,7 @@ class TestEnh2Xyz(NewFile): ifcopenshell.api.run("georeference.add_georeferencing", ifc) map_conversion = ifc.by_type("IfcMapConversion")[0] map_conversion.Eastings = 1.0 - assert subject.enh2xyz([0.0, 0.0, 0.0], map_conversion) == (-1.0, 0.0, 0.0) + assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0) def test_applying_both_blender_offset_and_map_conversion(self): props = bpy.context.scene.BIMGeoreferenceProperties @@ -322,7 +324,7 @@ class TestEnh2Xyz(NewFile): ifcopenshell.api.run("georeference.add_georeferencing", ifc) map_conversion = ifc.by_type("IfcMapConversion")[0] map_conversion.Northings = 1.0 - assert subject.enh2xyz([0.0, 0.0, 0.0], map_conversion) == (-1.0, -1.0, 0.0) + assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, -1.0, 0.0) class TestSetIfcGridNorth(NewFile): From ef56c6ce7e6e0ceb28a67d4d004f2f9b2073294b Mon Sep 17 00:00:00 2001 From: taylor Date: Thu, 5 Oct 2023 20:12:06 +1300 Subject: [PATCH 16/22] Fixed model reference --- .../ifcopenshell/api/sequence/assign_product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py index 5e4393457f..d8003227d1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py @@ -61,7 +61,7 @@ class Usecase: wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall") # Let's construct that wall! - ifcopenshell.api.run("sequence.assign_product", relating_product=wall, related_object=task) + ifcopenshell.api.run("sequence.assign_product", model, relating_product=wall, related_object=task) """ self.file = file self.settings = { From ec4f0e5ad6fbbf5c40258c08e38b7ba6bf3ade1c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Oct 2023 15:32:45 +0500 Subject: [PATCH 17/22] Fixed issue applying array with Sync Children #3822 --- src/blenderbim/blenderbim/bim/module/model/array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/array.py b/src/blenderbim/blenderbim/bim/module/model/array.py index 087bf1e436..f8a40c2713 100644 --- a/src/blenderbim/blenderbim/bim/module/model/array.py +++ b/src/blenderbim/blenderbim/bim/module/model/array.py @@ -169,7 +169,6 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator): pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") data = json.loads(pset["Data"]) - data[self.item]["count"] = 1 if (self.keep_objs) & (self.item < (len(data) - 1)): self.report( @@ -188,6 +187,8 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator): tool.Blender.Modifier.Array.bake_children_transform(element, self.item) tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, False) + if not self.keep_objs: + data[self.item]["count"] = 1 tool.Model.regenerate_array(parent, data, self.keep_objs) pset = tool.Ifc.get().by_id(pset["id"]) From 948993e06692784d1db61261ebf024b4409f1af7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 5 Oct 2023 22:47:05 +1100 Subject: [PATCH 18/22] Fix #3845. Select similar for predefined type now takes into account inheritance. --- src/blenderbim/blenderbim/bim/module/search/operator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py index ee018ccef8..78b5887751 100644 --- a/src/blenderbim/blenderbim/bim/module/search/operator.py +++ b/src/blenderbim/blenderbim/bim/module/search/operator.py @@ -822,10 +822,13 @@ class SelectSimilar(Operator, tool.Ifc.Operator): props = context.scene.BIMSearchProperties obj = context.active_object element = tool.Ifc.get_entity(obj) - value = ifcopenshell.util.selector.get_element_value(element, props.element_key) + key = props.element_key + if props.element_key == "PredefinedType": + key = "predefined_type" + value = ifcopenshell.util.selector.get_element_value(element, key) for obj in context.visible_objects: element = tool.Ifc.get_entity(obj) if not element: continue - if ifcopenshell.util.selector.get_element_value(element, props.element_key) == value: + if ifcopenshell.util.selector.get_element_value(element, key) == value: obj.select_set(True) From 371daa6c808141679f415b1b785b2402b4aabe1f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 5 Oct 2023 22:49:52 +1100 Subject: [PATCH 19/22] Revise COBie 2.4 mappings for facility, floor, and some of space worksheet in collaboration with Emma Hooper --- src/ifcfm/ifcfm/cobie24.py | 73 +++++++++----------------------- src/ifcfm/ifcfm/cobie24legacy.py | 1 + 2 files changed, 21 insertions(+), 53 deletions(-) diff --git a/src/ifcfm/ifcfm/cobie24.py b/src/ifcfm/ifcfm/cobie24.py index 29d13cc3c2..7134d8b72e 100644 --- a/src/ifcfm/ifcfm/cobie24.py +++ b/src/ifcfm/ifcfm/cobie24.py @@ -320,27 +320,21 @@ def get_facility_data(ifc_file, element): site_name = None site_description = None if site: - site_name = val(site.Name) or val(site.LongName) or site.GlobalId - site_description = val(site.Description) or val(site.LongName) or val(site.Name) + site_name = val(site.Name) + site_description = val(site.Description) project = None project_name = None project_description = None try: project = ifc_file.by_type("IfcProject")[0] - project_name = val(project.Name) or val(project.LongName) or project.GlobalId - project_description = val(project.Description) or val(project.LongName) or val(project.Name) + project_name = val(project.Name) + project_description = val(project.Description) except: pass - name = val(element.Name) or val(element.LongName) - if not name: - name = val(project.Name) or val(project.LongName) - if not name and site: - name = val(site.Name) or val(site.LongName) - return { - "Name": name, + "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), "Category": get_category(element), @@ -353,12 +347,12 @@ def get_facility_data(ifc_file, element): "AreaMeasurement": get_area_measurement(element), "ExternalSystem": get_external_system(element), "ExternalProjectObject": "IfcProject", - "ExternalProjectIdentifier": project.GlobalId if project else ifcopenshell.guid.new(), + "ExternalProjectIdentifier": project.GlobalId if project else None, "ExternalSiteObject": "IfcSite", - "ExternalSiteIdentifier": site.GlobalId if site else ifcopenshell.guid.new(), + "ExternalSiteIdentifier": site.GlobalId if site else None, "ExternalFacilityObject": "IfcBuilding", "ExternalFacilityIdentifier": element.GlobalId, - "Description": val(element.Description) or val(element.LongName) or val(element.Name), + "Description": val(element.Description), "ProjectDescription": project_description, "SiteDescription": site_description, "Phase": val(project.Phase) if project else None, @@ -366,10 +360,6 @@ def get_facility_data(ifc_file, element): def get_floor_data(ifc_file, element): - external_object = element.is_a() - if element.ObjectType and element.ObjectType.lower() in ("site", "ifcsite"): - external_object = "IfcSite" - height_names = { "Height", "NetHeight", @@ -400,9 +390,9 @@ def get_floor_data(ifc_file, element): "CreatedOn": get_created_on(element), "Category": get_category(element), "ExternalSystem": get_external_system(element), - "ExternalObject": external_object, + "ExternalObject": element.is_a(), "ExternalIdentifier": element.GlobalId, - "Description": val(element.Description) or val(element.LongName) or val(element.Name), + "Description": val(element.Description), "Elevation": val(elevation), "Height": height, } @@ -439,7 +429,7 @@ def get_space_data(ifc_file, element): "CreatedOn": get_created_on(element), "Category": get_category(element), "FloorName": floor_name, - "Description": val(element.Description) or val(element.LongName) or val(element.Name), + "Description": val(element.Description), "ExternalSystem": get_external_system(element), "ExternalObject": element.is_a(), "ExternalIdentifier": element.GlobalId, @@ -976,6 +966,7 @@ def get_owner_name(element): def get_created_on(element): if getattr(element, "OwnerHistory", None): return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() + return "1900-12-31T23:59:59" # Yes, really def get_external_system(element): @@ -1009,13 +1000,20 @@ def get_area_measurement(element): if result: return result + psets = ifcopenshell.util.element.get_psets(element) + if psets: + for _, props in psets.items(): + for name, value in props.items(): + if name == "MethodOfMeasurement": + return value + def get_category(element): references = list(ifcopenshell.util.classification.get_references(element)) results = [] for reference in references: if reference.is_a("IfcClassification"): - results.append(reference.Name) + continue elif reference.is_a("IfcClassificationReference"): identification = val(getattr(reference, "Identification", getattr(reference, "ItemReference", None))) if val(reference.Name) and identification and val(reference.Name) != identification: @@ -1024,40 +1022,9 @@ def get_category(element): results.append(reference.Name) elif identification: results.append(identification) - elif reference.ReferencedSource and val(reference.ReferencedSource.Name): - results.append(reference.ReferencedSource.Name) - elif val(reference.Location): - results.append(reference.Location) if results: return ",".join(results) - category_props = [ - ("Assembly Code", "Assembly Description"), - ("Category Code", "Category Description"), - ("Classification Code", "Classification Description"), - ("OmniClass Number", "OmniClass Title"), - ("Uniclass Code", "Uniclass Description"), - ] - - psets = ifcopenshell.util.element.get_psets(element) - properties = {} - if psets: - for _, props in psets.items(): - properties.update(props) - - for code, description in category_props: - code = val(properties.get(code, None)) - if code: - description = val(properties.get(description, None)) - if code and description: - results.append(code + " : " + description) - else: - results.append(code) - if results: - return ",".join(results) - - return val(getattr(element, "ObjectType", None)) - def get_pao_address(person, organization, name): for actor in [organization, person]: diff --git a/src/ifcfm/ifcfm/cobie24legacy.py b/src/ifcfm/ifcfm/cobie24legacy.py index 0de1a783ad..83c9f85d9c 100644 --- a/src/ifcfm/ifcfm/cobie24legacy.py +++ b/src/ifcfm/ifcfm/cobie24legacy.py @@ -993,6 +993,7 @@ def get_owner_name(element): def get_created_on(element): if getattr(element, "OwnerHistory", None): return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() + return "1900-12-31T23:59:59" # Yes, really def get_external_system(element): From ceae7d4fdb7f33ac90ef16ca0283617e9cda16be Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Oct 2023 17:31:12 +0500 Subject: [PATCH 20/22] DumbWallJoiner to work with IfcBooleanResult correctly #3831 It wasn't detecting extrusion as part of IfcBooleanResult and was using default wall data as fallback. Occured after 0044ef5 Solves #3831 Related to #3709 --- src/blenderbim/blenderbim/bim/module/model/wall.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index 54b81214a0..f4cd1e346f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -1262,8 +1262,13 @@ class DumbWallJoiner: results["is_sloped"] = True results["height"] = (item.Depth * self.unit_scale) / (1 / cos(results["x_angle"])) break - elif item.is_a("IfcBooleanClippingResult"): + elif item.is_a("IfcBooleanClippingResult"): # should be before IfcBooleanResult check item = item.FirstOperand + elif item.is_a("IfcBooleanResult"): + if item.FirstOperand.is_a("IfcExtrudedAreaSolid") or item.FirstOperand.is_a("IfcBooleanResult"): + item = item.FirstOperand + else: + item = item.SecondOperand else: break return results From 68a1da91900a07d7b597f9bd4dfd0898d6bdeff8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 5 Oct 2023 18:38:15 +0500 Subject: [PATCH 21/22] test for model.get_manual_booleans() #3709 added test that model.get_manual_booleans will ignore IfcBooleanClippings until we find some other solution to distinguish automatic and manual clippings. And some refactor along the way. --- src/blenderbim/test/tool/test_model.py | 46 +++++++++++++++---- .../ifcopenshell/api/geometry/add_boolean.py | 26 ++++------- .../test/api/geometry/test_add_boolean.py | 46 +++++++++++++++++++ 3 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 src/ifcopenshell-python/test/api/geometry/test_add_boolean.py diff --git a/src/blenderbim/test/tool/test_model.py b/src/blenderbim/test/tool/test_model.py index 90546c0a16..20329a3cad 100644 --- a/src/blenderbim/test/tool/test_model.py +++ b/src/blenderbim/test/tool/test_model.py @@ -52,12 +52,14 @@ class TestGenerateOccurrenceName(NewFile): 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): + def setup_profile_represntation(self, clippings=[]): ifc = ifcopenshell.file() + self.ifc = ifc 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") @@ -65,17 +67,43 @@ class TestGetManualBooleans(NewFile): 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, + "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) + 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, clippings=clippings + ) ifcopenshell.api.run("geometry.assign_representation", ifc, product=element, representation=representation) + return element, representation + + def test_manual_booleans(self): + element, representation = self.setup_profile_represntation() 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) + ifcopenshell.api.run( + "geometry.add_boolean", self.ifc, representation=representation, type="IfcHalfSpaceSolid", matrix=matrix + ) assert len(subject.get_manual_booleans(element)) == 1 + def test_automatic_booleans_ignored(self): + clipping = { + "type": "IfcBooleanClippingResult", + "operand_type": "IfcHalfSpaceSolid", + "matrix": np.eye(4).tolist(), + } + element, representation = self.setup_profile_represntation(clippings=[clipping]) + assert len(subject.get_manual_booleans(element)) == 0 diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py index 7569f58bbc..c477d0cfe6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit +import numpy as np class Usecase: @@ -44,7 +45,6 @@ class Usecase: elif self.settings["type"] == "Mesh": if self.settings["blender_obj"]: result = self.create_blender_mesh() - representation_type = "Clipping" items = [] for item in self.settings["representation"].Items: # For now, we don't use IfcBooleanClippingResult. @@ -56,23 +56,13 @@ class Usecase: self.settings["representation"].Items = items def create_half_space_solid(self): - clipping = self.settings["matrix"] - return self.file.createIfcHalfSpaceSolid( - self.file.createIfcPlane( - self.file.createIfcAxis2Placement3D( - self.file.createIfcCartesianPoint( - ( - self.convert_si_to_unit(clipping[0][3]), - self.convert_si_to_unit(clipping[1][3]), - self.convert_si_to_unit(clipping[2][3]), - ) - ), - self.file.createIfcDirection((clipping[0][2], clipping[1][2], clipping[2][2])), - self.file.createIfcDirection((clipping[0][0], clipping[1][0], clipping[2][0])), - ) - ), - False, - ) + clipping = np.array(self.settings["matrix"])[:3] + local_z = self.file.createIfcDirection(clipping[:, 2].tolist()) + local_x = self.file.createIfcDirection(clipping[:, 0].tolist()) + point = self.file.createIfcCartesianPoint(self.convert_si_to_unit(clipping[:, 3]).tolist()) + placement = self.file.createIfcAxis2Placement3D(point, local_z, local_x) + plane = self.file.createIfcPlane(placement) + return self.file.createIfcHalfSpaceSolid(plane, AgreementFlag=False) def create_blender_mesh(self): self.ifc_vertices = [] diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py new file mode 100644 index 0000000000..041e6c1980 --- /dev/null +++ b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py @@ -0,0 +1,46 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell 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. +# +# IfcOpenShell 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 IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api +import numpy as np +from ifcopenshell.util.shape_builder import ShapeBuilder + + +class TestAddBoolean(test.bootstrap.IFC4): + def test_returning_ifc_boolean_result(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model") + body = ifcopenshell.api.run( + "context.add_context", + self.file, + context_type="Model", + context_identifier="Body", + target_view="MODEL_VIEW", + parent=model, + ) + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + + builder = ShapeBuilder(self.file) + extrusion = builder.extrude(builder.rectangle()) + rep = builder.get_representation(body, extrusion) + ifcopenshell.api.run("geometry.assign_representation", self.file, product=wall, representation=rep) + + ifcopenshell.api.run("geometry.add_boolean", self.file, representation=rep, matrix=np.eye(4)) + assert rep.Items[0].is_a() == "IfcBooleanResult" + assert rep.RepresentationType == "CSG" From 285fc2269ebdd83a8ab5f16095512495feef3fb2 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 5 Oct 2023 15:52:59 +0200 Subject: [PATCH 22/22] #3852 Update pyproject.toml --- src/ifctester/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifctester/pyproject.toml b/src/ifctester/pyproject.toml index be2f338522..1936495312 100644 --- a/src/ifctester/pyproject.toml +++ b/src/ifctester/pyproject.toml @@ -24,4 +24,4 @@ dependencies = [ include = ["ifctester"] exclude = ["test*"] [tool.setuptools.package-data] -ifctester = ["*.xsd"] +ifctester = ["*.xsd", "templates/*.html"]