diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 48bc67a294..3ecb915601 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -216,39 +216,6 @@ def get_enum_items(data, prop_name, context=None): return items -def get_obj_ifc_definition_id(context, obj, obj_type): - if obj_type == "Object": - return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id - elif obj_type == "Material": - return bpy.data.materials.get(obj).BIMObjectProperties.ifc_definition_id - elif obj_type == "MaterialSet": - return ifcopenshell.util.element.get_material( - tool.Ifc.get_entity(bpy.data.objects.get(obj)), should_skip_usage=True - ).id() - elif obj_type == "MaterialSetItem": - return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id - elif obj_type == "Task": - return context.scene.BIMTaskTreeProperties.tasks[ - context.scene.BIMWorkScheduleProperties.active_task_index - ].ifc_definition_id - elif obj_type == "Cost": - return context.scene.BIMCostProperties.cost_items[ - context.scene.BIMCostProperties.active_cost_item_index - ].ifc_definition_id - elif obj_type == "Resource": - return context.scene.BIMResourceTreeProperties.resources[ - context.scene.BIMResourceProperties.active_resource_index - ].ifc_definition_id - elif obj_type == "Profile": - return context.scene.BIMProfileProperties.profiles[ - context.scene.BIMProfileProperties.active_profile_index - ].ifc_definition_id - elif obj_type == "WorkSchedule": - return context.scene.BIMWorkScheduleProperties.active_work_schedule_id - elif obj_type == "Group": - prop = context.scene.BIMGroupProperties - return prop.groups[prop.active_group_index].ifc_definition_id - # hack to close popup # https://blender.stackexchange.com/a/202576/130742 def close_operator_panel(event): diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index 607ba967f5..84f2f89599 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -293,7 +293,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator): identification = active_reference[1] for obj in objects: - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) references = ifcopenshell.util.classification.get_references(element, should_inherit=False) for reference in references: @@ -358,7 +358,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator): break for obj in objects: - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) if not ifc_definition_id: continue ifcopenshell.api.run( @@ -406,7 +406,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator): classification.Location = bsdd_classification.domain_namespace_uri for obj in objects: - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) if not ifc_definition_id: continue element = tool.Ifc.get().by_id(ifc_definition_id) diff --git a/src/blenderbim/blenderbim/bim/module/material/data.py b/src/blenderbim/blenderbim/bim/module/material/data.py index d2a07d480d..4e984b93c9 100644 --- a/src/blenderbim/blenderbim/bim/module/material/data.py +++ b/src/blenderbim/blenderbim/bim/module/material/data.py @@ -19,6 +19,7 @@ import os import bpy import ifcopenshell +import ifcopenshell.util.doc import ifcopenshell.util.schema import blenderbim.tool as tool diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index 66d0701697..c699a63d52 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -85,7 +85,6 @@ class EnablePsetEditing(bpy.types.Operator): def execute(self, context): self.props = get_pset_props(context, self.obj, self.obj_type) self.props.properties.clear() - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type) if self.pset_id: pset = tool.Ifc.get().by_id(self.pset_id) @@ -250,7 +249,7 @@ class DisablePsetEditing(bpy.types.Operator, Operator): props = get_pset_props(context, self.obj, self.obj_type) if props.active_pset_id: pset = tool.Ifc.get().by_id(props.active_pset_id) - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context) if tool.Pset.is_pset_empty(pset): ifcopenshell.api.run( "pset.remove_pset", tool.Ifc.get(), product=tool.Ifc.get().by_id(ifc_definition_id), pset=pset @@ -272,7 +271,7 @@ class EditPset(bpy.types.Operator, Operator): def _execute(self, context): self.file = IfcStore.get_file() props = get_pset_props(context, self.obj, self.obj_type) - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) properties = {} @@ -343,7 +342,7 @@ class RemovePset(bpy.types.Operator, Operator): pset_name = tool.Ifc.get().by_id(self.pset_id).Name for obj in objects: props = get_pset_props(context, obj, self.obj_type) - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None) if pset: @@ -374,7 +373,7 @@ class AddQto(bpy.types.Operator, Operator): def _execute(self, context): self.file = IfcStore.get_file() props = get_pset_props(context, self.obj, self.obj_type) - ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type) + ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) bpy.ops.bim.enable_pset_editing( pset_id=0, pset_name=props.qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index b82a9159ce..fe1d651bee 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -76,7 +76,7 @@ class Blender: def get_name(cls, ifc_class, name): pass def get_selected_objects(cls): pass def create_ifc_object(cls, ifc_class: str, name: str = None, data=None): pass - def get_obj_ifc_definition_id(cls, obj=None, obj_type=None): pass + def get_obj_ifc_definition_id(cls, obj=None, obj_type=None, context=None): pass def is_ifc_object(cls, obj): pass def is_ifc_class_active(cls, ifc_class): pass def get_viewport_context(cls): pass diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 290b72f23a..73a7c7626a 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -17,13 +17,17 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import bmesh import json import ifcopenshell.api +import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.bim +import addon_utils from mathutils import Vector from pathlib import Path -import addon_utils +from blenderbim.bim.ifc import IFC_CONNECTED_TYPE +from typing import Any, Optional, Union, Literal, Iterable, Callable VIEWPORT_ATTRIBUTES = [ @@ -37,6 +41,8 @@ VIEWPORT_ATTRIBUTES = [ "clip_planes", ] +OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera] + class Blender(blenderbim.core.tool.Blender): OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE = ("MESH", "CURVE", "SURFACE", "META", "FONT", "LATTICE", "ARMATURE") @@ -44,7 +50,7 @@ class Blender(blenderbim.core.tool.Blender): TYPE_MANAGER_ICON = "LIGHTPROBE_VOLUME" if bpy.app.version >= (4, 1, 0) else "LIGHTPROBE_GRID" @classmethod - def get_area_props(cls, context): + def get_area_props(cls, context: bpy.types.Context) -> Any: try: if context.screen.name.endswith("-nonnormal"): # Ctrl-space temporary fullscreen screen = bpy.data.screens[context.screen.name[0 : -len("-nonnormal")]] @@ -56,12 +62,12 @@ class Blender(blenderbim.core.tool.Blender): return @classmethod - def set_active_object(cls, obj): + def set_active_object(cls, obj: bpy.types.Object) -> None: bpy.context.view_layer.objects.active = obj obj.select_set(True) @classmethod - def setup_tabs(cls): + def setup_tabs(cls) -> None: # https://blender.stackexchange.com/questions/140644/how-can-make-the-state-of-a-boolean-property-relative-to-the-3d-view-area for screen in bpy.data.screens: if len(screen.BIMAreaProperties) == 20: @@ -71,7 +77,7 @@ class Blender(blenderbim.core.tool.Blender): screen.BIMAreaProperties.add() @classmethod - def is_tab(cls, context, tab): + def is_tab(cls, context: bpy.types.Context, tab: str) -> bool: aprops = cls.get_area_props(context) if not aprops: return context.screen.BIMTabProperties.tab == tab @@ -80,7 +86,7 @@ class Blender(blenderbim.core.tool.Blender): return aprops.tab == tab @classmethod - def is_default_scene(cls): + def is_default_scene(cls) -> bool: if len(bpy.context.scene.objects) != 3: return False if {obj.type for obj in bpy.context.scene.objects} == {"MESH", "LIGHT", "CAMERA"}: @@ -88,7 +94,7 @@ class Blender(blenderbim.core.tool.Blender): return False @classmethod - def get_name(cls, ifc_class, name): + def get_name(cls, ifc_class: str, name: str) -> str: if not bpy.data.objects.get(f"{ifc_class}/{name}"): return name i = 2 @@ -97,13 +103,15 @@ class Blender(blenderbim.core.tool.Blender): return f"{name} {i}" @classmethod - def get_selected_objects(cls): + def get_selected_objects(cls) -> set[bpy.types.Object]: if bpy.context.selected_objects: return set(bpy.context.selected_objects + [bpy.context.active_object]) return set([bpy.context.active_object]) @classmethod - def create_ifc_object(cls, ifc_class: str, name: str = None, data=None) -> bpy.types.Object: + def create_ifc_object( + cls, ifc_class: str, name: Optional[str] = None, data: Optional[OBJECT_DATA_TYPE] = None + ) -> bpy.types.Object: name = name or "My " + ifc_class name = cls.get_name(ifc_class, name) obj = bpy.data.objects.new(name, data) @@ -111,7 +119,11 @@ class Blender(blenderbim.core.tool.Blender): return obj @classmethod - def get_obj_ifc_definition_id(cls, obj=None, obj_type=None): + def get_obj_ifc_definition_id( + cls, obj: Optional[str] = None, obj_type: Optional[str] = None, context: Optional[bpy.types.Context] = None + ) -> Union[int, None]: + if context is None: + context = bpy.context if obj_type == "Object": return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id elif obj_type == "Material": @@ -123,41 +135,42 @@ class Blender(blenderbim.core.tool.Blender): elif obj_type == "MaterialSetItem": return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id elif obj_type == "Task": - return bpy.context.scene.BIMTaskTreeProperties.tasks[ - bpy.context.scene.BIMWorkScheduleProperties.active_task_index + return context.scene.BIMTaskTreeProperties.tasks[ + context.scene.BIMWorkScheduleProperties.active_task_index ].ifc_definition_id elif obj_type == "Cost": - return bpy.context.scene.BIMCostProperties.cost_items[ - bpy.context.scene.BIMCostProperties.active_cost_item_index + return context.scene.BIMCostProperties.cost_items[ + context.scene.BIMCostProperties.active_cost_item_index ].ifc_definition_id elif obj_type == "Resource": - return bpy.context.scene.BIMResourceTreeProperties.resources[ - bpy.context.scene.BIMResourceProperties.active_resource_index + return context.scene.BIMResourceTreeProperties.resources[ + context.scene.BIMResourceProperties.active_resource_index ].ifc_definition_id elif obj_type == "Profile": - return bpy.context.scene.BIMProfileProperties.profiles[ - bpy.context.scene.BIMProfileProperties.active_profile_index + return context.scene.BIMProfileProperties.profiles[ + context.scene.BIMProfileProperties.active_profile_index ].ifc_definition_id elif obj_type == "WorkSchedule": - return bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id + return context.scene.BIMWorkScheduleProperties.active_work_schedule_id elif obj_type == "Group": - prop = bpy.context.scene.BIMGroupProperties + prop = context.scene.BIMGroupProperties return prop.groups[prop.active_group_index].ifc_definition_id @classmethod - def is_ifc_object(cls, obj): + def is_ifc_object(cls, obj: bpy.types.Object) -> bool: return bool(obj.BIMObjectProperties.ifc_definition_id) @classmethod - def is_ifc_class_active(cls, ifc_class): - if bpy.context.active_object: - if cls.is_ifc_object(bpy.context.active_object): - return tool.Ifc.get_entity(bpy.context.active_object).is_a(ifc_class) + def is_ifc_class_active(cls, ifc_class: str) -> bool: + obj = bpy.context.active_object + if obj: + if cls.is_ifc_object(obj): + return tool.Ifc.get_entity(obj).is_a(ifc_class) return False return False @classmethod - def show_info_message(cls, text, message_type="INFO"): + def show_info_message(cls, text: str, message_type: Literal["INFO", "ERROR"] = "INFO") -> None: """useful for showing error messages outside blender operators Possible `message_type`: `INFO` / `ERROR`""" @@ -168,14 +181,14 @@ class Blender(blenderbim.core.tool.Blender): bpy.context.window_manager.popup_menu(message_ui, title=message_type.capitalize(), icon=message_type) @classmethod - def get_view3d_area(cls): + def get_view3d_area(cls) -> Union[bpy.types.Area, None]: for window in bpy.context.window_manager.windows: for area in window.screen.areas: if area.type == "VIEW_3D": return area @classmethod - def get_blender_prop_default_value(cls, props, prop_name): + def get_blender_prop_default_value(cls, props, prop_name: str) -> Any: prop_bl_rna = props.bl_rna.properties[prop_name] if getattr(prop_bl_rna, "array_length", 0) > 0: prop_value = prop_bl_rna.default_array @@ -184,7 +197,7 @@ class Blender(blenderbim.core.tool.Blender): return prop_value @classmethod - def get_viewport_context(cls): + def get_viewport_context(cls) -> dict: """Get viewport area context for context overriding. Useful for calling operators outside viewport context. @@ -199,25 +212,25 @@ class Blender(blenderbim.core.tool.Blender): return context_override @classmethod - def get_viewport_position(cls): + def get_viewport_position(cls) -> dict: region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d copy_if_possible = lambda x: x.copy() if hasattr(x, "copy") else x viewport_data = {attr: copy_if_possible(getattr(region_3d, attr)) for attr in VIEWPORT_ATTRIBUTES} return viewport_data @classmethod - def set_viewport_position(cls, data): + def set_viewport_position(cls, data: dict) -> None: region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d for attr in VIEWPORT_ATTRIBUTES: setattr(region_3d, attr, data[attr]) @classmethod - def set_viewport_tool(cls, tool_name): + def set_viewport_tool(cls, tool_name: str) -> None: with bpy.context.temp_override(**tool.Blender.get_viewport_context()): bpy.ops.wm.tool_set_by_id(name=tool_name) @classmethod - def get_shader_editor_context(cls): + def get_shader_editor_context(cls) -> Union[dict, None]: for screen in bpy.data.screens: for area in screen.areas: if area.type == "NODE_EDITOR": @@ -227,7 +240,7 @@ class Blender(blenderbim.core.tool.Blender): return context_override @classmethod - def copy_node_graph(cls, material_to, material_from): + def copy_node_graph(cls, material_to: bpy.types.Material, material_from: bpy.types.Material) -> None: # https://projects.blender.org/blender/blender/issues/108763 if bpy.app.version[:2] == (4, 0): print( @@ -271,7 +284,9 @@ class Blender(blenderbim.core.tool.Blender): shader_editor.pin = previous_pin_setting @classmethod - def get_material_node(cls, blender_material, node_type, kwargs={}): + def get_material_node( + cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {} + ) -> Union[bpy.types.ShaderNode, None]: """returns first node from the `blender_material` shader graph with type `node_type`""" if not blender_material.use_nodes: return @@ -281,15 +296,15 @@ class Blender(blenderbim.core.tool.Blender): return node @classmethod - def update_screen(cls): + def update_screen(cls) -> None: bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1) @classmethod - def update_viewport(cls): + def update_viewport(cls) -> None: tool.Blender.get_viewport_context()["area"].tag_redraw() @classmethod - def force_depsgraph_update(cls): + def force_depsgraph_update(cls) -> None: """useful if you need to trigger callbacks like `depsgraph_update_pre`""" # blender is requiring some ID to be changed # to trigger depsgraph update @@ -298,9 +313,11 @@ class Blender(blenderbim.core.tool.Blender): bpy.context.view_layer.update() @classmethod - def ensure_unique_name(cls, name, objects, iteration=0): + def ensure_unique_name(cls, name: str, objects: Iterable[str], iteration=0) -> str: """returns a unique name for the given name and dictionary of objects - blender style name with .001, .002, etc. suffix + blender style name with .001, .002, etc. suffix. + + objects can be `bpy.data.objects`. """ current_iteration = name if not iteration else f"{name}.{iteration:03d}" if current_iteration not in objects: @@ -308,7 +325,7 @@ class Blender(blenderbim.core.tool.Blender): return cls.ensure_unique_name(name, objects, iteration + 1) @classmethod - def blender_path_to_posix(cls, blender_path): + def blender_path_to_posix(cls, blender_path: str) -> str: """Process blender path to be saved as posix. If path is relative the method will keep it relative to .ifc file @@ -323,7 +340,7 @@ class Blender(blenderbim.core.tool.Blender): return path.as_posix() @classmethod - def get_default_selection_keypmap(cls): + def get_default_selection_keypmap(cls) -> tuple: """keymap to replicate default blender selection behaviour with click and box selection""" # code below comes from blender_default.py which is part of default blender scripts licensed under GPL v2 # https://github.com/blender/blender/blob/master/release/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -370,7 +387,9 @@ class Blender(blenderbim.core.tool.Blender): return keymap @classmethod - def add_layout_hotkey_operator(cls, tool_name, layout, text, hotkey, description): + def add_layout_hotkey_operator( + cls, tool_name: str, layout: bpy.types.UILayout, text: str, hotkey: str, description: str + ) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]: modifiers = { "A": "EVENT_ALT", "S": "EVENT_SHIFT", @@ -387,7 +406,7 @@ class Blender(blenderbim.core.tool.Blender): return op, row @classmethod - def get_object_bounding_box(cls, obj): + def get_object_bounding_box(cls, obj: bpy.types.Object) -> dict: """Returns dict with local min and max x, y, z values for the object. Careful with using this method for objects in EDIT mode because @@ -408,14 +427,20 @@ class Blender(blenderbim.core.tool.Blender): return bbox_dict @classmethod - def select_and_activate_single_object(cls, context, active_object): + def select_and_activate_single_object(cls, context: bpy.types.Context, active_object: bpy.types.Object) -> None: for obj in context.selected_objects: obj.select_set(False) context.view_layer.objects.active = active_object active_object.select_set(True) @classmethod - def set_objects_selection(cls, context, active_object, selected_objects, clear_previous_selection=True): + def set_objects_selection( + cls, + context: bpy.types.Context, + active_object: Optional[bpy.types.Object] = None, + selected_objects: list[bpy.types.Object] = list(), + clear_previous_selection=True, + ) -> None: if clear_previous_selection: for obj in context.selected_objects: obj.select_set(False) @@ -426,7 +451,7 @@ class Blender(blenderbim.core.tool.Blender): active_object.select_set(True) @classmethod - def enum_property_has_valid_index(cls, props, prop_name, enum_items): + def enum_property_has_valid_index(cls, props, prop_name: str, enum_items: tuple) -> bool: """method created for readibility and to avoid console warnings like `pyrna_enum_to_py: current value '17' matches no enum in 'BIMModelProperties', '', 'relating_type_id'` """ @@ -437,7 +462,7 @@ class Blender(blenderbim.core.tool.Blender): return current_value_index < len(enum_items) @classmethod - def append_data_block(cls, filepath, data_block_type, name, link=False, relative=False): + def append_data_block(cls, filepath: str, data_block_type: str, name: str, link=False, relative=False) -> dict: if Path(filepath) == Path(bpy.data.filepath): data_block = getattr(bpy.data, data_block_type).get(name, None) if not data_block: @@ -451,7 +476,7 @@ class Blender(blenderbim.core.tool.Blender): return {"data_block": getattr(data_to, data_block_type)[0], "msg": ""} @classmethod - def remove_data_block(cls, data_block, do_unlink=True): + def remove_data_block(cls, data_block: bpy.types.ID, do_unlink=True) -> None: """Removes a datablock (such as a mesh) See https://projects.blender.org/blender/blender/issues/118787 for more @@ -473,7 +498,7 @@ class Blender(blenderbim.core.tool.Blender): ## BMESH UTILS ## @classmethod - def apply_bmesh(cls, mesh, bm, obj=None): + def apply_bmesh(cls, mesh: bpy.types.Mesh, bm: bmesh.types.BMesh, obj: Optional[bpy.types.Object] = None) -> None: """`obj` argument is not optional if you plan to update mesh in EDIT mode and it's possible that that mesh object won't be currenly active.""" import bmesh @@ -507,7 +532,7 @@ class Blender(blenderbim.core.tool.Blender): mesh.update() @classmethod - def get_bmesh_for_mesh(cls, mesh, clean=False): + def get_bmesh_for_mesh(cls, mesh: bpy.types.Mesh, clean=False) -> bmesh.types.BMesh: import bmesh if mesh.is_editmode: @@ -521,7 +546,16 @@ class Blender(blenderbim.core.tool.Blender): return bm @classmethod - def bmesh_join(cls, bm_a, bm_b, callback=None): + def bmesh_join( + cls, + bm_a: bmesh.types.BMesh, + bm_b: bmesh.types.BMesh, + callback: Optional[ + Callable[ + [bmesh.types.BMesh, list[bmesh.types.BMVert], list[bmesh.types.BMEdge], list[bmesh.types.BMFace]], None + ] + ] = None, + ): """Join two meshes into single one, store it in `bm_a`""" import bmesh @@ -536,7 +570,7 @@ class Blender(blenderbim.core.tool.Blender): return bm_a @classmethod - def toggle_edit_mode(cls, context): + def toggle_edit_mode(cls, context: bpy.types.Context) -> set: ao = context.active_object if not ao: return {"CANCELLED"} @@ -548,21 +582,21 @@ class Blender(blenderbim.core.tool.Blender): return {"CANCELLED"} @classmethod - def is_object_an_ifc_class(cls, obj, classes): + def is_object_an_ifc_class(cls, obj: bpy.types.Object, classes: Iterable[str]) -> bool: if not tool.Ifc.get(): return False element = tool.Ifc.get_entity(obj) return element and element.is_a() in classes @classmethod - def get_object_from_guid(cls, guid): + def get_object_from_guid(cls, guid: str) -> Union[IFC_CONNECTED_TYPE, None]: element = tool.Ifc.get().by_guid(guid) obj = tool.Ifc.get_object(element) if obj: return obj @classmethod - def lock_transform(cls, obj, lock_state=True): + def lock_transform(cls, obj: bpy.types.ObjectBase, lock_state=True) -> None: for prop in ("lock_location", "lock_rotation", "lock_scale"): attr = getattr(obj, prop) for axis_idx in range(3): @@ -571,7 +605,9 @@ class Blender(blenderbim.core.tool.Blender): operator_invoke_filepath_hotkeys_description = "Hold Shift to open the file, Alt to browse containing directory" @classmethod - def operator_invoke_filepath_hotkeys(cls, operator, context, event, filepath: Path): + def operator_invoke_filepath_hotkeys( + cls, operator: bpy.types.Operator, context: bpy.types.Context, event: bpy.types.Event, filepath: Path + ) -> Union[set, None]: if not event.alt and not event.shift: return diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py index 0d320df452..5427dd9c23 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py @@ -19,7 +19,11 @@ # Note: it is the intent for you to override these with your own functions -def get_application(ifc): +import ifcopenshell +from typing import Union + + +def get_application(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]: """Returns the application representing the authoring software It is expected for you to overload this function with your own @@ -39,7 +43,7 @@ def get_application(ifc): return (app or [None])[0] -def get_user(ifc): +def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]: """Returns the active authoring user It is expected for you to overload this function with your own diff --git a/src/ifcopenshell-python/ifcopenshell/util/doc.py b/src/ifcopenshell-python/ifcopenshell/util/doc.py index 6ee10ada7f..8f9f6cf34e 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/doc.py +++ b/src/ifcopenshell-python/ifcopenshell/util/doc.py @@ -18,10 +18,10 @@ import json from pathlib import Path -from pprint import pprint import copy import ifcopenshell import ifcopenshell.util.attribute +import ifcopenshell.util.schema try: import glob @@ -103,7 +103,7 @@ def get_db(version): return db.get(version) -def get_schema_by_name(version): +def get_schema_by_name(version: str): global schema_by_name version = ifcopenshell.util.schema.get_fallback_schema(version) if not schema_by_name[version]: diff --git a/src/ifcopenshell-python/ifcopenshell/util/placement.py b/src/ifcopenshell-python/ifcopenshell/util/placement.py index 6b82a293a2..f05959677d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/placement.py +++ b/src/ifcopenshell-python/ifcopenshell/util/placement.py @@ -20,8 +20,10 @@ import numpy as np import ifcopenshell from typing import Literal, Iterable +MatrixType = np.ndarray[np.ndarray[float]] -def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> np.array: + +def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixType: """Converts a location, X, and Z axis vector to a 4x4 transformation matrix IFC uses a right-handed coordinate system, so it is not necessary to @@ -34,7 +36,7 @@ def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> np.array: :param x: The +X vector / axis of the matrix :type x: iterable[float] :return: A 4x4 numpy matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ x = x / np.linalg.norm(x) z = z / np.linalg.norm(z) @@ -46,7 +48,7 @@ def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> np.array: return r.T -def get_axis2placement(placement: ifcopenshell.entity_instance) -> np.array: +def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType: """Parses an IfcAxis2Placement (2D or 3D) to a 4x4 transformation matrix Note that this function only parses a single placement axis. If you want to @@ -57,7 +59,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> np.array: :param placement: The IfcLocalPlacement enitity :type placement: ifcopenshell.entity_instance.entity_instance :return: A 4x4 numpy matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ if placement.is_a("IfcAxis2Placement3D"): z = np.array(placement.Axis.DirectionRatios if placement.Axis else (0, 0, 1)) @@ -74,7 +76,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> np.array: return a2p(o, z, x) -def get_local_placement(placement: ifcopenshell.entity_instance) -> np.array: +def get_local_placement(placement: ifcopenshell.entity_instance) -> MatrixType: """Parse a local placement into a 4x4 transformation matrix This is typically used to find the location and rotation of an element. The @@ -97,7 +99,7 @@ def get_local_placement(placement: ifcopenshell.entity_instance) -> np.array: :param placement: The IfcLocalPlacement entity :type placement: ifcopenshell.entity_instance.entity_instance :return: A 4x4 numpy matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ if placement is None: return np.eye(4) @@ -108,7 +110,7 @@ def get_local_placement(placement: ifcopenshell.entity_instance) -> np.array: return np.dot(parent, get_axis2placement(placement.RelativePlacement)) -def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) -> np.array: +def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) -> MatrixType: """Parses an IfcCartesianTransformationOperator into a 4x4 transformation matrix Note that in general you will not need to call this directly. See @@ -117,7 +119,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) -> :param item: The IfcCartesianTransformationOperator entity :type item: ifcopenshell.entity_instance.entity_instance :return: A 4x4 numpy transformation matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ origin = np.array(inst.LocalOrigin.Coordinates) axis1 = np.array((1.0, 0.0, 0.0)) @@ -153,7 +155,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) -> return m4 -def get_mappeditem_transformation(item: ifcopenshell.entity_instance) -> np.array: +def get_mappeditem_transformation(item: ifcopenshell.entity_instance) -> MatrixType: """Parse an IfcMappedItem into a 4x4 transformation matrix Mapped items take a representation with an origin and transform them with a @@ -163,7 +165,7 @@ def get_mappeditem_transformation(item: ifcopenshell.entity_instance) -> np.arra :param item: The IfcMappedItem entity :type item: ifcopenshell.entity_instance.entity_instance :return: A 4x4 numpy transformation matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ m4 = get_axis2placement(item.MappingSource.MappingOrigin) # TODO 2d @@ -188,7 +190,7 @@ def get_storey_elevation(storey: ifcopenshell.entity_instance) -> float: return getattr(storey, "Elevation", 0.0) or 0.0 -def rotation(angle: float, axis: Literal["X", "Y", "Z"], is_degrees=True) -> np.array: +def rotation(angle: float, axis: Literal["X", "Y", "Z"], is_degrees=True) -> MatrixType: """Create a 4x4 numpy matrix representing an euler rotation :param angle: The angle of rotation @@ -199,7 +201,7 @@ def rotation(angle: float, axis: Literal["X", "Y", "Z"], is_degrees=True) -> np. radians. Defaults to true (i.e. degrees). :type is_degrees: bool :return: A 4x4 numpy rotation matrix - :rtype: np.array[np.array[float]] + :rtype: np.ndarray[np.ndarray[float]] """ theta = np.radians(angle) if is_degrees else angle cos, sin = np.cos(theta), np.sin(theta) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py index f4a6335f0b..4aaf6a08fe 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py @@ -24,7 +24,7 @@ import ifcopenshell.util.representation from typing import Optional, Literal, Union, Iterable tol = 1e-6 -AXIS_LITERAL = Union[Literal["X"], Literal["Y"], Literal["Z"]] +AXIS_LITERAL = Literal["X", "Y", "Z"] VECTOR_3D = tuple[float, float, float] diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index 03900607b1..aebb8df9b5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -603,7 +603,7 @@ def format_length( precision: float, decimal_places: int = 2, suppress_zero_inches=True, - unit_system: Union[Literal["metric"], Literal["imperial"]] = "imperial", + unit_system: Literal["metric", "imperial"] = "imperial", input_unit="foot", output_unit="foot", ) -> str: