typing and small refactor

This commit is contained in:
Andrej730
2024-03-22 13:58:07 +05:00
parent 5aa6123806
commit 7132c4e9bf
11 changed files with 126 additions and 117 deletions
-33
View File
@@ -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):
@@ -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)
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell
import ifcopenshell.util.doc
import ifcopenshell.util.schema
import blenderbim.tool as tool
@@ -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
+1 -1
View File
@@ -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
+93 -57
View File
@@ -17,13 +17,17 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
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
@@ -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
@@ -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]:
@@ -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)
@@ -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]
@@ -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: