From 83b5494c81af41465d23a1035f45afdaebb2d6d4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 6 Oct 2023 13:04:54 +1100 Subject: [PATCH] New documentation for shape utility --- .../ifcopenshell/util/shape.py | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py index d4da57390c..cc9e9dbce5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py @@ -25,12 +25,33 @@ tol = 1e-6 def is_x(value, x, tolerance=None): + """Checks whether a value is equivalent to X given a tolerance + + :param value: Input value + :type value: float + :param x: The value to compare to + :type x: float + :param tolerance: The tolerance to use. Defaults to 1e-6. + :type tolerance: float + :return: True or false + :rtype: bool + """ if tolerance is None: tolerance = tol return abs(x - value) < tolerance def get_volume(geometry): + """Calculates the total internal volume of a geometry + + Volumes of non-manifold geometry will be unpredictable. + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The volume in m3 + :rtype: float + """ + # https://stackoverflow.com/questions/1406029/how-to-calculate-the-volume-of-a-3d-mesh-object-the-surface-of-which-is-made-up def signed_triangle_volume(p1, p2, p3): v321 = p3[0] * p2[1] * p1[2] @@ -52,26 +73,63 @@ def get_volume(geometry): def get_x(geometry): + """Calculates the X length of the geometry + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The X dimension + :rtype: float + """ x_values = [geometry.verts[i] for i in range(0, len(geometry.verts), 3)] return max(x_values) - min(x_values) def get_y(geometry): + """Calculates the Y length of the geometry + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The Y dimension + :rtype: float + """ y_values = [geometry.verts[i + 1] for i in range(0, len(geometry.verts), 3)] return max(y_values) - min(y_values) def get_z(geometry): + """Calculates the Z length of the geometry + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The Z dimension + :rtype: float + """ z_values = [geometry.verts[i + 2] for i in range(0, len(geometry.verts), 3)] return max(z_values) - min(z_values) def get_shape_matrix(shape): + """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 + """ 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): + """Calculates the bounding box centroid of the geometry + + The centroid is in local coordinates relative to the object's placement. + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: A tuple representing the XYZ centroid + :rtype: tuple[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)] @@ -85,6 +143,18 @@ def get_bbox_centroid(geometry): def get_element_bbox_centroid(element, geometry): + """Calculates the element's bounding box centroid + + The centroid is in global coordinates. Note that if you have the shape, it + is more efficient to use ``get_shape_bbox_centroid``. + + :param element: The element occurrence + :type: ifcopenshell.entity_instance.entity_instance + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: A tuple representing the XYZ centroid + :rtype: tuple[float] + """ centroid = get_bbox_centroid(geometry) if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"): return centroid @@ -93,32 +163,107 @@ def get_element_bbox_centroid(element, geometry): def get_shape_bbox_centroid(shape, geometry): + """Calculates the shape's bounding box centroid + + The centroid is in global coordinates. Note that if you do not have the + shape, you can use ``get_element_bbox_centroid``. + + :param shape: Shape output calculated by IfcOpenShell + :type shape: shape + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: A tuple representing the XYZ centroid + :rtype: tuple[float] + """ centroid = get_bbox_centroid(geometry) return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3] def get_vertices(geometry): + """Get all the vertices as a numpy array + + Vertices are in local coordinates. + + Results are a nested numpy array e.g. [[v1x, v1y, v1z], [v2x, v2y, v2z], ...] + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: A numpy array listing all the vertices. Each vertex is a numpy array with XYZ coordinates. + :rtype: np.array[np.array[float]] + """ verts = geometry.verts 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): + """Get all the edges as a numpy array + + Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...] + + Note that although geometry always holds triangulated faces, edges will + represent the original tessellation or BRep's faces, which may be quads or + ngons. + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: A numpy array listing all the edges. Each edge is a numpy array with two vertex indices. + :rtype: np.array[np.array[int]] + """ edges = geometry.edges return [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)] def get_faces(geometry): + """Get all the faces as a numpy array + + Faces are always triangulated. + + Results are a nested numpy array e.g. [[f1v1, f1v2, f1v3], [f2v1, f2v2, f2v3], ...] + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: A numpy array listing all the faces. Each face is a numpy array with three vertex indices. + :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)] def get_shape_vertices(shape, geometry): + """Get the shape's vertices as a numpy array + + Vertices are in global coordinates. If you do not have the shape, you can + use ``get_element_vertices``. + + Results are a nested numpy array e.g. [[v1x, v1y, v1z], [v2x, v2y, v2z], ...] + + :param shape: Shape output calculated by IfcOpenShell + :type shape: shape + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: A numpy array listing all the vertices. Each vertex is a numpy array with XYZ coordinates. + :rtype: np.array[np.array[float]] + """ verts = get_vertices(geometry) mat = get_shape_matrix(shape) return np.array([mat @ np.array([verts[i], verts[i + 1], verts[i + 2]]) for i in range(0, len(verts), 3)]) def get_element_vertices(element, geometry): + """Get the element's vertices as a numpy array + + Vertices are in global coordinates. Note that if you have the shape, it is + more efficient to use ``get_shape_vertices``. + + Results are a nested numpy array e.g. [[v1x, v1y, v1z], [v2x, v2y, v2z], ...] + + :param element: The element occurrence + :type: ifcopenshell.entity_instance.entity_instance + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: A numpy array listing all the vertices. Each vertex is a numpy array with XYZ coordinates. + :rtype: np.array[np.array[float]] + """ verts = get_vertices(geometry) if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"): return verts @@ -127,32 +272,104 @@ def get_element_vertices(element, geometry): def get_bottom_elevation(geometry): + """Gets the lowest local Z ordinate of the geometry + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The Z value + :rtype: float + """ z_values = [geometry.verts[i + 2] for i in range(0, len(geometry.verts), 3)] return min(z_values) def get_top_elevation(geometry): + """Gets the highest local Z ordinate of the geometry + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The Z value + :rtype: float + """ z_values = [geometry.verts[i + 2] for i in range(0, len(geometry.verts), 3)] return max(z_values) def get_shape_bottom_elevation(shape, geometry): + """Gets the lowest global Z ordinate of the shape + + If you do not have the shape, you can use ``get_element_bottom_elevation`` + instead. + + :param shape: Shape output calculated by IfcOpenShell + :type shape: shape + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The Z value + :rtype: float + """ return min([v[2] for v in get_shape_vertices(shape, geometry)]) def get_shape_top_elevation(shape, geometry): + """Gets the highest global Z ordinate of the shape + + If you do not have the shape, you can use ``get_element_top_elevation`` + instead. + + :param shape: Shape output calculated by IfcOpenShell + :type shape: shape + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The Z value + :rtype: float + """ return max([v[2] for v in get_shape_vertices(shape, geometry)]) def get_element_bottom_elevation(element, geometry): + """Gets the lowest global Z ordinate of the element + + Note that if you have the shape, it is more efficient to use + ``get_shape_bottom_elevation``. + + :param element: The element occurrence + :type: ifcopenshell.entity_instance.entity_instance + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The Z value + :rtype: float + """ return min([v[2] for v in get_element_vertices(element, geometry)]) def get_element_top_elevation(element, geometry): + """Gets the highest global Z ordinate of the element + + Note that if you have the shape, it is more efficient to use + ``get_shape_top_elevation``. + + :param element: The element occurrence + :type: ifcopenshell.entity_instance.entity_instance + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The Z value + :rtype: float + """ return max([v[2] for v in get_element_vertices(element, geometry)]) def get_bbox(vertices): + """Gets the bounding box of vertices + + :param vertices: An iterable of vertices + :type: iterable + :return: The bounding box value represented as a tuple of two numpy arrays. + The first holds the bottom left corner and the second holds the top + right. E.g. (np.array([minx, miny, minz]), np.array([maxx, maxy, + maxz])) + :rtype: tuple[np.array[float]] + """ x_values = [v[0] for v in vertices] y_values = [v[1] for v in vertices] z_values = [v[2] for v in vertices] @@ -166,6 +383,15 @@ def get_bbox(vertices): def get_area_vf(vertices, 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. + :type: np.array[iterable[float]] + :param faces: A list of faces, such as returned from get_faces. + :type: np.array[iterable[int]] + :return: The surface area. + :rtype: float + """ # Calculate the triangle normal vectors v1 = vertices[faces[:, 1]] - vertices[faces[:, 0]] v2 = vertices[faces[:, 2]] - vertices[faces[:, 0]] @@ -181,6 +407,13 @@ def get_area_vf(vertices, faces): def get_area(geometry): + """Calculates the surface area of the geometry + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The surface area. + :rtype: float + """ verts = geometry.verts faces = geometry.faces vertices = np.array([[verts[i], verts[i + 1], verts[i + 2]] for i in range(0, len(verts), 3)]) @@ -189,6 +422,27 @@ def get_area(geometry): def get_side_area(geometry, axis="Y", direction=None): + """Calculates the total surface area of surfaces that are visible from the specified axis + + This is typically useful for calculating elevational areas. For example, + you might want to calculate the side area of a wall (i.e. only one side, + not both). + + Surfaces do not need to be exactly perpendicular in the direction of the + specified axis. A surface is counted so long as it is visible from that + axis. + + Note that this calculates the actual area, not the projected 2D area. If + you want the projected area, use ``get_footprint_area``. + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :param axis: Either X, Y, or Z. Defaults to Y, which is used for standard + walls. + :type axis: str + :return: The surface area. + :rtype: float + """ if direction is None: direction = {"X": (1.0, 0.0, 0.0), "Y": (0.0, 1.0, 0.0), "Z": (0.0, 0.0, 1.0)}[axis] @@ -216,6 +470,29 @@ def get_side_area(geometry, axis="Y", direction=None): def get_footprint_area(geometry, axis="Z", direction=None): + """Calculates the total footprint (i.e. projected) surface area visible from along an axis + + This is typically useful for calculating footprint areas. For example, you + might want to calculate the top-down footprint area of a slab, ignoring + slopes in the slab. + + Surfaces do not need to be exactly perpendicular in the direction of the + specified axis. A surface is counted so long as it is visible from that + axis. + + Note that this calculates the 2D projected area, not the actual surface + area. If you want the actual area, use ``get_side_area``. + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :param axis: Either X, Y, or Z. Defaults to Z. + :type axis: str,optional + :param direction: An XYZ iterable (e.g. (0., 0., 1.)). If a direction + vector is specified, this overrides the axis argument. + :type axis: iterable[float],optional + :return: The surface area. + :rtype: float + """ if direction is None: direction = {"X": (1.0, 0.0, 0.0), "Y": (0.0, 1.0, 0.0), "Z": (0.0, 0.0, 1.0)}[axis] @@ -248,6 +525,16 @@ def get_footprint_area(geometry, axis="Z", direction=None): def get_outer_surface_area(geometry): + """Calculates the outer surface area (i.e. all sides except for top and bottom) + + This is typically useful for calculating painted areas of beams which + exclude the end faces (at the minimum and maximum local Z). + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The surface area. + :rtype: float + """ verts = geometry.verts faces = geometry.faces vertices = np.array([[verts[i], verts[i + 1], verts[i + 2]] for i in range(0, len(verts), 3)]) @@ -268,6 +555,16 @@ def get_outer_surface_area(geometry): def get_footprint_perimeter(geometry): + """Calculates the footprint perimeter of the geometry + + All faces with a negative Z normal are considered and the distance of all + perimeter edges are totaled. + + :param geometry: Geometry output calculated by IfcOpenShell + :type geometry: geometry + :return: The perimeter length + :rtype: float + """ verts = geometry.verts faces = geometry.faces vertices = np.array([[verts[i], verts[i + 1], verts[i + 2]] for i in range(0, len(verts), 3)]) @@ -307,6 +604,16 @@ def get_footprint_perimeter(geometry): def get_profiles(element): + """Gets all 2D profiles used in the definition of a parametric shape + + Profiles may be retrieved either from material profile sets or from swept + solid extrusions. This is useful for later doing 2D take-off from profiles. + + :param element: The element occurrence + :type: ifcopenshell.entity_instance.entity_instance + :return: A list of profiles + :rtype: list[ifcopenshell.entity_instance.entity_instance] + """ material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) if material and material.is_a("IfcMaterialProfileSet"): return [mp.Profile for mp in material.MaterialProfiles] @@ -314,6 +621,13 @@ def get_profiles(element): def get_extrusions(element): + """Gets all extruded area solids used to define an element's model body geometry + + :param element: The element occurrence + :type: ifcopenshell.entity_instance.entity_instance + :return: A list of extrusion representation items + :rtype: list[ifcopenshell.entity_instance.entity_instance] + """ representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return