diff --git a/src/blenderbim/blenderbim/bim/module/patch/prop.py b/src/blenderbim/blenderbim/bim/module/patch/prop.py index 1fd91dc973..e965e63e15 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/prop.py +++ b/src/blenderbim/blenderbim/bim/module/patch/prop.py @@ -18,6 +18,7 @@ import bpy import importlib +import importlib.util from pathlib import Path import ifcpatch from blenderbim.bim.prop import StrProperty, Attribute diff --git a/src/ifcopenshell-python/ifcopenshell/util/representation.py b/src/ifcopenshell-python/ifcopenshell/util/representation.py index 68842d3ef3..95e91490d0 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/representation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/representation.py @@ -18,9 +18,16 @@ import numpy as np 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: elements = ifc_file.by_type("IfcGeometricRepresentationSubContext") else: @@ -35,7 +42,12 @@ def get_context(ifc_file, context, subcontext=None, target_view=None): 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): 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.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: for r in element.Representation.Representations: 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 -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"): return resolve_representation(representation.Items[0].MappingSource.MappedRepresentation) 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: matrix = np.eye(4) - results = [] + results: list[ResolvedItemDict] = [] for item in representation.Items or []: # Be forgiving of invalid IFCs because Revit :( if item.is_a("IfcMappedItem"): 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() results.extend(resolve_items(item.MappingSource.MappedRepresentation, rep_matrix)) else: - results.append({"matrix": matrix.copy(), "item": item}) + results.append(ResolvedItemDict(matrix=matrix.copy(), item=item)) return results diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 9c2236b2d3..c0e0201a5d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -28,6 +28,7 @@ import ifcopenshell.util.placement import ifcopenshell.util.geolocation import ifcopenshell.util.classification import ifcopenshell.util.schema +import ifcopenshell.util.shape from decimal import Decimal from typing import Optional, Any, Union @@ -373,8 +374,7 @@ def set_element_value( coord_i = "xyz".index(key) prev_value = matrix[coord_i][3] new_value = float(value) if value else 0.0 - TOLERANCE = 1.0e-5 - if new_value + TOLERANCE > prev_value > new_value - TOLERANCE: + if ifcopenshell.util.shape.is_x(new_value, prev_value): return matrix[coord_i][3] = new_value diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py index b50b762498..f4a6335f0b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py @@ -21,11 +21,14 @@ import numpy as np import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation +from typing import Optional, Literal, Union, Iterable 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 :param value: Input value @@ -42,7 +45,7 @@ def is_x(value, x, tolerance=None): return abs(x - value) < tolerance -def get_volume(geometry): +def get_volume(geometry) -> float: """Calculates the total internal volume of a geometry Volumes of non-manifold geometry will be unpredictable. @@ -73,7 +76,7 @@ def get_volume(geometry): return abs(sum(volumes)) -def get_x(geometry): +def get_x(geometry) -> float: """Calculates the X length of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -85,7 +88,7 @@ def get_x(geometry): return max(x_values) - min(x_values) -def get_y(geometry): +def get_y(geometry) -> float: """Calculates the Y length of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -97,7 +100,7 @@ def get_y(geometry): return max(y_values) - min(y_values) -def get_z(geometry): +def get_z(geometry) -> float: """Calculates the Z length of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -109,7 +112,7 @@ def get_z(geometry): 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 :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])) -def get_bbox_centroid(geometry): +def get_bbox_centroid(geometry) -> tuple[float]: """Calculates the bounding box centroid of the geometry 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)) -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 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] -def get_shape_bbox_centroid(shape, geometry): +def get_shape_bbox_centroid(shape, geometry) -> tuple[float]: """Calculates the shape's bounding box centroid 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] -def get_vertices(geometry): +def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]: """Get all the vertices as a numpy array 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)]) -def get_edges(geometry): +def get_edges(geometry) -> np.ndarray[np.ndarray[int]]: """Get all the edges as a numpy array 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)] -def get_faces(geometry): +def get_faces(geometry) -> np.ndarray[np.ndarray[int]]: """Get all the faces as a numpy array 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)] -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 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) -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 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) -def get_bottom_elevation(geometry): +def get_bottom_elevation(geometry) -> float: """Gets the lowest local Z ordinate of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -285,7 +288,7 @@ def get_bottom_elevation(geometry): return min(z_values) -def get_top_elevation(geometry): +def get_top_elevation(geometry) -> float: """Gets the highest local Z ordinate of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -297,7 +300,7 @@ def get_top_elevation(geometry): 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 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)]) -def get_shape_top_elevation(shape, geometry): +def get_shape_top_elevation(shape, geometry) -> float: """Gets the highest global Z ordinate of the shape 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)]) -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 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)]) -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 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)]) -def get_bbox(vertices): +def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]: """Gets the bounding box 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])) -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 :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 -def get_area(geometry): +def get_area(geometry) -> float: """Calculates the surface area of the geometry :param geometry: Geometry output calculated by IfcOpenShell @@ -423,7 +426,11 @@ def get_area(geometry): 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 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) -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 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 -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) 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) -def get_footprint_perimeter(geometry): +def get_footprint_perimeter(geometry) -> float: """Calculates the footprint perimeter of the geometry 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)]) -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 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)] -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 :param element: The element occurrence