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 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 # hack to close popup
# https://blender.stackexchange.com/a/202576/130742 # https://blender.stackexchange.com/a/202576/130742
def close_operator_panel(event): def close_operator_panel(event):
@@ -293,7 +293,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
identification = active_reference[1] identification = active_reference[1]
for obj in objects: 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) element = tool.Ifc.get().by_id(ifc_definition_id)
references = ifcopenshell.util.classification.get_references(element, should_inherit=False) references = ifcopenshell.util.classification.get_references(element, should_inherit=False)
for reference in references: for reference in references:
@@ -358,7 +358,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
break break
for obj in objects: 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: if not ifc_definition_id:
continue continue
ifcopenshell.api.run( ifcopenshell.api.run(
@@ -406,7 +406,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
classification.Location = bsdd_classification.domain_namespace_uri classification.Location = bsdd_classification.domain_namespace_uri
for obj in objects: 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: if not ifc_definition_id:
continue continue
element = tool.Ifc.get().by_id(ifc_definition_id) element = tool.Ifc.get().by_id(ifc_definition_id)
@@ -19,6 +19,7 @@
import os import os
import bpy import bpy
import ifcopenshell import ifcopenshell
import ifcopenshell.util.doc
import ifcopenshell.util.schema import ifcopenshell.util.schema
import blenderbim.tool as tool import blenderbim.tool as tool
@@ -85,7 +85,6 @@ class EnablePsetEditing(bpy.types.Operator):
def execute(self, context): def execute(self, context):
self.props = get_pset_props(context, self.obj, self.obj_type) self.props = get_pset_props(context, self.obj, self.obj_type)
self.props.properties.clear() 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: if self.pset_id:
pset = tool.Ifc.get().by_id(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) props = get_pset_props(context, self.obj, self.obj_type)
if props.active_pset_id: if props.active_pset_id:
pset = tool.Ifc.get().by_id(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): if tool.Pset.is_pset_empty(pset):
ifcopenshell.api.run( ifcopenshell.api.run(
"pset.remove_pset", tool.Ifc.get(), product=tool.Ifc.get().by_id(ifc_definition_id), pset=pset "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): def _execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type) 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) element = tool.Ifc.get().by_id(ifc_definition_id)
properties = {} properties = {}
@@ -343,7 +342,7 @@ class RemovePset(bpy.types.Operator, Operator):
pset_name = tool.Ifc.get().by_id(self.pset_id).Name pset_name = tool.Ifc.get().by_id(self.pset_id).Name
for obj in objects: for obj in objects:
props = get_pset_props(context, obj, self.obj_type) 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) element = tool.Ifc.get().by_id(ifc_definition_id)
pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None) pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None)
if pset: if pset:
@@ -374,7 +373,7 @@ class AddQto(bpy.types.Operator, Operator):
def _execute(self, context): def _execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type) 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) element = tool.Ifc.get().by_id(ifc_definition_id)
bpy.ops.bim.enable_pset_editing( 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 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_name(cls, ifc_class, name): pass
def get_selected_objects(cls): pass def get_selected_objects(cls): pass
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None): 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_object(cls, obj): pass
def is_ifc_class_active(cls, ifc_class): pass def is_ifc_class_active(cls, ifc_class): pass
def get_viewport_context(cls): 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/>. # along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
import bmesh
import json import json
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.element
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.bim import blenderbim.bim
import addon_utils
from mathutils import Vector from mathutils import Vector
from pathlib import Path 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 = [ VIEWPORT_ATTRIBUTES = [
@@ -37,6 +41,8 @@ VIEWPORT_ATTRIBUTES = [
"clip_planes", "clip_planes",
] ]
OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera]
class Blender(blenderbim.core.tool.Blender): class Blender(blenderbim.core.tool.Blender):
OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE = ("MESH", "CURVE", "SURFACE", "META", "FONT", "LATTICE", "ARMATURE") 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" TYPE_MANAGER_ICON = "LIGHTPROBE_VOLUME" if bpy.app.version >= (4, 1, 0) else "LIGHTPROBE_GRID"
@classmethod @classmethod
def get_area_props(cls, context): def get_area_props(cls, context: bpy.types.Context) -> Any:
try: try:
if context.screen.name.endswith("-nonnormal"): # Ctrl-space temporary fullscreen if context.screen.name.endswith("-nonnormal"): # Ctrl-space temporary fullscreen
screen = bpy.data.screens[context.screen.name[0 : -len("-nonnormal")]] screen = bpy.data.screens[context.screen.name[0 : -len("-nonnormal")]]
@@ -56,12 +62,12 @@ class Blender(blenderbim.core.tool.Blender):
return return
@classmethod @classmethod
def set_active_object(cls, obj): def set_active_object(cls, obj: bpy.types.Object) -> None:
bpy.context.view_layer.objects.active = obj bpy.context.view_layer.objects.active = obj
obj.select_set(True) obj.select_set(True)
@classmethod @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 # 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: for screen in bpy.data.screens:
if len(screen.BIMAreaProperties) == 20: if len(screen.BIMAreaProperties) == 20:
@@ -71,7 +77,7 @@ class Blender(blenderbim.core.tool.Blender):
screen.BIMAreaProperties.add() screen.BIMAreaProperties.add()
@classmethod @classmethod
def is_tab(cls, context, tab): def is_tab(cls, context: bpy.types.Context, tab: str) -> bool:
aprops = cls.get_area_props(context) aprops = cls.get_area_props(context)
if not aprops: if not aprops:
return context.screen.BIMTabProperties.tab == tab return context.screen.BIMTabProperties.tab == tab
@@ -80,7 +86,7 @@ class Blender(blenderbim.core.tool.Blender):
return aprops.tab == tab return aprops.tab == tab
@classmethod @classmethod
def is_default_scene(cls): def is_default_scene(cls) -> bool:
if len(bpy.context.scene.objects) != 3: if len(bpy.context.scene.objects) != 3:
return False return False
if {obj.type for obj in bpy.context.scene.objects} == {"MESH", "LIGHT", "CAMERA"}: 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 return False
@classmethod @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}"): if not bpy.data.objects.get(f"{ifc_class}/{name}"):
return name return name
i = 2 i = 2
@@ -97,13 +103,15 @@ class Blender(blenderbim.core.tool.Blender):
return f"{name} {i}" return f"{name} {i}"
@classmethod @classmethod
def get_selected_objects(cls): def get_selected_objects(cls) -> set[bpy.types.Object]:
if bpy.context.selected_objects: if bpy.context.selected_objects:
return set(bpy.context.selected_objects + [bpy.context.active_object]) return set(bpy.context.selected_objects + [bpy.context.active_object])
return set([bpy.context.active_object]) return set([bpy.context.active_object])
@classmethod @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 = name or "My " + ifc_class
name = cls.get_name(ifc_class, name) name = cls.get_name(ifc_class, name)
obj = bpy.data.objects.new(name, data) obj = bpy.data.objects.new(name, data)
@@ -111,7 +119,11 @@ class Blender(blenderbim.core.tool.Blender):
return obj return obj
@classmethod @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": if obj_type == "Object":
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "Material": elif obj_type == "Material":
@@ -123,41 +135,42 @@ class Blender(blenderbim.core.tool.Blender):
elif obj_type == "MaterialSetItem": elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
elif obj_type == "Task": elif obj_type == "Task":
return bpy.context.scene.BIMTaskTreeProperties.tasks[ return context.scene.BIMTaskTreeProperties.tasks[
bpy.context.scene.BIMWorkScheduleProperties.active_task_index context.scene.BIMWorkScheduleProperties.active_task_index
].ifc_definition_id ].ifc_definition_id
elif obj_type == "Cost": elif obj_type == "Cost":
return bpy.context.scene.BIMCostProperties.cost_items[ return context.scene.BIMCostProperties.cost_items[
bpy.context.scene.BIMCostProperties.active_cost_item_index context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id ].ifc_definition_id
elif obj_type == "Resource": elif obj_type == "Resource":
return bpy.context.scene.BIMResourceTreeProperties.resources[ return context.scene.BIMResourceTreeProperties.resources[
bpy.context.scene.BIMResourceProperties.active_resource_index context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id ].ifc_definition_id
elif obj_type == "Profile": elif obj_type == "Profile":
return bpy.context.scene.BIMProfileProperties.profiles[ return context.scene.BIMProfileProperties.profiles[
bpy.context.scene.BIMProfileProperties.active_profile_index context.scene.BIMProfileProperties.active_profile_index
].ifc_definition_id ].ifc_definition_id
elif obj_type == "WorkSchedule": 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": elif obj_type == "Group":
prop = bpy.context.scene.BIMGroupProperties prop = context.scene.BIMGroupProperties
return prop.groups[prop.active_group_index].ifc_definition_id return prop.groups[prop.active_group_index].ifc_definition_id
@classmethod @classmethod
def is_ifc_object(cls, obj): def is_ifc_object(cls, obj: bpy.types.Object) -> bool:
return bool(obj.BIMObjectProperties.ifc_definition_id) return bool(obj.BIMObjectProperties.ifc_definition_id)
@classmethod @classmethod
def is_ifc_class_active(cls, ifc_class): def is_ifc_class_active(cls, ifc_class: str) -> bool:
if bpy.context.active_object: obj = bpy.context.active_object
if cls.is_ifc_object(bpy.context.active_object): if obj:
return tool.Ifc.get_entity(bpy.context.active_object).is_a(ifc_class) if cls.is_ifc_object(obj):
return tool.Ifc.get_entity(obj).is_a(ifc_class)
return False return False
return False return False
@classmethod @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 """useful for showing error messages outside blender operators
Possible `message_type`: `INFO` / `ERROR`""" 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) bpy.context.window_manager.popup_menu(message_ui, title=message_type.capitalize(), icon=message_type)
@classmethod @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 window in bpy.context.window_manager.windows:
for area in window.screen.areas: for area in window.screen.areas:
if area.type == "VIEW_3D": if area.type == "VIEW_3D":
return area return area
@classmethod @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] prop_bl_rna = props.bl_rna.properties[prop_name]
if getattr(prop_bl_rna, "array_length", 0) > 0: if getattr(prop_bl_rna, "array_length", 0) > 0:
prop_value = prop_bl_rna.default_array prop_value = prop_bl_rna.default_array
@@ -184,7 +197,7 @@ class Blender(blenderbim.core.tool.Blender):
return prop_value return prop_value
@classmethod @classmethod
def get_viewport_context(cls): def get_viewport_context(cls) -> dict:
"""Get viewport area context for context overriding. """Get viewport area context for context overriding.
Useful for calling operators outside viewport context. Useful for calling operators outside viewport context.
@@ -199,25 +212,25 @@ class Blender(blenderbim.core.tool.Blender):
return context_override return context_override
@classmethod @classmethod
def get_viewport_position(cls): def get_viewport_position(cls) -> dict:
region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
copy_if_possible = lambda x: x.copy() if hasattr(x, "copy") else x 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} viewport_data = {attr: copy_if_possible(getattr(region_3d, attr)) for attr in VIEWPORT_ATTRIBUTES}
return viewport_data return viewport_data
@classmethod @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 region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
for attr in VIEWPORT_ATTRIBUTES: for attr in VIEWPORT_ATTRIBUTES:
setattr(region_3d, attr, data[attr]) setattr(region_3d, attr, data[attr])
@classmethod @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()): with bpy.context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.wm.tool_set_by_id(name=tool_name) bpy.ops.wm.tool_set_by_id(name=tool_name)
@classmethod @classmethod
def get_shader_editor_context(cls): def get_shader_editor_context(cls) -> Union[dict, None]:
for screen in bpy.data.screens: for screen in bpy.data.screens:
for area in screen.areas: for area in screen.areas:
if area.type == "NODE_EDITOR": if area.type == "NODE_EDITOR":
@@ -227,7 +240,7 @@ class Blender(blenderbim.core.tool.Blender):
return context_override return context_override
@classmethod @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 # https://projects.blender.org/blender/blender/issues/108763
if bpy.app.version[:2] == (4, 0): if bpy.app.version[:2] == (4, 0):
print( print(
@@ -271,7 +284,9 @@ class Blender(blenderbim.core.tool.Blender):
shader_editor.pin = previous_pin_setting shader_editor.pin = previous_pin_setting
@classmethod @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`""" """returns first node from the `blender_material` shader graph with type `node_type`"""
if not blender_material.use_nodes: if not blender_material.use_nodes:
return return
@@ -281,15 +296,15 @@ class Blender(blenderbim.core.tool.Blender):
return node return node
@classmethod @classmethod
def update_screen(cls): def update_screen(cls) -> None:
bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1) bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
@classmethod @classmethod
def update_viewport(cls): def update_viewport(cls) -> None:
tool.Blender.get_viewport_context()["area"].tag_redraw() tool.Blender.get_viewport_context()["area"].tag_redraw()
@classmethod @classmethod
def force_depsgraph_update(cls): def force_depsgraph_update(cls) -> None:
"""useful if you need to trigger callbacks like `depsgraph_update_pre`""" """useful if you need to trigger callbacks like `depsgraph_update_pre`"""
# blender is requiring some ID to be changed # blender is requiring some ID to be changed
# to trigger depsgraph update # to trigger depsgraph update
@@ -298,9 +313,11 @@ class Blender(blenderbim.core.tool.Blender):
bpy.context.view_layer.update() bpy.context.view_layer.update()
@classmethod @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 """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}" current_iteration = name if not iteration else f"{name}.{iteration:03d}"
if current_iteration not in objects: 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) return cls.ensure_unique_name(name, objects, iteration + 1)
@classmethod @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. """Process blender path to be saved as posix.
If path is relative the method will keep it relative to .ifc file 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() return path.as_posix()
@classmethod @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""" """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 # 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 # 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 return keymap
@classmethod @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 = { modifiers = {
"A": "EVENT_ALT", "A": "EVENT_ALT",
"S": "EVENT_SHIFT", "S": "EVENT_SHIFT",
@@ -387,7 +406,7 @@ class Blender(blenderbim.core.tool.Blender):
return op, row return op, row
@classmethod @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. """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 Careful with using this method for objects in EDIT mode because
@@ -408,14 +427,20 @@ class Blender(blenderbim.core.tool.Blender):
return bbox_dict return bbox_dict
@classmethod @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: for obj in context.selected_objects:
obj.select_set(False) obj.select_set(False)
context.view_layer.objects.active = active_object context.view_layer.objects.active = active_object
active_object.select_set(True) active_object.select_set(True)
@classmethod @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: if clear_previous_selection:
for obj in context.selected_objects: for obj in context.selected_objects:
obj.select_set(False) obj.select_set(False)
@@ -426,7 +451,7 @@ class Blender(blenderbim.core.tool.Blender):
active_object.select_set(True) active_object.select_set(True)
@classmethod @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 """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'` `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) return current_value_index < len(enum_items)
@classmethod @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): if Path(filepath) == Path(bpy.data.filepath):
data_block = getattr(bpy.data, data_block_type).get(name, None) data_block = getattr(bpy.data, data_block_type).get(name, None)
if not data_block: 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": ""} return {"data_block": getattr(data_to, data_block_type)[0], "msg": ""}
@classmethod @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) """Removes a datablock (such as a mesh)
See https://projects.blender.org/blender/blender/issues/118787 for more See https://projects.blender.org/blender/blender/issues/118787 for more
@@ -473,7 +498,7 @@ class Blender(blenderbim.core.tool.Blender):
## BMESH UTILS ## ## BMESH UTILS ##
@classmethod @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 """`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.""" and it's possible that that mesh object won't be currenly active."""
import bmesh import bmesh
@@ -507,7 +532,7 @@ class Blender(blenderbim.core.tool.Blender):
mesh.update() mesh.update()
@classmethod @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 import bmesh
if mesh.is_editmode: if mesh.is_editmode:
@@ -521,7 +546,16 @@ class Blender(blenderbim.core.tool.Blender):
return bm return bm
@classmethod @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`""" """Join two meshes into single one, store it in `bm_a`"""
import bmesh import bmesh
@@ -536,7 +570,7 @@ class Blender(blenderbim.core.tool.Blender):
return bm_a return bm_a
@classmethod @classmethod
def toggle_edit_mode(cls, context): def toggle_edit_mode(cls, context: bpy.types.Context) -> set:
ao = context.active_object ao = context.active_object
if not ao: if not ao:
return {"CANCELLED"} return {"CANCELLED"}
@@ -548,21 +582,21 @@ class Blender(blenderbim.core.tool.Blender):
return {"CANCELLED"} return {"CANCELLED"}
@classmethod @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(): if not tool.Ifc.get():
return False return False
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
return element and element.is_a() in classes return element and element.is_a() in classes
@classmethod @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) element = tool.Ifc.get().by_guid(guid)
obj = tool.Ifc.get_object(element) obj = tool.Ifc.get_object(element)
if obj: if obj:
return obj return obj
@classmethod @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"): for prop in ("lock_location", "lock_rotation", "lock_scale"):
attr = getattr(obj, prop) attr = getattr(obj, prop)
for axis_idx in range(3): 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" operator_invoke_filepath_hotkeys_description = "Hold Shift to open the file, Alt to browse containing directory"
@classmethod @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: if not event.alt and not event.shift:
return return
@@ -19,7 +19,11 @@
# Note: it is the intent for you to override these with your own functions # 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 """Returns the application representing the authoring software
It is expected for you to overload this function with your own 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] 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 """Returns the active authoring user
It is expected for you to overload this function with your own It is expected for you to overload this function with your own
@@ -18,10 +18,10 @@
import json import json
from pathlib import Path from pathlib import Path
from pprint import pprint
import copy import copy
import ifcopenshell import ifcopenshell
import ifcopenshell.util.attribute import ifcopenshell.util.attribute
import ifcopenshell.util.schema
try: try:
import glob import glob
@@ -103,7 +103,7 @@ def get_db(version):
return db.get(version) return db.get(version)
def get_schema_by_name(version): def get_schema_by_name(version: str):
global schema_by_name global schema_by_name
version = ifcopenshell.util.schema.get_fallback_schema(version) version = ifcopenshell.util.schema.get_fallback_schema(version)
if not schema_by_name[version]: if not schema_by_name[version]:
@@ -20,8 +20,10 @@ import numpy as np
import ifcopenshell import ifcopenshell
from typing import Literal, Iterable 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 """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 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 :param x: The +X vector / axis of the matrix
:type x: iterable[float] :type x: iterable[float]
:return: A 4x4 numpy matrix :return: A 4x4 numpy matrix
:rtype: np.array[np.array[float]] :rtype: np.ndarray[np.ndarray[float]]
""" """
x = x / np.linalg.norm(x) x = x / np.linalg.norm(x)
z = z / np.linalg.norm(z) 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 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 """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 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 :param placement: The IfcLocalPlacement enitity
:type placement: ifcopenshell.entity_instance.entity_instance :type placement: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy matrix :return: A 4x4 numpy matrix
:rtype: np.array[np.array[float]] :rtype: np.ndarray[np.ndarray[float]]
""" """
if placement.is_a("IfcAxis2Placement3D"): if placement.is_a("IfcAxis2Placement3D"):
z = np.array(placement.Axis.DirectionRatios if placement.Axis else (0, 0, 1)) 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) 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 """Parse a local placement into a 4x4 transformation matrix
This is typically used to find the location and rotation of an element. The 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 :param placement: The IfcLocalPlacement entity
:type placement: ifcopenshell.entity_instance.entity_instance :type placement: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy matrix :return: A 4x4 numpy matrix
:rtype: np.array[np.array[float]] :rtype: np.ndarray[np.ndarray[float]]
""" """
if placement is None: if placement is None:
return np.eye(4) 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)) 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 """Parses an IfcCartesianTransformationOperator into a 4x4 transformation matrix
Note that in general you will not need to call this directly. See 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 :param item: The IfcCartesianTransformationOperator entity
:type item: ifcopenshell.entity_instance.entity_instance :type item: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy transformation matrix :return: A 4x4 numpy transformation matrix
:rtype: np.array[np.array[float]] :rtype: np.ndarray[np.ndarray[float]]
""" """
origin = np.array(inst.LocalOrigin.Coordinates) origin = np.array(inst.LocalOrigin.Coordinates)
axis1 = np.array((1.0, 0.0, 0.0)) axis1 = np.array((1.0, 0.0, 0.0))
@@ -153,7 +155,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) ->
return m4 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 """Parse an IfcMappedItem into a 4x4 transformation matrix
Mapped items take a representation with an origin and transform them with a 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 :param item: The IfcMappedItem entity
:type item: ifcopenshell.entity_instance.entity_instance :type item: ifcopenshell.entity_instance.entity_instance
:return: A 4x4 numpy transformation matrix :return: A 4x4 numpy transformation matrix
:rtype: np.array[np.array[float]] :rtype: np.ndarray[np.ndarray[float]]
""" """
m4 = get_axis2placement(item.MappingSource.MappingOrigin) m4 = get_axis2placement(item.MappingSource.MappingOrigin)
# TODO 2d # TODO 2d
@@ -188,7 +190,7 @@ def get_storey_elevation(storey: ifcopenshell.entity_instance) -> float:
return getattr(storey, "Elevation", 0.0) or 0.0 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 """Create a 4x4 numpy matrix representing an euler rotation
:param angle: The angle of 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). radians. Defaults to true (i.e. degrees).
:type is_degrees: bool :type is_degrees: bool
:return: A 4x4 numpy rotation matrix :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 theta = np.radians(angle) if is_degrees else angle
cos, sin = np.cos(theta), np.sin(theta) cos, sin = np.cos(theta), np.sin(theta)
@@ -24,7 +24,7 @@ import ifcopenshell.util.representation
from typing import Optional, Literal, Union, Iterable from typing import Optional, Literal, Union, Iterable
tol = 1e-6 tol = 1e-6
AXIS_LITERAL = Union[Literal["X"], Literal["Y"], Literal["Z"]] AXIS_LITERAL = Literal["X", "Y", "Z"]
VECTOR_3D = tuple[float, float, float] VECTOR_3D = tuple[float, float, float]
@@ -603,7 +603,7 @@ def format_length(
precision: float, precision: float,
decimal_places: int = 2, decimal_places: int = 2,
suppress_zero_inches=True, suppress_zero_inches=True,
unit_system: Union[Literal["metric"], Literal["imperial"]] = "imperial", unit_system: Literal["metric", "imperial"] = "imperial",
input_unit="foot", input_unit="foot",
output_unit="foot", output_unit="foot",
) -> str: ) -> str: