diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py
index 481e23817e..d7b6bb915b 100644
--- a/src/blenderbim/blenderbim/bim/import_ifc.py
+++ b/src/blenderbim/blenderbim/bim/import_ifc.py
@@ -1935,7 +1935,9 @@ class IfcImporter:
# See bug 3546
# ios_edges holds true edges that aren't triangulated.
- mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry)))
+ #
+ # we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
+ mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
diff --git a/src/ifcopenshell-python/ifcopenshell/util/placement.py b/src/ifcopenshell-python/ifcopenshell/util/placement.py
index 19fd3835f8..a5c3312dab 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/placement.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/placement.py
@@ -17,10 +17,13 @@
# along with IfcOpenShell. If not, see .
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)
diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py
index 798000005a..931f4c0327 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/shape.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py
@@ -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.
diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
index 13efca8da5..fdea29fe14 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see .
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
"""