util.shape - replace ShapeType with W.Triangulation for accuracy

This commit is contained in:
Andrej730
2025-07-17 16:17:20 +05:00
parent 2edfe1e5d8
commit 728f9e2c04
@@ -26,7 +26,7 @@ import ifcopenshell.util.placement
import ifcopenshell.util.representation import ifcopenshell.util.representation
from ifcopenshell.util.shape_builder import VectorType from ifcopenshell.util.shape_builder import VectorType
from math import radians, cos from math import radians, cos
from ifcopenshell.geom import ShapeElementType, ShapeType from ifcopenshell.geom import ShapeElementType
from typing import Optional, Literal, Union from typing import Optional, Literal, Union
tol = 1e-6 tol = 1e-6
@@ -36,7 +36,7 @@ VECTOR_3D = tuple[float, float, float]
MatrixType = npt.NDArray[np.float64] MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`""" """`npt.NDArray[np.float64]`"""
# NOTE: See IfcGeomRepresentation.h for ShapeType buffer types. # NOTE: See IfcGeomRepresentation.h for W.Triangulation buffer types.
# NOTE: For functions that return a single scalar ensure to use .item() to # NOTE: For functions that return a single scalar ensure to use .item() to
# return the Python float instead of numpy float # return the Python float instead of numpy float
@@ -58,7 +58,7 @@ def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
return abs(x - value) < tolerance return abs(x - value) < tolerance
def get_volume(geometry: ShapeType) -> float: def get_volume(geometry: W.Triangulation) -> float:
"""Calculates the total internal volume of a geometry """Calculates the total internal volume of a geometry
Volumes of non-manifold geometry will be unpredictable. Volumes of non-manifold geometry will be unpredictable.
@@ -88,7 +88,7 @@ def get_volume(geometry: ShapeType) -> float:
return abs(sum(volumes)) return abs(sum(volumes))
def get_x(geometry: ShapeType) -> float: def get_x(geometry: W.Triangulation) -> float:
"""Calculates the X length of the geometry """Calculates the X length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -98,7 +98,7 @@ def get_x(geometry: ShapeType) -> float:
return (np.max(verts_flat[0::3]) - np.min(verts_flat[0::3])).item() return (np.max(verts_flat[0::3]) - np.min(verts_flat[0::3])).item()
def get_y(geometry: ShapeType) -> float: def get_y(geometry: W.Triangulation) -> float:
"""Calculates the Y length of the geometry """Calculates the Y length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -108,7 +108,7 @@ def get_y(geometry: ShapeType) -> float:
return (np.max(verts_flat[1::3]) - np.min(verts_flat[1::3])).item() return (np.max(verts_flat[1::3]) - np.min(verts_flat[1::3])).item()
def get_z(geometry: ShapeType) -> float: def get_z(geometry: W.Triangulation) -> float:
"""Calculates the Z length of the geometry """Calculates the Z length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -118,7 +118,7 @@ def get_z(geometry: ShapeType) -> float:
return (np.max(verts_flat[2::3]) - np.min(verts_flat[2::3])).item() return (np.max(verts_flat[2::3]) - np.min(verts_flat[2::3])).item()
def get_max_xy(geometry: ShapeType) -> float: def get_max_xy(geometry: W.Triangulation) -> float:
"""Gets the maximum X or Y length of the geometry """Gets the maximum X or Y length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -127,7 +127,7 @@ def get_max_xy(geometry: ShapeType) -> float:
return max(get_x(geometry), get_y(geometry)) return max(get_x(geometry), get_y(geometry))
def get_max_xyz(geometry: ShapeType) -> float: def get_max_xyz(geometry: W.Triangulation) -> float:
"""Gets the maximum X, Y, or Z length of the geometry """Gets the maximum X, Y, or Z length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -136,7 +136,7 @@ def get_max_xyz(geometry: ShapeType) -> float:
return max(get_x(geometry), get_y(geometry), get_z(geometry)) return max(get_x(geometry), get_y(geometry), get_z(geometry))
def get_min_xyz(geometry: ShapeType) -> float: def get_min_xyz(geometry: W.Triangulation) -> float:
"""Gets the minimum X, Y, or Z length of the geometry """Gets the minimum X, Y, or Z length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -154,7 +154,7 @@ def get_shape_matrix(shape: ShapeElementType) -> MatrixType:
return np.frombuffer(shape.transformation_buffer, "d").reshape((4, 4), order="F") return np.frombuffer(shape.transformation_buffer, "d").reshape((4, 4), order="F")
def get_bbox_centroid(geometry: ShapeType) -> tuple[float, float, float]: def get_bbox_centroid(geometry: W.Triangulation) -> tuple[float, float, float]:
"""Calculates the bounding box centroid of the geometry """Calculates the bounding box centroid of the geometry
The centroid is in local coordinates relative to the object's placement. The centroid is in local coordinates relative to the object's placement.
@@ -166,7 +166,7 @@ def get_bbox_centroid(geometry: ShapeType) -> tuple[float, float, float]:
return (np.min(vertices_array, axis=0) + np.max(vertices_array, axis=0)) / 2 return (np.min(vertices_array, axis=0) + np.max(vertices_array, axis=0)) / 2
def get_vert_centroid(geometry: ShapeType) -> tuple[float, float, float]: def get_vert_centroid(geometry: W.Triangulation) -> tuple[float, float, float]:
"""Calculates the average vertex centroid of the geometry """Calculates the average vertex centroid of the geometry
The centroid is in local coordinates relative to the object's placement. The centroid is in local coordinates relative to the object's placement.
@@ -177,7 +177,9 @@ def get_vert_centroid(geometry: ShapeType) -> tuple[float, float, float]:
return np.mean(get_vertices(geometry), axis=0) return np.mean(get_vertices(geometry), axis=0)
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry: ShapeType) -> npt.NDArray[np.float64]: def get_element_bbox_centroid(
element: ifcopenshell.entity_instance, geometry: W.Triangulation
) -> npt.NDArray[np.float64]:
"""Calculates the element's bounding box centroid """Calculates the element's bounding box centroid
The centroid is in global coordinates. Note that if you have the shape, it The centroid is in global coordinates. Note that if you have the shape, it
@@ -194,7 +196,7 @@ def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry: S
return (mat @ np.array([*centroid, 1.0]))[0:3] return (mat @ np.array([*centroid, 1.0]))[0:3]
def get_shape_bbox_centroid(shape: ShapeElementType, geometry: ShapeType) -> npt.NDArray[np.float64]: def get_shape_bbox_centroid(shape: ShapeElementType, geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Calculates the shape's bounding box centroid """Calculates the shape's bounding box centroid
The centroid is in global coordinates. Note that if you do not have the The centroid is in global coordinates. Note that if you do not have the
@@ -208,7 +210,7 @@ def get_shape_bbox_centroid(shape: ShapeElementType, geometry: ShapeType) -> npt
return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3] return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3]
def get_vertices(geometry: ShapeType, is_2d: bool = False) -> npt.NDArray[np.float64]: def get_vertices(geometry: W.Triangulation, is_2d: bool = False) -> npt.NDArray[np.float64]:
"""Get all the vertices as a numpy array """Get all the vertices as a numpy array
Vertices are in local coordinates. Vertices are in local coordinates.
@@ -223,7 +225,7 @@ def get_vertices(geometry: ShapeType, is_2d: bool = False) -> npt.NDArray[np.flo
return np.frombuffer(geometry.verts_buffer, "d").reshape(-1, 3) return np.frombuffer(geometry.verts_buffer, "d").reshape(-1, 3)
def get_edges(geometry: ShapeType) -> npt.NDArray[np.int32]: def get_edges(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get all the edges as a numpy array """Get all the edges as a numpy array
Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...] Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...]
@@ -239,7 +241,7 @@ def get_edges(geometry: ShapeType) -> npt.NDArray[np.int32]:
return np.frombuffer(geometry.edges_buffer, dtype="i").reshape(-1, 2) return np.frombuffer(geometry.edges_buffer, dtype="i").reshape(-1, 2)
def get_faces(geometry: ShapeType) -> npt.NDArray[np.int32]: def get_faces(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get all the faces as a numpy array """Get all the faces as a numpy array
Faces are always triangulated. If the shape is a BRep and you want to get Faces are always triangulated. If the shape is a BRep and you want to get
@@ -254,7 +256,7 @@ def get_faces(geometry: ShapeType) -> npt.NDArray[np.int32]:
return np.frombuffer(geometry.faces_buffer, dtype="i").reshape(-1, 3) return np.frombuffer(geometry.faces_buffer, dtype="i").reshape(-1, 3)
def get_material_colors(geometry: ShapeType) -> npt.NDArray[np.float64]: def get_material_colors(geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Get material colors as a numpy array. """Get material colors as a numpy array.
:return: A numpy array listing RGBA color for each shape's material. :return: A numpy array listing RGBA color for each shape's material.
@@ -265,7 +267,7 @@ def get_material_colors(geometry: ShapeType) -> npt.NDArray[np.float64]:
return np.frombuffer(geometry.colors_buffer, dtype="d").reshape(-1, 4) return np.frombuffer(geometry.colors_buffer, dtype="d").reshape(-1, 4)
def get_normals(geometry: ShapeType) -> npt.NDArray[np.float64]: def get_normals(geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Get vertex normals as a numpy array. """Get vertex normals as a numpy array.
See geometry settings documentation for settings that affect normals. See geometry settings documentation for settings that affect normals.
@@ -276,12 +278,12 @@ def get_normals(geometry: ShapeType) -> npt.NDArray[np.float64]:
return np.frombuffer(geometry.normals_buffer, dtype="d").reshape(-1, 3) return np.frombuffer(geometry.normals_buffer, dtype="d").reshape(-1, 3)
def get_shape_material_styles(geometry: ShapeType) -> tuple[W.style, ...]: def get_shape_material_styles(geometry: W.Triangulation) -> tuple[W.style, ...]:
"""Get list of material styles.""" """Get list of material styles."""
return geometry.materials return geometry.materials
def get_faces_material_style_ids(geometry: ShapeType) -> npt.NDArray[np.int32]: def get_faces_material_style_ids(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get material styles ids for the geometry faces. """Get material styles ids for the geometry faces.
Return a list of corresponding indices of styles from get_shape_material_styles for each face. Return a list of corresponding indices of styles from get_shape_material_styles for each face.
@@ -290,12 +292,12 @@ def get_faces_material_style_ids(geometry: ShapeType) -> npt.NDArray[np.int32]:
return np.frombuffer(geometry.material_ids_buffer, dtype="i") return np.frombuffer(geometry.material_ids_buffer, dtype="i")
def get_faces_representation_item_ids(geometry: ShapeType) -> npt.NDArray[np.int32]: def get_faces_representation_item_ids(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get representation item ids for the geometry faces.""" """Get representation item ids for the geometry faces."""
return np.frombuffer(geometry.item_ids_buffer, dtype="i") return np.frombuffer(geometry.item_ids_buffer, dtype="i")
def get_edges_representation_item_ids(geometry: ShapeType) -> npt.NDArray[np.int32]: def get_edges_representation_item_ids(geometry: W.Triangulation) -> npt.NDArray[np.int32]:
"""Get representation item ids for the geometry edges. """Get representation item ids for the geometry edges.
Can be useful for geometry without faces and in general is more universal Can be useful for geometry without faces and in general is more universal
@@ -304,7 +306,7 @@ def get_edges_representation_item_ids(geometry: ShapeType) -> npt.NDArray[np.int
return np.frombuffer(geometry.edges_item_ids_buffer, dtype="i") return np.frombuffer(geometry.edges_item_ids_buffer, dtype="i")
def get_shape_vertices(shape: ShapeElementType, geometry: ShapeType) -> npt.NDArray[np.float64]: def get_shape_vertices(shape: ShapeElementType, geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Get the shape's vertices as a numpy array """Get the shape's vertices as a numpy array
Vertices are in global coordinates. If you do not have the shape, you can Vertices are in global coordinates. If you do not have the shape, you can
@@ -322,7 +324,7 @@ def get_shape_vertices(shape: ShapeElementType, geometry: ShapeType) -> npt.NDAr
return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1) return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1)
def get_element_vertices(element: ifcopenshell.entity_instance, geometry: ShapeType) -> npt.NDArray[np.float64]: def get_element_vertices(element: ifcopenshell.entity_instance, geometry: W.Triangulation) -> npt.NDArray[np.float64]:
"""Get the element's vertices as a numpy array """Get the element's vertices as a numpy array
Vertices are in global coordinates. Note that if you have the shape, it is Vertices are in global coordinates. Note that if you have the shape, it is
@@ -341,7 +343,7 @@ def get_element_vertices(element: ifcopenshell.entity_instance, geometry: ShapeT
return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1) return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1)
def get_bottom_elevation(geometry: ShapeType) -> float: def get_bottom_elevation(geometry: W.Triangulation) -> float:
"""Gets the lowest local Z ordinate of the geometry """Gets the lowest local Z ordinate of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -351,7 +353,7 @@ def get_bottom_elevation(geometry: ShapeType) -> float:
return np.min(verts_flat[2::3]).item() return np.min(verts_flat[2::3]).item()
def get_top_elevation(geometry: ShapeType) -> float: def get_top_elevation(geometry: W.Triangulation) -> float:
"""Gets the highest local Z ordinate of the geometry """Gets the highest local Z ordinate of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -361,7 +363,7 @@ def get_top_elevation(geometry: ShapeType) -> float:
return np.max(verts_flat[2::3]).item() return np.max(verts_flat[2::3]).item()
def get_shape_bottom_elevation(shape: ShapeType, geometry: ShapeType) -> float: def get_shape_bottom_elevation(shape: ShapeElementType, geometry: W.Triangulation) -> float:
"""Gets the lowest global Z ordinate of the shape """Gets the lowest global Z ordinate of the shape
If you do not have the shape, you can use :func:`get_element_bottom_elevation` If you do not have the shape, you can use :func:`get_element_bottom_elevation`
@@ -374,7 +376,7 @@ def get_shape_bottom_elevation(shape: ShapeType, geometry: ShapeType) -> float:
return min([v[2] for v in get_shape_vertices(shape, geometry)]) return min([v[2] for v in get_shape_vertices(shape, geometry)])
def get_shape_top_elevation(shape: ShapeType, geometry: ShapeType) -> float: def get_shape_top_elevation(shape: ShapeElementType, geometry: W.Triangulation) -> float:
"""Gets the highest global Z ordinate of the shape """Gets the highest global Z ordinate of the shape
If you do not have the shape, you can use :func:`get_element_top_elevation` If you do not have the shape, you can use :func:`get_element_top_elevation`
@@ -387,7 +389,7 @@ def get_shape_top_elevation(shape: ShapeType, geometry: ShapeType) -> float:
return max([v[2] for v in get_shape_vertices(shape, geometry)]) return max([v[2] for v in get_shape_vertices(shape, geometry)])
def get_element_bottom_elevation(element: ifcopenshell.entity_instance, geometry: ShapeType) -> float: def get_element_bottom_elevation(element: ifcopenshell.entity_instance, geometry: W.Triangulation) -> float:
"""Gets the lowest global Z ordinate of the element """Gets the lowest global Z ordinate of the element
Note that if you have the shape, it is more efficient to use Note that if you have the shape, it is more efficient to use
@@ -400,7 +402,7 @@ def get_element_bottom_elevation(element: ifcopenshell.entity_instance, geometry
return min([v[2] for v in get_element_vertices(element, geometry)]) return min([v[2] for v in get_element_vertices(element, geometry)])
def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry: ShapeType) -> float: def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry: W.Triangulation) -> float:
"""Gets the highest global Z ordinate of the element """Gets the highest global Z ordinate of the element
Note that if you have the shape, it is more efficient to use Note that if you have the shape, it is more efficient to use
@@ -446,7 +448,7 @@ def get_area_vf(vertices: npt.NDArray[np.float64], faces: npt.NDArray[np.int32])
return mesh_area.item() return mesh_area.item()
def get_area(geometry: ShapeType) -> float: def get_area(geometry: W.Triangulation) -> float:
"""Calculates the surface area of the geometry """Calculates the surface area of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -458,7 +460,7 @@ def get_area(geometry: ShapeType) -> float:
def get_side_area( def get_side_area(
geometry: ShapeType, geometry: W.Triangulation,
axis: AXIS_LITERAL = "Y", axis: AXIS_LITERAL = "Y",
direction: Optional[VectorType] = None, direction: Optional[VectorType] = None,
angle: float = 90.0, angle: float = 90.0,
@@ -508,7 +510,7 @@ def get_side_area(
return get_area_vf(vertices, filtered_faces) return get_area_vf(vertices, filtered_faces)
def get_max_side_area(geometry: ShapeType) -> float: def get_max_side_area(geometry: W.Triangulation) -> float:
"""Returns the maximum X, Y, or Z side area """Returns the maximum X, Y, or Z side area
See :func:`get_side_area` for how side area is calculated. See :func:`get_side_area` for how side area is calculated.
@@ -519,12 +521,12 @@ def get_max_side_area(geometry: ShapeType) -> float:
return max(get_side_area(geometry, axis="X"), get_side_area(geometry, axis="Y"), get_side_area(geometry, axis="Z")) return max(get_side_area(geometry, axis="X"), get_side_area(geometry, axis="Y"), get_side_area(geometry, axis="Z"))
def get_top_area(geometry: ShapeType) -> float: def get_top_area(geometry: W.Triangulation) -> float:
return get_side_area(geometry, axis="Z", angle=45) return get_side_area(geometry, axis="Z", angle=45)
def get_footprint_area( def get_footprint_area(
geometry: ShapeType, geometry: W.Triangulation,
axis: AXIS_LITERAL = "Z", axis: AXIS_LITERAL = "Z",
direction: Optional[VECTOR_3D] = None, direction: Optional[VECTOR_3D] = None,
) -> float: ) -> float:
@@ -602,7 +604,7 @@ def get_footprint_area(
return unioned_polygon.area return unioned_polygon.area
def get_outer_surface_area(geometry: ShapeType) -> float: def get_outer_surface_area(geometry: W.Triangulation) -> float:
"""Calculates the outer surface area (i.e. all sides except for top and bottom) """Calculates the outer surface area (i.e. all sides except for top and bottom)
This is typically useful for calculating painted areas of beams which This is typically useful for calculating painted areas of beams which
@@ -628,7 +630,7 @@ def get_outer_surface_area(geometry: ShapeType) -> float:
return get_area_vf(vertices, filtered_faces) return get_area_vf(vertices, filtered_faces)
def get_footprint_perimeter(geometry: ShapeType) -> float: def get_footprint_perimeter(geometry: W.Triangulation) -> float:
"""Calculates the footprint perimeter of the geometry """Calculates the footprint perimeter of the geometry
All faces with a negative Z normal are considered and the distance of all All faces with a negative Z normal are considered and the distance of all
@@ -731,7 +733,7 @@ def get_base_extrusions(element: ifcopenshell.entity_instance) -> Union[list[ifc
return extrusions return extrusions
def get_total_edge_length(geometry: ShapeType) -> float: def get_total_edge_length(geometry: W.Triangulation) -> float:
"""Calculates the total length of edges in a given geometry. """Calculates the total length of edges in a given geometry.
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell