diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 8162b5effd..9908324d9d 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -211,6 +211,7 @@ class AddRepresentation(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj props = context.scene.BIMGeometryProperties oprops = obj.BIMGeometryProperties ifc_context = int(oprops.contexts or "0") or None diff --git a/src/bonsai/bonsai/bim/module/spatial/data.py b/src/bonsai/bonsai/bim/module/spatial/data.py index dfecc2b253..eb8e6585b4 100644 --- a/src/bonsai/bonsai/bim/module/spatial/data.py +++ b/src/bonsai/bonsai/bim/module/spatial/data.py @@ -110,7 +110,7 @@ class SpatialDecompositionData: pass @classmethod - def subelement_class(cls): + def subelement_class(cls) -> list[tuple[str, str, str]]: results = [] props = bpy.context.scene.BIMSpatialDecompositionProperties if not (container := props.active_container): diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py index 1666b3539d..65cbcd1989 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py @@ -19,6 +19,7 @@ from __future__ import annotations import bpy.types import math import bmesh +import ifcopenshell.util.shape_builder import ifcopenshell.util.unit from mathutils import Vector, Matrix from typing import Union, Optional, Literal, Any, TYPE_CHECKING @@ -35,25 +36,15 @@ EPSILON = 1e-6 def add_representation( file: ifcopenshell.file, *, # keywords only as this API implementation is probably not final - # IfcGeometricRepresentationContext context: ifcopenshell.entity_instance, - # This is (currently) a Blender object, hence this depends on Blender now blender_object: bpy.types.Object, - # This is (currently) a Blender data object, hence this depends on Blender now geometry: Union[bpy.types.Mesh, bpy.types.Curve], - # Optionally apply a vector offset to all coordinates coordinate_offset: Optional[Vector] = None, - # How many representation items to create total_items: int = 1, - # A scale factor to apply for all vectors in case the unit is different unit_scale: Optional[float] = None, - # If we should force faceted breps for meshes should_force_faceted_brep: bool = False, - # If we should force triangulation for meshes should_force_triangulation: bool = False, - # If UV coordinates should also be generated should_generate_uvs: bool = False, - # Whether to cast a mesh into a particular class ifc_representation_class: Optional[ Literal[ "IfcExtrudedAreaSolid/IfcRectangleProfileDef", @@ -65,11 +56,26 @@ def add_representation( "IfcTextLiteral", ] ] = None, - # The material profile set if the extrusion requires it profile_set_usage: Optional[ifcopenshell.entity_instance] = None, - # The text literal if the representation requires it text_literal: Optional[ifcopenshell.entity_instance] = None, -) -> ifcopenshell.entity_instance: +) -> Union[ifcopenshell.entity_instance, None]: + """Add an IfcShapeRepresentation. + + :param context: The IfcGeometricRepresentationContext. + :param blender_object: This is (currently) a Blender object, hence this depends on Blender now. + :param geometry: This is (currently) a Blender data object, hence this depends on Blender now. + :param coordinate_offset: Optionally apply a vector offset to all coordinates (in SI units). + :param total_items: How many representation items to create. + :param unit_scale: A scale factor to apply for all vectors in case the unit is different. + :param should_force_faceted_brep: If we should force faceted breps for meshes. + :param should_force_triangulation: If we should force triangulation for meshes. + :param should_generate_uvs: If UV coordinates should also be generated. + :param ifc_representation_class: Whether to cast a mesh into a particular class + :param profile_set_usage: The material profile set if the extrusion requires it + :param text_literal: The text literal if the representation requires it + :return: IfcShapeRepresentation or None if couldn't create representation + for the provided context. + """ # lazy import Helper to avoid circular import if "Helper" not in globals(): from bonsai.bim.module.geometry.helper import Helper @@ -100,8 +106,9 @@ def add_representation( class Usecase: file: ifcopenshell.file settings: dict[str, Any] + ifc_vertices: list[ifcopenshell.entity_instance] - def execute(self): + def execute(self) -> Union[ifcopenshell.entity_instance, None]: self.is_manifold = None if ( isinstance(self.settings["geometry"], bpy.types.Mesh) @@ -116,7 +123,7 @@ class Usecase: return self.create_plan_representation() return self.create_variable_representation() - def should_triangulate_face(self, face, threshold=EPSILON): + def should_triangulate_face(self, face: bmesh.types.BMFace, threshold: float = EPSILON) -> bool: vz = face.normal co = face.verts[0].co if vz.length < 0.5: @@ -132,7 +139,7 @@ class Usecase: return any([abs((tM @ v.co).z) > threshold for v in face.verts]) - def evaluate_geometry(self): + def evaluate_geometry(self) -> None: for modifier in self.settings["blender_object"].modifiers: if modifier.type == "BOOLEAN": modifier.show_viewport = False @@ -163,7 +170,7 @@ class Usecase: if modifier.type == "BOOLEAN": modifier.show_viewport = True - def create_model_representation(self): + def create_model_representation(self) -> Union[ifcopenshell.entity_instance, None]: if self.settings["context"].is_a() == "IfcGeometricRepresentationContext": return self.create_variable_representation() elif self.settings["ifc_representation_class"] == "IfcTextLiteral": @@ -199,7 +206,7 @@ class Usecase: elif self.settings["context"].ContextIdentifier == "Lighting": return self.create_lighting_representation() - def create_plan_representation(self): + def create_plan_representation(self) -> Union[ifcopenshell.entity_instance, None]: if self.settings["ifc_representation_class"] == "IfcTextLiteral": return self.create_text_representation(is_2d=True) elif self.settings["ifc_representation_class"] == "IfcGeometricCurveSet/IfcTextLiteral": @@ -233,7 +240,7 @@ class Usecase: else: return self.create_annotation2d_representation() - def create_lighting_representation(self): + def create_lighting_representation(self) -> ifcopenshell.entity_instance: return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, @@ -241,11 +248,11 @@ class Usecase: [self.create_light_source()], ) - def create_light_source(self): + def create_light_source(self) -> Union[ifcopenshell.entity_instance, None]: if self.settings["geometry"].type == "POINT": return self.create_light_source_positional() - def create_light_source_positional(self): + def create_light_source_positional(self) -> ifcopenshell.entity_instance: return self.file.create_entity( "IfcLightSourcePositional", **{ @@ -255,7 +262,7 @@ class Usecase: }, ) - def create_text_representation(self, is_2d=False): + def create_text_representation(self, is_2d: bool = False) -> ifcopenshell.entity_instance: return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, @@ -263,7 +270,7 @@ class Usecase: [self.create_text()], ) - def create_text(self): + def create_text(self) -> ifcopenshell.entity_instance: if self.settings["text_literal"]: return self.settings["text_literal"] origin = self.file.createIfcAxis2Placement3D( @@ -277,7 +284,7 @@ class Usecase: "TEXT", origin, "RIGHT", self.file.createIfcPlanarExtent(1000, 1000), "bottom-left" ) - def create_variable_representation(self): + def create_variable_representation(self) -> Union[ifcopenshell.entity_instance, None]: if isinstance(self.settings["geometry"], bpy.types.Curve) and self.settings["geometry"].bevel_depth: return self.create_swept_disk_solid_representation() elif isinstance(self.settings["geometry"], bpy.types.Curve): @@ -303,7 +310,7 @@ class Usecase: return self.create_material_profile_set_extrusion_representation() return self.create_mesh_representation() - def create_camera_block_representation(self): + def create_camera_block_representation(self) -> ifcopenshell.entity_instance: raster_x = self.settings["geometry"].BIMCameraProperties.raster_x raster_y = self.settings["geometry"].BIMCameraProperties.raster_y @@ -331,7 +338,7 @@ class Usecase: [self.file.createIfcCsgSolid(block)], ) - def create_camera_pyramid_representation(self): + def create_camera_pyramid_representation(self) -> ifcopenshell.entity_instance: raster_x = self.settings["geometry"].BIMCameraProperties.raster_x raster_y = self.settings["geometry"].BIMCameraProperties.raster_y fov = self.settings["geometry"].angle @@ -373,13 +380,13 @@ class Usecase: [self.file.createIfcCsgSolid(clipping_result)], ) - def is_camera_landscape(self): + def is_camera_landscape(self) -> bool: return ( self.settings["geometry"].BIMCameraProperties.raster_x > self.settings["geometry"].BIMCameraProperties.raster_y ) - def create_swept_disk_solid_representation(self): + def create_swept_disk_solid_representation(self) -> ifcopenshell.entity_instance: return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, @@ -387,13 +394,13 @@ class Usecase: self.create_swept_disk_solids(), ) - def create_curve3d_representation(self): + def create_curve3d_representation(self) -> Union[ifcopenshell.entity_instance, None]: if curves := self.create_curves(): return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Curve3D", curves ) - def create_curve2d_representation(self): + def create_curve2d_representation(self) -> ifcopenshell.entity_instance: return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, @@ -401,7 +408,7 @@ class Usecase: self.create_curves(is_2d=True), ) - def create_curve_bounded_planes(self, is_2d=False): + def create_curve_bounded_planes(self, is_2d: bool = False) -> list[ifcopenshell.entity_instance]: items = [] if self.file.schema != "IFC2X3": points = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d=False) @@ -414,7 +421,7 @@ class Usecase: items.append(self.file.createIfcCurveBoundedPlane(BasisSurface=plane, OuterBoundary=curve)) return items - def create_plane(self, polygon): + def create_plane(self, polygon: bpy.types.MeshPolygon) -> ifcopenshell.entity_instance: return self.file.createIfcPlane( Position=self.file.createIfcAxis2Placement3D( Location=self.file.createIfcCartesianPoint(polygon.center), @@ -422,7 +429,7 @@ class Usecase: ) ) - def create_annotation_fill_areas(self, is_2d=False) -> list[ifcopenshell.entity_instance]: + def create_annotation_fill_areas(self, is_2d: bool = False) -> list[ifcopenshell.entity_instance]: items = [] if self.file.schema != "IFC2X3": points = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d=is_2d) @@ -435,14 +442,16 @@ class Usecase: return items def create_curve_from_polygon( - self, points: ifcopenshell.entity_instance, polygon: bpy.types.MeshPolygon, is_2d=False + self, points: ifcopenshell.entity_instance, polygon: bpy.types.MeshPolygon, is_2d: bool = False ) -> ifcopenshell.entity_instance: indices = list(polygon.vertices) indices.append(indices[0]) edge_loop = [self.file.createIfcLineIndex((v1 + 1, v2 + 1)) for v1, v2 in zip(indices, indices[1:])] return self.file.createIfcIndexedPolyCurve(points, edge_loop) - def create_curve_from_polygon_ifc2x3(self, polygon, is_2d=False): + def create_curve_from_polygon_ifc2x3( + self, polygon: bpy.types.MeshPolygon, is_2d: bool = False + ) -> ifcopenshell.entity_instance: indices = list(polygon.vertices) indices.append(indices[0]) points = [ @@ -451,7 +460,7 @@ class Usecase: ] return self.file.createIfcPolyline([points[i] for i in indices]) - def create_swept_disk_solids(self): + def create_swept_disk_solids(self) -> list[ifcopenshell.entity_instance]: curves = self.create_curves() results = [] radius = self.convert_si_to_unit(round(self.settings["geometry"].bevel_depth, 3)) @@ -459,7 +468,7 @@ class Usecase: results.append(self.file.createIfcSweptDiskSolid(curve, radius)) return results - def is_mesh_curve_consecutive(self, geom_data): + def is_mesh_curve_consecutive(self, geom_data: bpy.types.Mesh) -> bool: import bonsai.tool as tool bm = tool.Blender.get_bmesh_for_mesh(geom_data) @@ -496,16 +505,18 @@ class Usecase: return True if not validate_edge(edge0, start_vert, processed_verts): - return + return False if edge1 and not validate_edge(edge1, start_vert, processed_verts): - return + return False if len(processed_verts) != n_verts: return False return True - def create_curves(self, should_exclude_faces=False, is_2d=False, ignore_non_loose_edges=False): + def create_curves( + self, should_exclude_faces: bool = False, is_2d: bool = False, ignore_non_loose_edges: bool = False + ) -> list[ifcopenshell.entity_instance]: geom_data = self.settings["geometry"] if isinstance(geom_data, bpy.types.Mesh): @@ -540,7 +551,9 @@ class Usecase: tool.Blender.set_objects_selection(bpy.context, active_object, selected_objects) return curves - def create_curves_from_mesh(self, should_exclude_faces=False, is_2d=False): + def create_curves_from_mesh( + self, should_exclude_faces: bool = False, is_2d: bool = False + ) -> list[ifcopenshell.entity_instance]: geom_data = self.settings["geometry"].copy() self.remove_doubles_from_mesh(geom_data) curves = [] @@ -567,7 +580,7 @@ class Usecase: curves.append(self.file.createIfcIndexedPolyCurve(points, edge_loop)) return curves - def remove_doubles_from_mesh(self, mesh): + def remove_doubles_from_mesh(self, mesh: bpy.types.Mesh) -> None: import bonsai.tool as tool bm = tool.Blender.get_bmesh_for_mesh(mesh) @@ -608,7 +621,9 @@ class Usecase: curves.append(self.file.createIfcPolyline(loop_points)) return curves - def create_curves_from_curve_ifc2x3(self, is_2d=False, curve_object_data=None): + def create_curves_from_curve_ifc2x3( + self, is_2d: bool = False, curve_object_data: Optional[bpy.types.Curve] = None + ) -> list[ifcopenshell.entity_instance]: # TODO: support interpolated curves, not just polylines if not curve_object_data: curve_object_data = self.settings["geometry"] @@ -622,7 +637,9 @@ class Usecase: results.append(self.file.createIfcPolyline(ifc_points)) return results - def create_curves_from_curve(self, is_2d=False, curve_object_data=None): + def create_curves_from_curve( + self, is_2d: bool = False, curve_object_data: Optional[bpy.types.Curve] = None + ) -> list[ifcopenshell.entity_instance]: # TODO: support interpolated curves, not just polylines if not curve_object_data: curve_object_data = self.settings["geometry"] @@ -640,7 +657,7 @@ class Usecase: return results - def create_point_cloud_representation(self, is_2d=False): + def create_point_cloud_representation(self, is_2d: bool = False) -> ifcopenshell.entity_instance: if self.file.schema == "IFC2X3": geometric_set = [] for point in self.settings["geometry"].vertices: @@ -663,7 +680,7 @@ class Usecase: [point_cloud], ) - def create_rectangle_extrusion_representation(self): + def create_rectangle_extrusion_representation(self) -> ifcopenshell.entity_instance: helper = Helper(self.file) indices = helper.auto_detect_rectangle_profile_extruded_area_solid(self.settings["geometry"]) profile_def = helper.create_rectangle_profile_def(self.settings["geometry"], indices["profile"]) @@ -675,7 +692,7 @@ class Usecase: [item], ) - def create_circle_extrusion_representation(self): + def create_circle_extrusion_representation(self) -> ifcopenshell.entity_instance: helper = Helper(self.file) indices = helper.auto_detect_circle_profile_extruded_area_solid(self.settings["geometry"]) profile_def = helper.create_circle_profile_def(self.settings["geometry"], indices["profile"]) @@ -687,7 +704,7 @@ class Usecase: [item], ) - def create_arbitrary_extrusion_representation(self): + def create_arbitrary_extrusion_representation(self) -> ifcopenshell.entity_instance: helper = Helper(self.file) indices = helper.auto_detect_arbitrary_closed_profile_extruded_area_solid(self.settings["geometry"]) profile_def = helper.create_arbitrary_closed_profile_def(self.settings["geometry"], indices["profile"]) @@ -699,7 +716,7 @@ class Usecase: [item], ) - def create_arbitrary_void_extrusion_representation(self): + def create_arbitrary_void_extrusion_representation(self) -> ifcopenshell.entity_instance: helper = Helper(self.file) indices = helper.auto_detect_arbitrary_profile_with_voids_extruded_area_solid(self.settings["geometry"]) if not indices["inner_curves"]: @@ -715,7 +732,7 @@ class Usecase: [item], ) - def create_material_profile_set_extrusion_representation(self): + def create_material_profile_set_extrusion_representation(self) -> ifcopenshell.entity_instance: profile_set = self.settings["profile_set_usage"].ForProfileSet profile_def = profile_set.CompositeProfile or profile_set.MaterialProfiles[0].Profile position = None @@ -738,14 +755,14 @@ class Usecase: [item], ) - def create_mesh_representation(self): + def create_mesh_representation(self) -> ifcopenshell.entity_instance: if self.file.schema == "IFC2X3" or self.settings["should_force_faceted_brep"]: return self.create_faceted_brep() if self.settings["should_force_triangulation"]: return self.create_triangulated_face_set() return self.create_polygonal_face_set() - def create_faceted_brep(self): + def create_faceted_brep(self) -> ifcopenshell.entity_instance: self.create_vertices() ifc_raw_items = [None] * self.settings["total_items"] for i, value in enumerate(ifc_raw_items): @@ -770,7 +787,7 @@ class Usecase: items, ) - def create_triangulated_face_set(self): + def create_triangulated_face_set(self) -> ifcopenshell.entity_instance: ifc_raw_items = [None] * self.settings["total_items"] if self.settings["should_generate_uvs"]: ifc_raw_uv_items = [None] * self.settings["total_items"] @@ -814,7 +831,7 @@ class Usecase: items, ) - def create_polygonal_face_set(self): + def create_polygonal_face_set(self) -> ifcopenshell.entity_instance: ifc_raw_items = [None] * self.settings["total_items"] for i, value in enumerate(ifc_raw_items): ifc_raw_items[i] = [] @@ -831,7 +848,7 @@ class Usecase: items, ) - def create_vertices(self, is_2d=False): + def create_vertices(self, is_2d: bool = False) -> None: if is_2d: for v in self.settings["geometry"].vertices: co = self.convert_si_to_unit(v.co) @@ -844,7 +861,13 @@ class Usecase: ] ) - def create_cartesian_point(self, x, y, z=None, is_model_coords=True): + def create_cartesian_point( + self, x: float, y: float, z: Optional[float] = None, is_model_coords: bool = True + ) -> ifcopenshell.entity_instance: + """Create IfcCartesianPoint. + + x, y, z coords are provided in SI units. + """ if is_model_coords and self.settings["coordinate_offset"]: x += self.settings["coordinate_offset"][0] y += self.settings["coordinate_offset"][1] @@ -858,8 +881,8 @@ class Usecase: return self.file.createIfcCartesianPoint((x, y, z)) def create_cartesian_point_list_from_vertices( - self, vertices: list[bpy.types.MeshVertex], is_2d=False, is_model_coords=True - ): + self, vertices: list[bpy.types.MeshVertex], is_2d: bool = False, is_model_coords: bool = True + ) -> ifcopenshell.entity_instance: if is_model_coords and self.settings["coordinate_offset"]: if is_2d: xy_offset = Vector((self.settings["coordinate_offset"][0:2])) @@ -877,7 +900,7 @@ class Usecase: def convert_si_to_unit(self, co): return co / self.settings["unit_scale"] - def create_annotation2d_representation(self): + def create_annotation2d_representation(self) -> ifcopenshell.entity_instance: if isinstance(self.settings["geometry"], bpy.types.Mesh) and len(self.settings["geometry"].polygons): items = self.create_annotation_fill_areas(is_2d=True) elif isinstance(self.settings["geometry"], bpy.types.Mesh) and not len(self.settings["geometry"].edges): @@ -891,7 +914,7 @@ class Usecase: items, ) - def create_annotation3d_representation(self): + def create_annotation3d_representation(self) -> ifcopenshell.entity_instance: items = [] if isinstance(self.settings["geometry"], bpy.types.Mesh) and len(self.settings["geometry"].polygons): items = self.create_annotation_fill_areas(is_2d=False) @@ -908,7 +931,7 @@ class Usecase: items, ) - def create_geometric_curve_set_representation(self, is_2d=False): + def create_geometric_curve_set_representation(self, is_2d: bool = False) -> ifcopenshell.entity_instance: geometric_curve_set = self.file.createIfcGeometricCurveSet(self.create_curves(is_2d=is_2d)) return self.file.createIfcShapeRepresentation( self.settings["context"], @@ -917,7 +940,7 @@ class Usecase: [geometric_curve_set], ) - def create_box_representation(self): + def create_box_representation(self) -> ifcopenshell.entity_instance: obj = self.settings["blender_object"] bounding_box = self.file.createIfcBoundingBox( self.create_cartesian_point(obj.bound_box[0][0], obj.bound_box[0][1], obj.bound_box[0][2]), @@ -932,7 +955,7 @@ class Usecase: [bounding_box], ) - def create_structural_reference_representation(self): + def create_structural_reference_representation(self) -> ifcopenshell.entity_instance: if len(self.settings["geometry"].vertices) == 1: return self.file.createIfcTopologyRepresentation( self.settings["context"], @@ -947,10 +970,10 @@ class Usecase: [self.create_edge()], ) - def create_vertex_point(self, point): + def create_vertex_point(self, point: Vector) -> ifcopenshell.entity_instance: return self.file.createIfcVertexPoint(self.create_cartesian_point(point.x, point.y, point.z)) - def create_edge(self): + def create_edge(self) -> Union[ifcopenshell.entity_instance, None]: if hasattr(self.settings["geometry"], "splines"): points = self.get_spline_points(self.settings["geometry"].splines[0]) else: diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index 7f3a318444..a6abb3e7f3 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -115,7 +115,7 @@ def write(output: Union[ifcopenshell.file, str], filepath: str) -> None: def extract_docs( submodule_name: str, cls_name: str, method_name: str = "__init__", boilerplate_args: Optional[Iterable[str]] = None -): +) -> Union[dict[str, Any], None]: """Extract class docstrings and method arguments :param submodule_name: Submodule from which to extract the class @@ -137,7 +137,7 @@ def extract_docs( print(f"Error : IFCPatch {str(submodule)} could not load because : {str(e)}") -def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Iterable[str], None]): +def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Iterable[str], None]) -> dict[str, Any]: inputs = collections.OrderedDict() method = getattr(cls, method_name) docs: dict[str, Any] = {"class": cls} diff --git a/src/ifcpatch/ifcpatch/recipes/RecycleNonRootedElements.py b/src/ifcpatch/ifcpatch/recipes/RecycleNonRootedElements.py index 6bb08bb678..c94ddfeb30 100644 --- a/src/ifcpatch/ifcpatch/recipes/RecycleNonRootedElements.py +++ b/src/ifcpatch/ifcpatch/recipes/RecycleNonRootedElements.py @@ -17,11 +17,12 @@ # along with IfcPatch. If not, see . from collections import deque +import logging import ifcopenshell.util.element class Patcher: - def __init__(self, src, file, logger): + def __init__(self, src: str, file: ifcopenshell.file, logger: logging.Logger): """Optimise the filesize of an IFC model by reusing non-rooted elements It is possible to non-losslessly optimise the filesize of an IFC model.