This commit is contained in:
Andrej730
2024-03-21 14:17:02 +05:00
parent afbf001694
commit ac191ac71d
4 changed files with 83 additions and 40 deletions
@@ -18,6 +18,7 @@
import bpy import bpy
import importlib import importlib
import importlib.util
from pathlib import Path from pathlib import Path
import ifcpatch import ifcpatch
from blenderbim.bim.prop import StrProperty, Attribute from blenderbim.bim.prop import StrProperty, Attribute
@@ -18,9 +18,16 @@
import numpy as np import numpy as np
import ifcopenshell import ifcopenshell
import ifcopenshell.util.placement
from typing import Optional, Union, TypedDict
def get_context(ifc_file, context, subcontext=None, target_view=None): def get_context(
ifc_file: ifcopenshell.file,
context: str,
subcontext: Optional[str] = None,
target_view: Optional[str] = None,
) -> Union[ifcopenshell.entity_instance, None]:
if subcontext or target_view: if subcontext or target_view:
elements = ifc_file.by_type("IfcGeometricRepresentationSubContext") elements = ifc_file.by_type("IfcGeometricRepresentationSubContext")
else: else:
@@ -35,7 +42,12 @@ def get_context(ifc_file, context, subcontext=None, target_view=None):
return element return element
def is_representation_of_context(representation, context, subcontext=None, target_view=None): def is_representation_of_context(
representation: ifcopenshell.entity_instance,
context: Union[ifcopenshell.entity_instance, str],
subcontext: Optional[str] = None,
target_view: Optional[str] = None,
) -> bool:
if isinstance(context, ifcopenshell.entity_instance): if isinstance(context, ifcopenshell.entity_instance):
return representation.ContextOfItems == context return representation.ContextOfItems == context
@@ -52,11 +64,16 @@ def is_representation_of_context(representation, context, subcontext=None, targe
and representation.ContextOfItems.ContextIdentifier == subcontext and representation.ContextOfItems.ContextIdentifier == subcontext
and representation.ContextOfItems.ContextType == context and representation.ContextOfItems.ContextType == context
) )
elif representation.ContextOfItems.ContextType == context:
return True return representation.ContextOfItems.ContextType == context
def get_representation(element, context, subcontext=None, target_view=None): def get_representation(
element: ifcopenshell.entity_instance,
context: Union[ifcopenshell.entity_instance, str],
subcontext: Optional[str] = None,
target_view: Optional[str] = None,
) -> Union[ifcopenshell.entity_instance, None]:
if element.is_a("IfcProduct") and element.Representation: if element.is_a("IfcProduct") and element.Representation:
for r in element.Representation.Representations: for r in element.Representation.Representations:
if is_representation_of_context(r, context, subcontext, target_view): if is_representation_of_context(r, context, subcontext, target_view):
@@ -67,16 +84,30 @@ def get_representation(element, context, subcontext=None, target_view=None):
return r.MappedRepresentation return r.MappedRepresentation
def resolve_representation(representation): def resolve_representation(representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Resolve possibly mapped representation.
:param representation: IfcRepresentation
:type representation: ifcopenshell.entity_instance.entity_instance
:return: Representation resolved from mappings
:rtype: ifcopenshell.entity_instance.entity_instance
"""
if len(representation.Items) == 1 and representation.Items[0].is_a("IfcMappedItem"): if len(representation.Items) == 1 and representation.Items[0].is_a("IfcMappedItem"):
return resolve_representation(representation.Items[0].MappingSource.MappedRepresentation) return resolve_representation(representation.Items[0].MappingSource.MappedRepresentation)
return representation return representation
def resolve_items(representation, matrix=None): class ResolvedItemDict(TypedDict):
matrix: np.array
item: ifcopenshell.entity_instance
def resolve_items(
representation: ifcopenshell.entity_instance, matrix: Optional[np.array] = None
) -> list[ResolvedItemDict]:
if matrix is None: if matrix is None:
matrix = np.eye(4) matrix = np.eye(4)
results = [] results: list[ResolvedItemDict] = []
for item in representation.Items or []: # Be forgiving of invalid IFCs because Revit :( for item in representation.Items or []: # Be forgiving of invalid IFCs because Revit :(
if item.is_a("IfcMappedItem"): if item.is_a("IfcMappedItem"):
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(item) rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(item)
@@ -84,5 +115,5 @@ def resolve_items(representation, matrix=None):
rep_matrix = rep_matrix @ matrix.copy() rep_matrix = rep_matrix @ matrix.copy()
results.extend(resolve_items(item.MappingSource.MappedRepresentation, rep_matrix)) results.extend(resolve_items(item.MappingSource.MappedRepresentation, rep_matrix))
else: else:
results.append({"matrix": matrix.copy(), "item": item}) results.append(ResolvedItemDict(matrix=matrix.copy(), item=item))
return results return results
@@ -28,6 +28,7 @@ import ifcopenshell.util.placement
import ifcopenshell.util.geolocation import ifcopenshell.util.geolocation
import ifcopenshell.util.classification import ifcopenshell.util.classification
import ifcopenshell.util.schema import ifcopenshell.util.schema
import ifcopenshell.util.shape
from decimal import Decimal from decimal import Decimal
from typing import Optional, Any, Union from typing import Optional, Any, Union
@@ -373,8 +374,7 @@ def set_element_value(
coord_i = "xyz".index(key) coord_i = "xyz".index(key)
prev_value = matrix[coord_i][3] prev_value = matrix[coord_i][3]
new_value = float(value) if value else 0.0 new_value = float(value) if value else 0.0
TOLERANCE = 1.0e-5 if ifcopenshell.util.shape.is_x(new_value, prev_value):
if new_value + TOLERANCE > prev_value > new_value - TOLERANCE:
return return
matrix[coord_i][3] = new_value matrix[coord_i][3] = new_value
@@ -21,11 +21,14 @@ import numpy as np
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.placement import ifcopenshell.util.placement
import ifcopenshell.util.representation import ifcopenshell.util.representation
from typing import Optional, Literal, Union, Iterable
tol = 1e-6 tol = 1e-6
AXIS_LITERAL = Union[Literal["X"], Literal["Y"], Literal["Z"]]
VECTOR_3D = tuple[float, float, float]
def is_x(value, x, tolerance=None): 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
:param value: Input value :param value: Input value
@@ -42,7 +45,7 @@ def is_x(value, x, tolerance=None):
return abs(x - value) < tolerance return abs(x - value) < tolerance
def get_volume(geometry): def get_volume(geometry) -> float:
"""Calculates the total internal volume of a geometry """Calculates the total internal volume of a geometry
Volumes of non-manifold geometry will be unpredictable. Volumes of non-manifold geometry will be unpredictable.
@@ -73,7 +76,7 @@ def get_volume(geometry):
return abs(sum(volumes)) return abs(sum(volumes))
def get_x(geometry): def get_x(geometry) -> float:
"""Calculates the X length of the geometry """Calculates the X length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -85,7 +88,7 @@ def get_x(geometry):
return max(x_values) - min(x_values) return max(x_values) - min(x_values)
def get_y(geometry): def get_y(geometry) -> float:
"""Calculates the Y length of the geometry """Calculates the Y length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -97,7 +100,7 @@ def get_y(geometry):
return max(y_values) - min(y_values) return max(y_values) - min(y_values)
def get_z(geometry): def get_z(geometry) -> float:
"""Calculates the Z length of the geometry """Calculates the Z length of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -109,7 +112,7 @@ def get_z(geometry):
return max(z_values) - min(z_values) return max(z_values) - min(z_values)
def get_shape_matrix(shape): def get_shape_matrix(shape) -> np.ndarray:
"""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
@@ -121,7 +124,7 @@ def get_shape_matrix(shape):
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): def get_bbox_centroid(geometry) -> tuple[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.
@@ -143,7 +146,7 @@ def get_bbox_centroid(geometry):
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, geometry): def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> tuple[float]:
"""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
@@ -163,7 +166,7 @@ def get_element_bbox_centroid(element, geometry):
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): def get_shape_bbox_centroid(shape, geometry) -> tuple[float]:
"""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
@@ -180,7 +183,7 @@ def get_shape_bbox_centroid(shape, 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): def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]:
"""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.
@@ -196,7 +199,7 @@ def get_vertices(geometry):
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): def get_edges(geometry) -> np.ndarray[np.ndarray[int]]:
"""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], ...]
@@ -214,7 +217,7 @@ def get_edges(geometry):
return [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)] return [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
def get_faces(geometry): def get_faces(geometry) -> np.ndarray[np.ndarray[int]]:
"""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
@@ -231,7 +234,7 @@ def get_faces(geometry):
return [[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)] return [[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)]
def get_shape_vertices(shape, geometry): def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]:
"""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
@@ -251,7 +254,7 @@ def get_shape_vertices(shape, geometry):
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, geometry): def get_element_vertices(element: ifcopenshell.entity_instance, geometry) -> np.ndarray[np.ndarray[float]]:
"""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
@@ -273,7 +276,7 @@ def get_element_vertices(element, geometry):
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_bottom_elevation(geometry): def get_bottom_elevation(geometry) -> float:
"""Gets the lowest local Z ordinate of the geometry """Gets the lowest local Z ordinate of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -285,7 +288,7 @@ def get_bottom_elevation(geometry):
return min(z_values) return min(z_values)
def get_top_elevation(geometry): def get_top_elevation(geometry) -> float:
"""Gets the highest local Z ordinate of the geometry """Gets the highest local Z ordinate of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -297,7 +300,7 @@ def get_top_elevation(geometry):
return max(z_values) return max(z_values)
def get_shape_bottom_elevation(shape, geometry): def get_shape_bottom_elevation(shape, geometry) -> float:
"""Gets the lowest global Z ordinate of the shape """Gets the lowest global Z ordinate of the shape
If you do not have the shape, you can use ``get_element_bottom_elevation`` If you do not have the shape, you can use ``get_element_bottom_elevation``
@@ -313,7 +316,7 @@ def get_shape_bottom_elevation(shape, geometry):
return min([v[2] for v in get_shape_vertices(shape, geometry)]) return min([v[2] for v in get_shape_vertices(shape, geometry)])
def get_shape_top_elevation(shape, geometry): def get_shape_top_elevation(shape, geometry) -> float:
"""Gets the highest global Z ordinate of the shape """Gets the highest global Z ordinate of the shape
If you do not have the shape, you can use ``get_element_top_elevation`` If you do not have the shape, you can use ``get_element_top_elevation``
@@ -329,7 +332,7 @@ def get_shape_top_elevation(shape, geometry):
return max([v[2] for v in get_shape_vertices(shape, geometry)]) return max([v[2] for v in get_shape_vertices(shape, geometry)])
def get_element_bottom_elevation(element, geometry): def get_element_bottom_elevation(element: ifcopenshell.entity_instance, geometry) -> float:
"""Gets the lowest global Z ordinate of the element """Gets the lowest global Z ordinate of the element
Note that if you have the shape, it is more efficient to use Note that if you have the shape, it is more efficient to use
@@ -345,7 +348,7 @@ def get_element_bottom_elevation(element, geometry):
return min([v[2] for v in get_element_vertices(element, geometry)]) return min([v[2] for v in get_element_vertices(element, geometry)])
def get_element_top_elevation(element, geometry): def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry) -> float:
"""Gets the highest global Z ordinate of the element """Gets the highest global Z ordinate of the element
Note that if you have the shape, it is more efficient to use Note that if you have the shape, it is more efficient to use
@@ -361,7 +364,7 @@ def get_element_top_elevation(element, 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): def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]:
"""Gets the bounding box of vertices """Gets the bounding box of vertices
:param vertices: An iterable of vertices :param vertices: An iterable of vertices
@@ -384,7 +387,7 @@ def get_bbox(vertices):
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, faces): def get_area_vf(vertices: np.ndarray[VECTOR_3D], faces: np.ndarray[Iterable[int]]) -> 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.
@@ -408,7 +411,7 @@ def get_area_vf(vertices, faces):
return mesh_area return mesh_area
def get_area(geometry): def get_area(geometry) -> float:
"""Calculates the surface area of the geometry """Calculates the surface area of the geometry
:param geometry: Geometry output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell
@@ -423,7 +426,11 @@ def get_area(geometry):
return get_area_vf(vertices, faces) return get_area_vf(vertices, faces)
def get_side_area(geometry, axis="Y", direction=None): def get_side_area(
geometry,
axis: AXIS_LITERAL = "Y",
direction: Optional[VECTOR_3D] = None,
) -> float:
"""Calculates the total surface area of surfaces that are visible from the specified axis """Calculates the total surface area of surfaces that are visible from the specified axis
This is typically useful for calculating elevational areas. For example, This is typically useful for calculating elevational areas. For example,
@@ -471,7 +478,11 @@ def get_side_area(geometry, axis="Y", direction=None):
return get_area_vf(vertices, filtered_faces) return get_area_vf(vertices, filtered_faces)
def get_footprint_area(geometry, axis="Z", direction=None): def get_footprint_area(
geometry,
axis: AXIS_LITERAL = "Z",
direction: Optional[VECTOR_3D] = None,
) -> float:
"""Calculates the total footprint (i.e. projected) surface area visible from along an axis """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 This is typically useful for calculating footprint areas. For example, you
@@ -551,7 +562,7 @@ def get_footprint_area(geometry, axis="Z", direction=None):
return unioned_polygon.area return unioned_polygon.area
def get_outer_surface_area(geometry): def get_outer_surface_area(geometry) -> float:
"""Calculates the outer surface area (i.e. all sides except for top and bottom) """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 This is typically useful for calculating painted areas of beams which
@@ -581,7 +592,7 @@ def get_outer_surface_area(geometry):
return get_area_vf(vertices, filtered_faces) return get_area_vf(vertices, filtered_faces)
def get_footprint_perimeter(geometry): def get_footprint_perimeter(geometry) -> float:
"""Calculates the footprint perimeter of the geometry """Calculates the footprint perimeter of the geometry
All faces with a negative Z normal are considered and the distance of all All faces with a negative Z normal are considered and the distance of all
@@ -630,7 +641,7 @@ def get_footprint_perimeter(geometry):
return sum([np.linalg.norm(vertices[e[0]] - vertices[e[1]]) for e in (all_edges - shared_edges)]) return sum([np.linalg.norm(vertices[e[0]] - vertices[e[1]]) for e in (all_edges - shared_edges)])
def get_profiles(element): def get_profiles(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
"""Gets all 2D profiles used in the definition of a parametric shape """Gets all 2D profiles used in the definition of a parametric shape
Profiles may be retrieved either from material profile sets or from swept Profiles may be retrieved either from material profile sets or from swept
@@ -647,7 +658,7 @@ def get_profiles(element):
return [e.SweptArea for e in get_extrusions(element)] return [e.SweptArea for e in get_extrusions(element)]
def get_extrusions(element): def get_extrusions(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
"""Gets all extruded area solids used to define an element's model body geometry """Gets all extruded area solids used to define an element's model body geometry
:param element: The element occurrence :param element: The element occurrence