fix numpy typing #4579

On older numpy versions, np.ndarray was less forgiving and wasn't allowing passing 1 argument instead of required 2.

And turned out numpy doesn't yet have typing for shapes (https://github.com/numpy/numpy/issues/16544), so all matrices and other shapes specified as `npt.NDArray[np.float64]`.

Fixed type discrepancies for `get_edges` and `get_faces` and also had to fix `import_ifc` as Blender apparently has problems with storing np.int32 in custom attributes (https://projects.blender.org/blender/blender/issues/121072), tested that Blender is okay with np.int32 in other cases we had (addressing BMesh.verts[i] where `i` is np.int32).
This commit is contained in:
Andrej730
2024-04-25 15:55:32 +05:00
parent 721466c429
commit 5b877b549a
4 changed files with 42 additions and 29 deletions
@@ -17,10 +17,13 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import numpy.typing as npt
import ifcopenshell
from typing import Literal, Iterable
MatrixType = np.ndarray[np.ndarray[float]]
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixType:
@@ -36,7 +39,7 @@ def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixTyp
:param x: The +X vector / axis of the matrix
:type x: iterable[float]
:return: A 4x4 numpy matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
x = x / np.linalg.norm(x)
z = z / np.linalg.norm(z)
@@ -59,7 +62,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
:param placement: The IfcLocalPlacement enitity
:type placement: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
ifc_class = placement.is_a()
if ifc_class in ("IfcAxis2Placement3D", "IfcAxis2PlacementLinear"):
@@ -72,7 +75,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
ifc_class = location.is_a()
print(
f'WARNING. Placement location of type "{ifc_class}" '
f'is not yet supported and placement {placement} may be placed incorrectly.'
f"is not yet supported and placement {placement} may be placed incorrectly."
)
o = (0.0, 0.0, 0.0)
@@ -117,7 +120,7 @@ def get_local_placement(placement: ifcopenshell.entity_instance) -> MatrixType:
:param placement: The IfcLocalPlacement entity
:type placement: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
if placement is None:
return np.eye(4)
@@ -137,7 +140,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) ->
:param item: The IfcCartesianTransformationOperator entity
:type item: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy transformation matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
origin = np.array(inst.LocalOrigin.Coordinates)
axis1 = np.array((1.0, 0.0, 0.0))
@@ -183,7 +186,7 @@ def get_mappeditem_transformation(item: ifcopenshell.entity_instance) -> MatrixT
:param item: The IfcMappedItem entity
:type item: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy transformation matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
m4 = get_axis2placement(item.MappingSource.MappingOrigin)
# TODO 2d
@@ -219,7 +222,7 @@ def rotation(angle: float, axis: Literal["X", "Y", "Z"], is_degrees=True) -> Mat
radians. Defaults to true (i.e. degrees).
:type is_degrees: bool
:return: A 4x4 numpy rotation matrix
:rtype: np.ndarray[np.ndarray[float]]
:rtype: MatrixType
"""
theta = np.radians(angle) if is_degrees else angle
cos, sin = np.cos(theta), np.sin(theta)
@@ -19,6 +19,7 @@
import shapely
import shapely.ops
import numpy as np
import numpy.typing as npt
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
@@ -28,6 +29,9 @@ tol = 1e-6
AXIS_LITERAL = Literal["X", "Y", "Z"]
VECTOR_3D = tuple[float, float, float]
MatrixType = npt.NDArray[np.float64]
"""`npt.NDArray[np.float64]`"""
def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
"""Checks whether a value is equivalent to X given a tolerance
@@ -113,19 +117,19 @@ def get_z(geometry) -> float:
return max(z_values) - min(z_values)
def get_shape_matrix(shape) -> np.ndarray:
def get_shape_matrix(shape) -> MatrixType:
"""Formats the transformation matrix of a shape as a 4x4 numpy array
:param shape: Shape output calculated by IfcOpenShell
:type shape: shape
:return: A 4x4 numpy array representing the transformation matrix
:rtype: np.array
:rtype: MatrixType
"""
m = shape.transformation.matrix.data
return np.array(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]))
def get_bbox_centroid(geometry) -> tuple[float]:
def get_bbox_centroid(geometry) -> tuple[float, float, float]:
"""Calculates the bounding box centroid of the geometry
The centroid is in local coordinates relative to the object's placement.
@@ -133,11 +137,14 @@ def get_bbox_centroid(geometry) -> tuple[float]:
:param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry
:return: A tuple representing the XYZ centroid
:rtype: tuple[float]
:rtype: tuple[float, float, float]
"""
x_values = [geometry.verts[i] for i in range(0, len(geometry.verts), 3)]
y_values = [geometry.verts[i + 1] for i in range(0, len(geometry.verts), 3)]
z_values = [geometry.verts[i + 2] for i in range(0, len(geometry.verts), 3)]
x_values: list[float]
y_values: list[float]
z_values: list[float]
minx = min(x_values)
maxx = max(x_values)
miny = min(y_values)
@@ -147,7 +154,7 @@ def get_bbox_centroid(geometry) -> tuple[float]:
return (minx + ((maxx - minx) / 2), miny + ((maxy - miny) / 2), minz + ((maxz - minz) / 2))
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> tuple[float]:
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> npt.NDArray[np.float64]:
"""Calculates the element's bounding box centroid
The centroid is in global coordinates. Note that if you have the shape, it
@@ -158,16 +165,16 @@ def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -
:param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry
:return: A tuple representing the XYZ centroid
:rtype: tuple[float]
:rtype: npt.NDArray[np.float64]
"""
centroid = get_bbox_centroid(geometry)
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
return centroid
return np.array(centroid)
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
return (mat @ np.array([*centroid, 1.0]))[0:3]
def get_shape_bbox_centroid(shape, geometry) -> tuple[float]:
def get_shape_bbox_centroid(shape, geometry) -> npt.NDArray[np.float64]:
"""Calculates the shape's bounding box centroid
The centroid is in global coordinates. Note that if you do not have the
@@ -178,13 +185,13 @@ def get_shape_bbox_centroid(shape, geometry) -> tuple[float]:
:param geometry: Geometry output calculated by IfcOpenShell
:type geometry: geometry
:return: A tuple representing the XYZ centroid
:rtype: tuple[float]
:rtype: npt.NDArray[np.float64]
"""
centroid = get_bbox_centroid(geometry)
return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3]
def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]:
def get_vertices(geometry) -> npt.NDArray[np.float64]:
"""Get all the vertices as a numpy array
Vertices are in local coordinates.
@@ -200,7 +207,7 @@ def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]:
return np.array([np.array([verts[i], verts[i + 1], verts[i + 2]]) for i in range(0, len(verts), 3)])
def get_edges(geometry) -> np.ndarray[np.ndarray[int]]:
def get_edges(geometry) -> npt.NDArray[np.int32]:
"""Get all the edges as a numpy array
Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...]
@@ -215,10 +222,10 @@ def get_edges(geometry) -> np.ndarray[np.ndarray[int]]:
:rtype: np.array[np.array[int]]
"""
edges = geometry.edges
return [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
return np.array([[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)])
def get_faces(geometry) -> np.ndarray[np.ndarray[int]]:
def get_faces(geometry) -> npt.NDArray[np.int32]:
"""Get all the faces as a numpy array
Faces are always triangulated. If the shape is a BRep and you want to get
@@ -232,10 +239,10 @@ def get_faces(geometry) -> np.ndarray[np.ndarray[int]]:
:rtype: np.array[np.array[int]]
"""
faces = geometry.faces
return [[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)]
return np.array([[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)])
def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]:
def get_shape_vertices(shape, geometry) -> npt.NDArray[np.float64]:
"""Get the shape's vertices as a numpy array
Vertices are in global coordinates. If you do not have the shape, you can
@@ -255,7 +262,7 @@ def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]:
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) -> np.ndarray[np.ndarray[float]]:
def get_element_vertices(element: ifcopenshell.entity_instance, geometry) -> npt.NDArray[np.float64]:
"""Get the element's vertices as a numpy array
Vertices are in global coordinates. Note that if you have the shape, it is
@@ -365,7 +372,7 @@ def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry) -
return max([v[2] for v in get_element_vertices(element, geometry)])
def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]:
def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""Gets the bounding box of vertices
:param vertices: An iterable of vertices
@@ -388,7 +395,7 @@ def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]:
return (np.array([minx, miny, minz]), np.array([maxx, maxy, maxz]))
def get_area_vf(vertices: np.ndarray[VECTOR_3D], faces: np.ndarray[Iterable[int]]) -> float:
def get_area_vf(vertices: npt.NDArray[np.float64], faces: npt.NDArray[np.int32]) -> float:
"""Calculates the surface area given a list of vertices and triangulated faces
:param vertices: A list of 3D vertices, such as returned from get_vertices.
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import numpy.typing as npt
import collections
import collections.abc
import ifcopenshell
@@ -533,13 +534,13 @@ class ShapeBuilder:
def create_axis2_placement_3d_from_matrix(
self,
matrix: Union[np.ndarray, None] = None,
matrix: Union[npt.NDArray[np.float64], None] = None,
) -> ifcopenshell.entity_instance:
"""
Create IfcAxis2Placement3D from numpy matrix.
:param matrix: 4x4 transformation matrix, defaults to `np.eye(4)`
:type matrix: np.array[np.array[float]], optional
:type matrix: npt.NDArray[np.float64], optional
:return: IfcAxis2Placement3D
:rtype: ifcopenshell.entity_instance
"""