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 ifcopenshell
import logging import logging
import numpy as np
class Patcher: class Patcher:
def __init__(self, file: ifcopenshell.file, logger: logging.Logger, ifc_class: str = "IfcSite"): def __init__(self, file: ifcopenshell.file, logger: logging.Logger, ifc_class: str = "", only_xy: bool = True):
"""Resets the location of a spatial element to 0,0,0 """Resets the location of non-geometric spatial elements to 0,0,0
Another more specialised patch to fix incorrect coordinate usage is to Often, non-geometric spatial elements are located at arbitrary
reset the location of spatial elements (sites, buildings, storeys) back locations relative to the model. Because they are non-geometric but
to 0,0,0. 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: Example:
.. code:: python .. code:: python
# All IfcSites will shift back to 0,0,0. # 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.file = file
self.logger = logger self.logger = logger
self.ifc_class = ifc_class self.ifc_class = ifc_class
self.only_xy = only_xy
def patch(self) -> None: def patch(self) -> None:
project = self.file.by_type("IfcProject")[0] project = self.file.by_type("IfcProject")[0]
spatial_elements = self.find_decomposed_ifc_class(project, self.ifc_class) queue = [project]
for spatial_element in spatial_elements: while queue:
self.patch_placement_to_origin(spatial_element) element = queue.pop()
if not self.ifc_class or element.is_a(self.ifc_class):
def find_decomposed_ifc_class( self.patch_placement_to_origin(element)
self, element: ifcopenshell.entity_instance, ifc_class: str if parts := ifcopenshell.util.element.get_parts(element):
) -> list[ifcopenshell.entity_instance]: queue.extend(parts)
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
def patch_placement_to_origin(self, element: ifcopenshell.entity_instance) -> None: def patch_placement_to_origin(self, element: ifcopenshell.entity_instance) -> None:
element.ObjectPlacement.RelativePlacement.Location.Coordinates = (0.0, 0.0, 0.0) if not getattr(element, "ObjectPlacement", None):
if element.ObjectPlacement.RelativePlacement.Axis: return
element.ObjectPlacement.RelativePlacement.Axis.DirectionRatios = (0.0, 0.0, 1.0) if self.only_xy:
if element.ObjectPlacement.RelativePlacement.RefDirection: m = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
element.ObjectPlacement.RelativePlacement.RefDirection.DirectionRatios = (1.0, 0.0, 0.0) 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