From 606a0b46f2fe389fd85dd7e9641ebe12cb1ae5ff Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 24 Apr 2024 11:28:49 +0500 Subject: [PATCH] MergeProject - support merging projects with different units --- src/ifcpatch/ifcpatch/recipes/MergeProject.py | 19 ++++++ src/ifcpatch/test/test_MergeProject.py | 66 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/ifcpatch/test/test_MergeProject.py diff --git a/src/ifcpatch/ifcpatch/recipes/MergeProject.py b/src/ifcpatch/ifcpatch/recipes/MergeProject.py index 260affcfa3..25b6a9b4c9 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeProject.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeProject.py @@ -18,6 +18,7 @@ import ifcopenshell import ifcopenshell.util.element +import ifcopenshell.util.unit from typing import Union from logging import Logger @@ -30,6 +31,9 @@ class Patcher: further processing will be done. This means that you may end up with duplicate spatial hierarchies (i.e. 2 sites, 2 buildings, etc). + Will automatically convert length units in the second model to the main + model's unit before merging. + :param filepath: The filepath of the second IFC model to merge into the first. The first model is already specified as the input to IfcPatch. @@ -48,6 +52,10 @@ class Patcher: def patch(self): source = ifcopenshell.open(self.filepath) + # make sure models units will match + if (main_unit := self.get_unit_name(self.file)) != self.get_unit_name(source): + source = ifcopenshell.util.unit.convert_file_length_units(source, main_unit) + self.existing_contexts: list[ifcopenshell.entity_instance] = self.file.by_type( "IfcGeometricRepresentationContext" ) @@ -69,6 +77,17 @@ class Patcher: self.reuse_existing_contexts() + def get_unit_name(self, ifc_file: ifcopenshell.file) -> str: + length_unit = ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT") + names = { + "METRE": "METERS", + "FOOT": "FEET", + "INCH": "INCHES", + "MILE": "MILES", + } + prefix = getattr(length_unit, "Prefix", None) or "" + return prefix + names[length_unit.Name.upper()] + def reuse_existing_contexts(self): to_delete = set() diff --git a/src/ifcpatch/test/test_MergeProject.py b/src/ifcpatch/test/test_MergeProject.py new file mode 100644 index 0000000000..93e9b9bbb5 --- /dev/null +++ b/src/ifcpatch/test/test_MergeProject.py @@ -0,0 +1,66 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 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 ifcpatch +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.placement +import test.bootstrap +import tempfile +import numpy as np +from pathlib import Path +from typing import Optional + + +class TestMergeProject(test.bootstrap.IFC4): + def setup_project(self, ifc_file: Optional[ifcopenshell.file] = None): + prefix = None if ifc_file else "MILLI" + if ifc_file is None: + ifc_file = ifcopenshell.file(schema=self.file.schema) + + project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject") + unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", prefix=prefix) + ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit]) + + matrix = np.eye(4) + matrix[:, 3] = (1, 2, 3, 1) + wall = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcWall") + ifcopenshell.api.run("geometry.edit_object_placement", ifc_file, product=wall, matrix=matrix, is_si=True) + return ifc_file + + def test_run(self): + self.file = self.setup_project(self.file) + second_file = self.setup_project() + temp_path = Path(tempfile.gettempdir()) / "second.ifc" + second_file.write(temp_path) + output = ifcpatch.execute({"file": self.file, "recipe": "MergeProject", "arguments": [str(temp_path)]}) + + assert len(output.by_type("IfcWall")) == 2 + wall1, wall2 = output.by_type("IfcWall") + + # test that units are converted + placement1 = ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement) + placement2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) + to_tuple = lambda arr: tuple(map(tuple, arr)) + matrix = np.eye(4) + matrix[:, 3] = (1, 2, 3, 1) + assert to_tuple(placement1) == to_tuple(placement2) == to_tuple(matrix) + + +class TestMergeProjectIFC2X3(test.bootstrap.IFC2X3, TestMergeProject): + pass