mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
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:
@@ -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
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user