mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-30 03:59:55 +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:
@@ -1935,7 +1935,9 @@ class IfcImporter:
|
|||||||
|
|
||||||
# See bug 3546
|
# See bug 3546
|
||||||
# ios_edges holds true edges that aren't triangulated.
|
# 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.add(num_vertices)
|
||||||
mesh.vertices.foreach_set("co", verts)
|
mesh.vertices.foreach_set("co", verts)
|
||||||
|
|||||||
@@ -17,10 +17,13 @@
|
|||||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
from typing import Literal, Iterable
|
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:
|
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
|
:param x: The +X vector / axis of the matrix
|
||||||
:type x: iterable[float]
|
:type x: iterable[float]
|
||||||
:return: A 4x4 numpy matrix
|
:return: A 4x4 numpy matrix
|
||||||
:rtype: np.ndarray[np.ndarray[float]]
|
:rtype: MatrixType
|
||||||
"""
|
"""
|
||||||
x = x / np.linalg.norm(x)
|
x = x / np.linalg.norm(x)
|
||||||
z = z / np.linalg.norm(z)
|
z = z / np.linalg.norm(z)
|
||||||
@@ -59,7 +62,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
|
|||||||
:param placement: The IfcLocalPlacement enitity
|
:param placement: The IfcLocalPlacement enitity
|
||||||
:type placement: ifcopenshell.entity_instance.entity_instance
|
:type placement: ifcopenshell.entity_instance.entity_instance
|
||||||
:return: A 4x4 numpy matrix
|
:return: A 4x4 numpy matrix
|
||||||
:rtype: np.ndarray[np.ndarray[float]]
|
:rtype: MatrixType
|
||||||
"""
|
"""
|
||||||
ifc_class = placement.is_a()
|
ifc_class = placement.is_a()
|
||||||
if ifc_class in ("IfcAxis2Placement3D", "IfcAxis2PlacementLinear"):
|
if ifc_class in ("IfcAxis2Placement3D", "IfcAxis2PlacementLinear"):
|
||||||
@@ -72,7 +75,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
|
|||||||
ifc_class = location.is_a()
|
ifc_class = location.is_a()
|
||||||
print(
|
print(
|
||||||
f'WARNING. Placement location of type "{ifc_class}" '
|
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)
|
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
|
:param placement: The IfcLocalPlacement entity
|
||||||
:type placement: ifcopenshell.entity_instance.entity_instance
|
:type placement: ifcopenshell.entity_instance.entity_instance
|
||||||
:return: A 4x4 numpy matrix
|
:return: A 4x4 numpy matrix
|
||||||
:rtype: np.ndarray[np.ndarray[float]]
|
:rtype: MatrixType
|
||||||
"""
|
"""
|
||||||
if placement is None:
|
if placement is None:
|
||||||
return np.eye(4)
|
return np.eye(4)
|
||||||
@@ -137,7 +140,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) ->
|
|||||||
:param item: The IfcCartesianTransformationOperator entity
|
:param item: The IfcCartesianTransformationOperator entity
|
||||||
:type item: ifcopenshell.entity_instance.entity_instance
|
:type item: ifcopenshell.entity_instance.entity_instance
|
||||||
:return: A 4x4 numpy transformation matrix
|
:return: A 4x4 numpy transformation matrix
|
||||||
:rtype: np.ndarray[np.ndarray[float]]
|
:rtype: MatrixType
|
||||||
"""
|
"""
|
||||||
origin = np.array(inst.LocalOrigin.Coordinates)
|
origin = np.array(inst.LocalOrigin.Coordinates)
|
||||||
axis1 = np.array((1.0, 0.0, 0.0))
|
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
|
:param item: The IfcMappedItem entity
|
||||||
:type item: ifcopenshell.entity_instance.entity_instance
|
:type item: ifcopenshell.entity_instance.entity_instance
|
||||||
:return: A 4x4 numpy transformation matrix
|
:return: A 4x4 numpy transformation matrix
|
||||||
:rtype: np.ndarray[np.ndarray[float]]
|
:rtype: MatrixType
|
||||||
"""
|
"""
|
||||||
m4 = get_axis2placement(item.MappingSource.MappingOrigin)
|
m4 = get_axis2placement(item.MappingSource.MappingOrigin)
|
||||||
# TODO 2d
|
# 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).
|
radians. Defaults to true (i.e. degrees).
|
||||||
:type is_degrees: bool
|
:type is_degrees: bool
|
||||||
:return: A 4x4 numpy rotation matrix
|
:return: A 4x4 numpy rotation matrix
|
||||||
:rtype: np.ndarray[np.ndarray[float]]
|
:rtype: MatrixType
|
||||||
"""
|
"""
|
||||||
theta = np.radians(angle) if is_degrees else angle
|
theta = np.radians(angle) if is_degrees else angle
|
||||||
cos, sin = np.cos(theta), np.sin(theta)
|
cos, sin = np.cos(theta), np.sin(theta)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import shapely
|
import shapely
|
||||||
import shapely.ops
|
import shapely.ops
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
import ifcopenshell.util.placement
|
import ifcopenshell.util.placement
|
||||||
import ifcopenshell.util.representation
|
import ifcopenshell.util.representation
|
||||||
@@ -28,6 +29,9 @@ tol = 1e-6
|
|||||||
AXIS_LITERAL = Literal["X", "Y", "Z"]
|
AXIS_LITERAL = Literal["X", "Y", "Z"]
|
||||||
VECTOR_3D = tuple[float, float, float]
|
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:
|
def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
|
||||||
"""Checks whether a value is equivalent to X given a tolerance
|
"""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)
|
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
|
"""Formats the transformation matrix of a shape as a 4x4 numpy array
|
||||||
|
|
||||||
:param shape: Shape output calculated by IfcOpenShell
|
:param shape: Shape output calculated by IfcOpenShell
|
||||||
:type shape: shape
|
:type shape: shape
|
||||||
:return: A 4x4 numpy array representing the transformation matrix
|
:return: A 4x4 numpy array representing the transformation matrix
|
||||||
:rtype: np.array
|
:rtype: MatrixType
|
||||||
"""
|
"""
|
||||||
m = shape.transformation.matrix.data
|
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]))
|
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
|
"""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.
|
||||||
@@ -133,11 +137,14 @@ def get_bbox_centroid(geometry) -> tuple[float]:
|
|||||||
:param geometry: Geometry output calculated by IfcOpenShell
|
:param geometry: Geometry output calculated by IfcOpenShell
|
||||||
:type geometry: geometry
|
:type geometry: geometry
|
||||||
:return: A tuple representing the XYZ centroid
|
: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)]
|
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)]
|
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)]
|
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)
|
minx = min(x_values)
|
||||||
maxx = max(x_values)
|
maxx = max(x_values)
|
||||||
miny = min(y_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))
|
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
|
"""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
|
||||||
@@ -158,16 +165,16 @@ def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -
|
|||||||
:param geometry: Geometry output calculated by IfcOpenShell
|
:param geometry: Geometry output calculated by IfcOpenShell
|
||||||
:type geometry: geometry
|
:type geometry: geometry
|
||||||
:return: A tuple representing the XYZ centroid
|
:return: A tuple representing the XYZ centroid
|
||||||
:rtype: tuple[float]
|
:rtype: npt.NDArray[np.float64]
|
||||||
"""
|
"""
|
||||||
centroid = get_bbox_centroid(geometry)
|
centroid = get_bbox_centroid(geometry)
|
||||||
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
|
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)
|
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||||
return (mat @ np.array([*centroid, 1.0]))[0:3]
|
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
|
"""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
|
||||||
@@ -178,13 +185,13 @@ def get_shape_bbox_centroid(shape, geometry) -> tuple[float]:
|
|||||||
:param geometry: Geometry output calculated by IfcOpenShell
|
:param geometry: Geometry output calculated by IfcOpenShell
|
||||||
:type geometry: geometry
|
:type geometry: geometry
|
||||||
:return: A tuple representing the XYZ centroid
|
:return: A tuple representing the XYZ centroid
|
||||||
:rtype: tuple[float]
|
:rtype: npt.NDArray[np.float64]
|
||||||
"""
|
"""
|
||||||
centroid = get_bbox_centroid(geometry)
|
centroid = get_bbox_centroid(geometry)
|
||||||
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) -> np.ndarray[np.ndarray[float]]:
|
def get_vertices(geometry) -> 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.
|
||||||
@@ -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)])
|
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
|
"""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], ...]
|
||||||
@@ -215,10 +222,10 @@ def get_edges(geometry) -> np.ndarray[np.ndarray[int]]:
|
|||||||
:rtype: np.array[np.array[int]]
|
:rtype: np.array[np.array[int]]
|
||||||
"""
|
"""
|
||||||
edges = geometry.edges
|
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
|
"""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
|
||||||
@@ -232,10 +239,10 @@ def get_faces(geometry) -> np.ndarray[np.ndarray[int]]:
|
|||||||
:rtype: np.array[np.array[int]]
|
:rtype: np.array[np.array[int]]
|
||||||
"""
|
"""
|
||||||
faces = geometry.faces
|
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
|
"""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
|
||||||
@@ -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)
|
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
|
"""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
|
||||||
@@ -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)])
|
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
|
"""Gets the bounding box of vertices
|
||||||
|
|
||||||
:param vertices: An iterable 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]))
|
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
|
"""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.
|
: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/>.
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
import collections
|
import collections
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
@@ -533,13 +534,13 @@ class ShapeBuilder:
|
|||||||
|
|
||||||
def create_axis2_placement_3d_from_matrix(
|
def create_axis2_placement_3d_from_matrix(
|
||||||
self,
|
self,
|
||||||
matrix: Union[np.ndarray, None] = None,
|
matrix: Union[npt.NDArray[np.float64], None] = None,
|
||||||
) -> ifcopenshell.entity_instance:
|
) -> ifcopenshell.entity_instance:
|
||||||
"""
|
"""
|
||||||
Create IfcAxis2Placement3D from numpy matrix.
|
Create IfcAxis2Placement3D from numpy matrix.
|
||||||
|
|
||||||
:param matrix: 4x4 transformation matrix, defaults to `np.eye(4)`
|
: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
|
:return: IfcAxis2Placement3D
|
||||||
:rtype: ifcopenshell.entity_instance
|
:rtype: ifcopenshell.entity_instance
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user