mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
New documentation for shape utility
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user