202 lines
9.0 KiB
Python
202 lines
9.0 KiB
Python
# IfcOpenShell - IFC toolkit and geometry engine
|
|
# Copyright (C) 2021 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/>.
|
|
|
|
from typing import Any, Optional, Union
|
|
|
|
import numpy as np
|
|
import numpy.typing as npt
|
|
|
|
import ifcopenshell.api.owner
|
|
import ifcopenshell.util.element
|
|
import ifcopenshell.util.placement
|
|
import ifcopenshell.util.unit
|
|
from ifcopenshell.util.shape_builder import ShapeBuilder
|
|
|
|
NPArrayOfFloats = npt.NDArray[np.float64]
|
|
|
|
|
|
def edit_object_placement(
|
|
file: ifcopenshell.file,
|
|
product: ifcopenshell.entity_instance,
|
|
matrix: Optional[NPArrayOfFloats] = None,
|
|
is_si: bool = True,
|
|
should_transform_children: bool = False,
|
|
) -> ifcopenshell.entity_instance:
|
|
"""Changes the object placement matrix of an element
|
|
|
|
The placement matrix is a 4x4 matrix describing the location and
|
|
orientation of an element in 3D. See
|
|
https://docs.ifcopenshell.org/ifcopenshell-python/geometry_creation.html#object-placements
|
|
for more details.
|
|
|
|
This only supports local placements. Grid and linear placements are not
|
|
supported.
|
|
|
|
:param matrix: A 4x4 matrix in numpy. If left blank, it is the identity
|
|
matrix (equivalent to ``np.eye(4)``).
|
|
:param is_si: If True, the matrix is given in SI units. If false, in
|
|
project units.
|
|
:param should_transform_children: A child element is a nested element,
|
|
opening, filling, etc. If True, child elements move along with the
|
|
parent; pass True when moving an assembly (roof, furniture group, etc.)
|
|
and you want all children to follow. If False (default), child elements
|
|
keep their current world positions; their local placements are rewritten
|
|
to compensate for the parent move.
|
|
:return: The new or updated IfcLocalPlacement entity
|
|
"""
|
|
usecase = Usecase()
|
|
usecase.file = file
|
|
usecase.settings = {
|
|
"product": product,
|
|
"matrix": matrix if matrix is not None else np.eye(4),
|
|
"is_si": is_si,
|
|
"should_transform_children": should_transform_children,
|
|
}
|
|
return usecase.execute()
|
|
|
|
|
|
class Usecase:
|
|
file: ifcopenshell.file
|
|
settings: dict[str, Any]
|
|
|
|
def execute(self):
|
|
if not hasattr(self.settings["product"], "ObjectPlacement"):
|
|
return
|
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
|
self.builder = ShapeBuilder(self.file)
|
|
|
|
if not self.settings["is_si"]:
|
|
self.convert_matrix_to_si(self.settings["matrix"])
|
|
|
|
children_settings = []
|
|
if not self.settings["should_transform_children"]:
|
|
children_settings = self.get_children_settings(self.settings["product"].ObjectPlacement)
|
|
|
|
placement_rel_to = self.get_placement_rel_to()
|
|
relative_placement = self.get_relative_placement(placement_rel_to)
|
|
new_placement = self.file.createIfcLocalPlacement(RelativePlacement=relative_placement)
|
|
|
|
old_placement = self.settings["product"].ObjectPlacement
|
|
|
|
if old_placement:
|
|
for inverse in self.file.get_inverse(old_placement):
|
|
if inverse.is_a("IfcLocalPlacement"):
|
|
ifcopenshell.util.element.replace_attribute(inverse, old_placement, new_placement)
|
|
|
|
if self.file.get_total_inverses(old_placement) == 1:
|
|
self.settings["product"].ObjectPlacement = None
|
|
old_placement.PlacementRelTo = None
|
|
ifcopenshell.util.element.remove_deep2(self.file, old_placement)
|
|
|
|
new_placement.PlacementRelTo = placement_rel_to
|
|
self.settings["product"].ObjectPlacement = new_placement
|
|
|
|
ifcopenshell.api.owner.update_owner_history(self.file, element=self.settings["product"])
|
|
|
|
for settings in children_settings:
|
|
self.settings = settings
|
|
self.execute()
|
|
|
|
return new_placement
|
|
|
|
def convert_matrix_to_si(self, matrix: NPArrayOfFloats):
|
|
matrix[0][3] *= self.unit_scale
|
|
matrix[1][3] *= self.unit_scale
|
|
matrix[2][3] *= self.unit_scale
|
|
|
|
def get_placement_rel_to(self) -> Union[ifcopenshell.entity_instance, None]:
|
|
product = self.settings["product"]
|
|
relating_object = None
|
|
|
|
if rels := getattr(product, "Decomposes", None):
|
|
relating_object = rels[0].RelatingObject
|
|
elif rels := getattr(product, "Nests", None):
|
|
relating_object = rels[0].RelatingObject
|
|
elif rels := getattr(product, "ContainedIn", None):
|
|
relating_object = rels[0].RelatedElement
|
|
elif rels := getattr(product, "VoidsElements", None):
|
|
relating_object = rels[0].RelatingBuildingElement
|
|
elif rels := getattr(product, "FillsVoids", None):
|
|
relating_object = rels[0].RelatingOpeningElement
|
|
elif rels := getattr(product, "ProjectsElements", None):
|
|
relating_object = rels[0].RelatingElement
|
|
# TODO: add tests when there will be adherence api
|
|
elif rels := getattr(product, "AdheresToElement", None):
|
|
relating_object = rels[0].RelatingElement
|
|
elif rels := getattr(product, "ContainedInStructure", None):
|
|
return rels[0].RelatingStructure.ObjectPlacement
|
|
|
|
if relating_object:
|
|
return getattr(relating_object, "ObjectPlacement", None)
|
|
|
|
def get_children_settings(self, placement: Union[ifcopenshell.entity_instance, None]) -> list[dict]:
|
|
if not placement:
|
|
return []
|
|
results = []
|
|
# NOTE: we ignore subchildren as we already adjust position for their parent
|
|
# therefore they're not present in `results` and `should_transform_children` should be `True`
|
|
for referenced_placement in placement.ReferencedByPlacements:
|
|
matrix = ifcopenshell.util.placement.get_local_placement(referenced_placement)
|
|
for obj in referenced_placement.PlacesObject:
|
|
if obj.is_a("IfcDistributionPort"):
|
|
# Although a port is technically a nested child, it is generally
|
|
# more intuitive that the ports always move with the parent.
|
|
continue
|
|
elif obj.is_a("IfcFeatureElement"):
|
|
# Feature elements affect the geometry of their parent, and
|
|
# so logically should always move with the parent. However,
|
|
# subchildren (fillings) shouldn't move.
|
|
placement2 = obj.ObjectPlacement
|
|
for referenced_placement2 in placement2.ReferencedByPlacements:
|
|
matrix2 = ifcopenshell.util.placement.get_local_placement(referenced_placement2)
|
|
for obj2 in referenced_placement2.PlacesObject:
|
|
results.append(
|
|
{"product": obj2, "matrix": matrix2, "is_si": False, "should_transform_children": True}
|
|
)
|
|
continue
|
|
results.append({"product": obj, "matrix": matrix, "is_si": False, "should_transform_children": True})
|
|
return results
|
|
|
|
def get_relative_placement(
|
|
self, placement_rel_to: Union[ifcopenshell.entity_instance, None]
|
|
) -> ifcopenshell.entity_instance:
|
|
if placement_rel_to:
|
|
relating_object_matrix = ifcopenshell.util.placement.get_local_placement(placement_rel_to)
|
|
relating_object_matrix[0][3] = self.convert_unit_to_si(relating_object_matrix[0][3])
|
|
relating_object_matrix[1][3] = self.convert_unit_to_si(relating_object_matrix[1][3])
|
|
relating_object_matrix[2][3] = self.convert_unit_to_si(relating_object_matrix[2][3])
|
|
else:
|
|
relating_object_matrix = np.eye(4)
|
|
m = self.settings["matrix"]
|
|
x = np.array((m[0][0], m[1][0], m[2][0]))
|
|
z = np.array((m[0][2], m[1][2], m[2][2]))
|
|
o = np.array((m[0][3], m[1][3], m[2][3]))
|
|
object_matrix = ifcopenshell.util.placement.a2p(o, z, x)
|
|
relative_placement_matrix = np.linalg.inv(relating_object_matrix) @ object_matrix
|
|
return self.builder.create_axis2_placement_3d(
|
|
self.convert_si_to_unit(relative_placement_matrix[:, 3][0:3]),
|
|
relative_placement_matrix[:, 2][0:3],
|
|
relative_placement_matrix[:, 0][0:3],
|
|
)
|
|
|
|
def convert_si_to_unit(self, co: NPArrayOfFloats) -> NPArrayOfFloats:
|
|
return co / self.unit_scale
|
|
|
|
def convert_unit_to_si(self, co: NPArrayOfFloats) -> NPArrayOfFloats:
|
|
return co * self.unit_scale
|