ResetSpatialElementLocations patch now supports only_xy toggle.

Also rewrote the patch to use API and util functions to make it more
robust.
This commit is contained in:
Dion Moult
2025-08-02 17:54:55 +10:00
parent 1cb44da76c
commit 7038cbe871
2 changed files with 98 additions and 29 deletions
@@ -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)
@@ -0,0 +1,64 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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