From e3228513c019bd12ea8abdc3761658a3d2dbd2a4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 31 Mar 2025 13:56:37 +0500 Subject: [PATCH] typing --- .../bonsai/bim/module/boundary/operator.py | 7 ++- src/bonsai/bonsai/bim/module/cost/operator.py | 5 +- src/bonsai/bonsai/bim/module/type/operator.py | 28 +++++++--- src/bonsai/bonsai/core/cost.py | 5 +- src/bonsai/bonsai/tool/cost.py | 8 ++- .../ifcopenshell/api/type/assign_type.py | 53 +++++++++---------- 6 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index ece7277275..820112bf9c 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -982,14 +982,17 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator): self.set_boundary_name(parent_boundary) return parent_boundary - def get_face_matrix(self, p1, p2, p3): + def get_face_matrix(self, p1: Vector, p2: Vector, p3: Vector) -> Matrix: edge1 = p2 - p1 edge2 = p3 - p1 normal = edge1.cross(edge2) + assert isinstance(normal, Vector) + z_axis = normal.normalized() x_axis = p2 - p1 x_axis.normalize() y_axis = z_axis.cross(x_axis) + assert isinstance(y_axis, Vector) mat = Matrix() mat.col[0][:3] = x_axis @@ -1059,7 +1062,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator): return surface - def set_boundary_name(self, boundary): + def set_boundary_name(self, boundary: ifcopenshell.entity_instance): """ By convention 1stLevel and 2ndLevel boundary have specific name and description See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcRelSpaceBoundary.htm diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index 9dfe4c20fd..7a6bbc4fd6 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -24,7 +24,7 @@ import bonsai.tool as tool from bpy_extras.io_utils import ImportHelper, ExportHelper import bonsai.tool as tool import bonsai.core.cost as core -from typing import get_args, TYPE_CHECKING +from typing import get_args, TYPE_CHECKING, Literal class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator): @@ -695,6 +695,9 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper): default=True, ) + if TYPE_CHECKING: + format: Literal["CSV", "XLSX", "ODS"] + @property def filename_ext(self) -> str: return f".{self.format.lower()}" diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index 5efd58600f..f93025e6af 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -24,11 +24,13 @@ import ifcopenshell.util.representation import ifcopenshell.util.type import ifcopenshell.util.unit import ifcopenshell.api +import ifcopenshell.api.type import bonsai.bim.helper import bonsai.tool as tool import bonsai.core.geometry import bonsai.core.type as core import bonsai.core.root +from typing import TYPE_CHECKING class AssignType(bpy.types.Operator, tool.Ifc.Operator): @@ -38,15 +40,18 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator): relating_type: bpy.props.IntProperty() related_object: bpy.props.StringProperty() + if TYPE_CHECKING: + relating_type: int + related_object: str + def _execute(self, context): relating_type = tool.Ifc.get().by_id( self.relating_type or int(context.active_object.BIMTypeProperties.relating_type) ) - related_objects = ( - [bpy.data.objects.get(self.related_object)] - if self.related_object - else context.selected_objects or [context.active_object] - ) + if self.related_object: + related_objects = [bpy.data.objects[self.related_object]] + else: + related_objects = tool.Blender.get_selected_objects() model_props = tool.Model.get_model_props() for obj in related_objects: element = tool.Ifc.get_entity(obj) @@ -63,17 +68,24 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} related_object: bpy.props.StringProperty() + if TYPE_CHECKING: + related_object: str + def _execute(self, context): def exclude_callback(attribute): return attribute.is_a("IfcProfileDef") and attribute.ProfileName self.file = tool.Ifc.get() - objs = [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects - for obj in objs: + if self.related_object: + related_objects = [bpy.data.objects[self.related_object]] + else: + related_objects = tool.Blender.get_selected_objects() + + for obj in related_objects: element = tool.Ifc.get_entity(obj) if not element or not element.is_a("IfcObject"): continue - ifcopenshell.api.run("type.unassign_type", self.file, related_objects=[element]) + ifcopenshell.api.type.unassign_type(self.file, related_objects=[element]) if element.Representation: new_active_representation = None diff --git a/src/bonsai/bonsai/core/cost.py b/src/bonsai/bonsai/core/cost.py index 7ac985ac46..33510320d3 100644 --- a/src/bonsai/bonsai/core/cost.py +++ b/src/bonsai/bonsai/core/cost.py @@ -337,7 +337,10 @@ def calculate_cost_item_resource_value(ifc: tool.Ifc, cost_item: ifcopenshell.en def export_cost_schedules( - cost: tool.Cost, filepath: str, format: str, cost_schedule: Union[ifcopenshell.entity_instance, None] = None + cost: tool.Cost, + filepath: str, + format: Literal["CSV", "ODS", "XLSX"], + cost_schedule: Union[ifcopenshell.entity_instance, None] = None, ) -> Union[str, None]: cost.play_sound() return cost.export_cost_schedules(filepath, format, cost_schedule) diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index 3775da554c..919e649b82 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -29,6 +29,7 @@ import bonsai.bim.helper import json from pathlib import Path from typing import Optional, Any, Generator, Union, Literal, TYPE_CHECKING +from typing_extensions import assert_never if TYPE_CHECKING: from bonsai.bim.module.cost.prop import BIMCostProperties, CostItemQuantity @@ -652,7 +653,10 @@ class Cost(bonsai.core.tool.Cost): @classmethod def export_cost_schedules( - cls, filepath: str, format: str, cost_schedule: Optional[ifcopenshell.entity_instance] = None + cls, + filepath: str, + format: Literal["CSV", "ODS", "XLSX"], + cost_schedule: Optional[ifcopenshell.entity_instance] = None, ) -> Union[str, None]: import subprocess import os @@ -680,6 +684,8 @@ class Cost(bonsai.core.tool.Cost): writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule) writer.write() + else: + assert_never(format) try: if path: if sys.platform == "win32": diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py index f4bfae022e..8c915646bc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py @@ -92,18 +92,14 @@ def assign_type( ambiguous, unknown or are so bespoke as to have no logical type. :param related_objects: The IfcElement occurrences. - :type related_objects: list[ifcopenshell.entity_instance] :param relating_type: The IfcElementType type. - :type relating_type: ifcopenshell.entity_instance :param should_map_representations: If a type has a representation map, IFC requires all occurrences to map those representations. Some IFC vendors might disobey this, or you might want to handle it yourusecase. In this scenario, you may set this to False. This also enabled adding material usages mapping. - :type should_map_representations: bool :return: The IfcRelDefinesByType relationship or `None` if `related_objects` was empty list. - :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -177,29 +173,26 @@ def assign_type( """ usecase = Usecase() usecase.file = file - usecase.settings = { - "related_objects": related_objects, - "relating_type": relating_type, - "should_map_representations": should_map_representations, - } - return usecase.execute() + return usecase.execute(related_objects, relating_type, should_map_representations) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] - def execute(self): - if not self.settings["related_objects"]: + def execute( + self, + related_objects: list[ifcopenshell.entity_instance], + relating_type: ifcopenshell.entity_instance, + should_map_representations: bool, + ): + if not related_objects: return - related_objects = set(self.settings["related_objects"]) - relating_type = self.settings["relating_type"] + related_objects_set = set(related_objects) ifc2x3 = self.file.schema == "IFC2X3" - related_objects = set(self.settings["related_objects"]) - relating_type = self.settings["relating_type"] + related_objects_set = set(related_objects) if ifc2x3: types = next(iter(relating_type.ObjectTypeOf), None) else: @@ -210,20 +203,20 @@ class Usecase: objects_with_types: list[ifcopenshell.entity_instance] = [] # check if there is anything to change - for object in related_objects: + for obj in related_objects_set: if ifc2x3: - object_rel = next((i for i in object.IsDefinedBy if i.is_a("IfcRelDefinesByType")), None) + object_rel = next((i for i in obj.IsDefinedBy if i.is_a("IfcRelDefinesByType")), None) else: - object_rel = next(iter(object.IsTypedBy), None) + object_rel = next(iter(obj.IsTypedBy), None) if object_rel is None: - objects_without_types.append(object) + objects_without_types.append(obj) continue # either rel doesn't exist or product is part of different rel if object_rel != types: previous_types_rels.add(object_rel) - objects_with_types.append(object) + objects_with_types.append(obj) objects_to_change = objects_without_types + objects_with_types # nothing to change @@ -232,7 +225,7 @@ class Usecase: # unassign from previous types for is_typed_by in previous_types_rels: - cur_related_objects = set(is_typed_by.RelatedObjects) - related_objects + cur_related_objects = set(is_typed_by.RelatedObjects) - related_objects_set if cur_related_objects: is_typed_by.RelatedObjects = list(cur_related_objects) ifcopenshell.api.owner.update_owner_history(self.file, **{"element": is_typed_by}) @@ -244,18 +237,18 @@ class Usecase: # assign objects to a new type if types: - types.RelatedObjects = list(set(types.RelatedObjects) | related_objects) + types.RelatedObjects = list(set(types.RelatedObjects) | related_objects_set) ifcopenshell.api.owner.update_owner_history(self.file, **{"element": types}) else: types = self.file.create_entity( "IfcRelDefinesByType", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.owner.create_owner_history(self.file), - RelatedObjects=list(related_objects), + RelatedObjects=list(related_objects_set), RelatingType=relating_type, ) - if self.settings["should_map_representations"]: + if should_map_representations: if getattr(relating_type, "RepresentationMaps", None): for related_object in objects_to_change: ifcopenshell.api.type.map_type_representations( @@ -263,11 +256,13 @@ class Usecase: related_object=related_object, relating_type=relating_type, ) - self.map_material_usages(objects_to_change) + self.map_material_usages(objects_to_change, relating_type) return types - def map_material_usages(self, related_objects: Iterable[ifcopenshell.entity_instance]) -> None: - type_material = ifcopenshell.util.element.get_material(self.settings["relating_type"]) + def map_material_usages( + self, related_objects: list[ifcopenshell.entity_instance], relating_type: ifcopenshell.entity_instance + ) -> None: + type_material = ifcopenshell.util.element.get_material(relating_type) if not type_material: return ifc_class = type_material.is_a()