import contextlib
from collections.abc import Iterator
from typing import Any, Literal, Optional
import bpy
# https://docs.blender.org/api/current/bpy_types_enum_items/object_mode_items.html#rna-enum-object-mode-items
Mode = Literal[
"OBJECT",
"EDIT",
"POSE",
"SCULPT",
"VERTEX_PAINT",
"WEIGHT_PAINT",
"TEXTURE_PAINT",
"PARTICLE_EDIT",
"EDIT_GPENCIL",
"SCULPT_GPENCIL",
"PAINT_GPENCIL",
"WEIGHT_GPENCIL",
"VERTEX_GPENCIL",
]
# https://docs.blender.org/api/current/bpy_types_enum_items/mesh_select_mode_items.html#rna-enum-mesh-select-mode-items
MeshSelectMode = Literal["VERT", "EDGE", "FACE"]
[docs]@contextlib.contextmanager
def mode(
bobj: Any,
*,
mode: Mode = "OBJECT",
mesh_select_mode: set[MeshSelectMode] = set(),
exit_mode: Optional[Mode] = None,
exit_mesh_select_mode: Optional[set[MeshSelectMode]] = None,
) -> Iterator[None]:
"""Context manager to set the active object and its modes.
Sets mode `mode` with mesh select modes `mesh_select_mode` for an
object `bobj` and upon exiting the context sets mode `exit_mode` and
mesh select modes `exit_mesh_select_mode`.
This is useful for example to perform changes in EDIT mode that
require a switch to a different mode to be written to the object's
data.
Parameters
----------
bobj : bpy.types.Object
The object that is set to be the active object.
mode : str, default="OBJECT"
The mode of `bobj` within the context. Must be one of
"OBJECT", "EDIT", "POSE", "SCULPT", "VERTEX_PAINT", "WEIGHT_PAINT",
"TEXTURE_PAINT", "PARTICLE_EDIT", "EDIT_GPENCIL", "SCULPT_GPENCIL",
"PAINT_GPENCIL", "WEIGHT_GPENCIL", "VERTEX_GPENCIL".
mesh_select_mode : set of { "VERT", "EDGE", "FACE" }, default=set()
The mesh select modes of `bobj` within the context. This
argument is ignored unless `mode == EDIT`.
exit_mode : same type as `mode` or None, default=None
The mode of `bobj` after exiting the context. If None, this will be
the same as `mode`.
exit_mesh_select_mode : same type as `mesh_select_mode` or None, default=None
The mesh select modes of `bobj` after exiting the context. If None,
this will be the same as `mesh_select_mode`.
Examples
--------
>>> import bpy
>>> from ddg.visualization.blender.context import mode
>>>
>>> bpy.ops.mesh.primitive_cube_add()
{'FINISHED'}
>>> cube = bpy.context.object
>>> with mode(cube, mode="EDIT", exit_mode="OBJECT"):
... print(f"Mode within the context: {cube.mode}")
...
Mode within the context: EDIT
>>> print(f"Mode after exiting the context: {cube.mode}")
Mode after exiting the context: OBJECT
Note that Blender doesn't write changes to edit meshes to the
mesh until the object exits edit mode.
>>> import bmesh
>>> import numpy as np
>>>
>>> co_original = np.vstack([v.co for v in cube.data.vertices])
>>> with mode(cube, mode="EDIT", exit_mode="OBJECT"):
... bm = bmesh.from_edit_mesh(cube.data)
... for v in bm.verts:
... v.co *= 3
... bmesh.update_edit_mesh(cube.data)
... co_in_context = np.vstack([v.co for v in cube.data.vertices])
...
... # Note that even after calling bmesh.update_edit_mesh,
... # cube.data.vertices coordinates haven't changed.
... # The changes are visible in the UI however!
... assert np.allclose(co_original, co_in_context)
...
Upon exiting the context, the mode is set to OBJECT mode. Since this
is a different mode from EDIT mode, the changes made in EDIT mode
are written to `cube.data`.
>>> co_after_exit = np.vstack([v.co for v in cube.data.vertices])
>>> assert np.allclose(3 * co_original, co_after_exit)
See Also
--------
bpy.ops.object.mode_set
Sets the object interaction mode.
bpy.ops.object.mode_set_with_submode
Sets the object interaction mode and optionally also the mesh
select modes. Used internally by this context manager.
bpy.types.Context.temp_override
A context manager that temporarily changes the values of
bpy.types.Context objects, e.g. bpy.context.
""" # noqa: E501
exit_mode_ = mode if exit_mode is None else exit_mode
exit_mesh_select_mode_ = (
mesh_select_mode if exit_mesh_select_mode is None else exit_mesh_select_mode
)
try:
bpy.context.view_layer.objects.active = bobj
bpy.ops.object.mode_set_with_submode(
mode=mode, toggle=False, mesh_select_mode=mesh_select_mode
)
yield
finally:
bpy.context.view_layer.objects.active = bobj
bpy.ops.object.mode_set_with_submode(
mode=exit_mode_,
toggle=False,
mesh_select_mode=exit_mesh_select_mode_,
)