New documentation for shape utility

This commit is contained in:
Dion Moult
2023-10-06 13:04:54 +11:00
parent eb836436b9
commit ed3e9f3347
@@ -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