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 """