From 7038cbe8718540c8718d84aa6a866159bd63f72d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 2 Aug 2025 17:54:55 +1000 Subject: [PATCH] ResetSpatialElementLocations patch now supports only_xy toggle. Also rewrote the patch to use API and util functions to make it more robust. --- .../recipes/ResetSpatialElementLocations.py | 63 +++++++++--------- .../test/test_ResetSpatialElementLocations.py | 64 +++++++++++++++++++ 2 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 src/ifcpatch/test/test_ResetSpatialElementLocations.py diff --git a/src/ifcpatch/ifcpatch/recipes/ResetSpatialElementLocations.py b/src/ifcpatch/ifcpatch/recipes/ResetSpatialElementLocations.py index a02ba69285..c373b507f3 100644 --- a/src/ifcpatch/ifcpatch/recipes/ResetSpatialElementLocations.py +++ b/src/ifcpatch/ifcpatch/recipes/ResetSpatialElementLocations.py @@ -18,52 +18,57 @@ import ifcopenshell import logging +import numpy as np class Patcher: - def __init__(self, file: ifcopenshell.file, logger: logging.Logger, ifc_class: str = "IfcSite"): - """Resets the location of a spatial element to 0,0,0 + def __init__(self, file: ifcopenshell.file, logger: logging.Logger, ifc_class: str = "", only_xy: bool = True): + """Resets the location of non-geometric spatial elements to 0,0,0 - Another more specialised patch to fix incorrect coordinate usage is to - reset the location of spatial elements (sites, buildings, storeys) back - to 0,0,0. + Often, non-geometric spatial elements are located at arbitrary + locations relative to the model. Because they are non-geometric but + still contain placements, many users do not realise that their + coordinates are actually ver far away and can cause precision issues if + "fit all in view" is used. - :param ifc_class: The class of spatial element to reset coordinates for. + This patch lets you selectively reset the location of spatial elements + (sites, buildings, storeys) back to 0,0,0. This is typically done after + other coordinate operation patches. Alternatively, consider using the + SetFalseOrigin patch which can do this operation built-in. + + :param ifc_class: The class of spatial element to reset coordinates + for. Leave blank if you want to reset everything. + :param only_xy: If True, only the X and Y coordinates will be affected. Example: .. code:: python # All IfcSites will shift back to 0,0,0. - ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetSpatialElementLocations", "arguments": ["IfcSite"]}) + ifcpatch.execute({"file": model, "recipe": "ResetSpatialElementLocations", "arguments": ["IfcSite"]}) """ self.file = file self.logger = logger self.ifc_class = ifc_class + self.only_xy = only_xy def patch(self) -> None: project = self.file.by_type("IfcProject")[0] - spatial_elements = self.find_decomposed_ifc_class(project, self.ifc_class) - for spatial_element in spatial_elements: - self.patch_placement_to_origin(spatial_element) - - def find_decomposed_ifc_class( - self, element: ifcopenshell.entity_instance, ifc_class: str - ) -> list[ifcopenshell.entity_instance]: - results = [] - rel_aggregates = element.IsDecomposedBy - if not rel_aggregates: - return results - for rel_aggregate in rel_aggregates: - for part in rel_aggregate.RelatedObjects: - if part.is_a(ifc_class): - results.append(part) - results.extend(self.find_decomposed_ifc_class(part, ifc_class)) - return results + queue = [project] + while queue: + element = queue.pop() + if not self.ifc_class or element.is_a(self.ifc_class): + self.patch_placement_to_origin(element) + if parts := ifcopenshell.util.element.get_parts(element): + queue.extend(parts) def patch_placement_to_origin(self, element: ifcopenshell.entity_instance) -> None: - element.ObjectPlacement.RelativePlacement.Location.Coordinates = (0.0, 0.0, 0.0) - if element.ObjectPlacement.RelativePlacement.Axis: - element.ObjectPlacement.RelativePlacement.Axis.DirectionRatios = (0.0, 0.0, 1.0) - if element.ObjectPlacement.RelativePlacement.RefDirection: - element.ObjectPlacement.RelativePlacement.RefDirection.DirectionRatios = (1.0, 0.0, 0.0) + if not getattr(element, "ObjectPlacement", None): + return + if self.only_xy: + m = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) + m[0][3] = 0.0 + m[1][3] = 0.0 + ifcopenshell.api.geometry.edit_object_placement(self.file, product=element, matrix=m, is_si=False) + return + ifcopenshell.api.geometry.edit_object_placement(self.file, product=element, matrix=np.eye(4), is_si=False) diff --git a/src/ifcpatch/test/test_ResetSpatialElementLocations.py b/src/ifcpatch/test/test_ResetSpatialElementLocations.py new file mode 100644 index 0000000000..5514357090 --- /dev/null +++ b/src/ifcpatch/test/test_ResetSpatialElementLocations.py @@ -0,0 +1,64 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 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 ifcopenshell.api.root +import ifcopenshell.api.unit +import ifcopenshell.util.unit +import ifcpatch +import numpy as np + + +class TestResetSpatialElementLocations(test.bootstrap.IFC4): + def test_run(self): + project = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) + site = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcSite") + ifcopenshell.api.aggregate.assign_object(self.file, products=[site], relating_object=project) + m = np.eye(4) + m[0][3] = 42.0 + m[2][3] = 42.0 + ifcopenshell.api.geometry.edit_object_placement(self.file, product=site, matrix=m) + ifcpatch.execute( + {"file": self.file, "recipe": "ResetSpatialElementLocations", "arguments": ["IfcBuilding", False]} + ) + m2 = ifcopenshell.util.placement.get_local_placement(site.ObjectPlacement) + assert np.allclose(m, m2) + ifcpatch.execute({"file": self.file, "recipe": "ResetSpatialElementLocations", "arguments": ["IfcSite", False]}) + m2 = ifcopenshell.util.placement.get_local_placement(site.ObjectPlacement) + assert np.allclose(m2, np.eye(4)) + ifcopenshell.api.geometry.edit_object_placement(self.file, product=site, matrix=m) + ifcpatch.execute({"file": self.file, "recipe": "ResetSpatialElementLocations", "arguments": ["", False]}) + m2 = ifcopenshell.util.placement.get_local_placement(site.ObjectPlacement) + assert np.allclose(m2, np.eye(4)) + + m = np.eye(4) + m2 = np.eye(4) + m[0][3] = 42.0 + m[1][3] = 42.0 + m[2][3] = m2[2][3] = 42.0 + ifcopenshell.api.geometry.edit_object_placement(self.file, product=site, matrix=m) + ifcpatch.execute({"file": self.file, "recipe": "ResetSpatialElementLocations", "arguments": ["", True]}) + m = ifcopenshell.util.placement.get_local_placement(site.ObjectPlacement) + assert np.allclose(m, m2) + + +class TestResetSpatialElementLocationsIFC2X3(test.bootstrap.IFC2X3, TestResetSpatialElementLocations): + pass