# SPDX-License-Identifier: MIT OR GPL-3.0-or-later import sys from collections.abc import Iterator from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path from typing import Optional import bpy from bpy.types import Context, Object from mathutils import Matrix from .logger import get_logger logger = get_logger(__name__) @dataclass(frozen=True) class SavedWorkspace: # Be careful not to include objects that might disappear before and after yield cursor_matrix: Matrix previous_object_name: Optional[str] previous_object_mode: Optional[str] active_object_name: Optional[str] active_object_hide_viewport: bool def enter_save_workspace( context: Context, obj: Optional[Object] = None, *, mode: str = "OBJECT" ) -> SavedWorkspace: # Save the position of the 3D cursor cursor_matrix = context.scene.cursor.matrix.copy() previous_object_name = None previous_object_mode = None previous_object = context.view_layer.objects.active if previous_object: # previous_object may disappear after yield. # We intern it just in case, but it might not be necessary. previous_object_name = sys.intern(previous_object.name) previous_object_mode = sys.intern(previous_object.mode) # If an obj argument is passed, set the mode to "OBJECT". If the mode # stays as is, it may not be possible to change the active object if previous_object != obj and previous_object_mode != "OBJECT": previous_object_hide_viewport = previous_object.hide_viewport if previous_object_hide_viewport: # If hide_viewport is True, mode_set may fail if left as is previous_object.hide_viewport = False bpy.ops.object.mode_set(mode="OBJECT") if previous_object.hide_viewport != previous_object_hide_viewport: previous_object.hide_viewport = previous_object_hide_viewport # If an object is passed, make it active if obj is not None: context.view_layer.objects.active = obj context.view_layer.update() active_object_name = None active_object_hide_viewport = False # Change the mode of the active object active_object = context.view_layer.objects.active if active_object: # active_object may disappear after yield. # We intern it just in case, but it might not be necessary. active_object_name = sys.intern(active_object.name) active_object_hide_viewport = active_object.hide_viewport # If hide_viewport is True, mode_set may fail if left as is # Currently we force it to be visible even when not changing mode, # but this may be inappropriate if active_object_hide_viewport: active_object.hide_viewport = False if active_object.mode != mode: bpy.ops.object.mode_set(mode=mode) return SavedWorkspace( cursor_matrix=cursor_matrix, previous_object_name=previous_object_name, previous_object_mode=previous_object_mode, active_object_name=active_object_name, active_object_hide_viewport=active_object_hide_viewport, ) def exit_save_workspace(context: Context, saved_workspace: SavedWorkspace) -> None: cursor_matrix = saved_workspace.cursor_matrix previous_object_name = saved_workspace.previous_object_name previous_object_mode = saved_workspace.previous_object_mode active_object_name = saved_workspace.active_object_name active_object_hide_viewport = saved_workspace.active_object_hide_viewport previous_object = None if previous_object_name is not None: previous_object = context.blend_data.objects.get(previous_object_name) current_active_object = context.view_layer.objects.active # Set the mode of the currently active object to "OBJECT". If the mode stays as is, # it may not be possible to change the active object if ( current_active_object and current_active_object != previous_object and current_active_object.mode != "OBJECT" ): current_active_object_hide_viewport = current_active_object.hide_viewport if current_active_object.hide_viewport: current_active_object.hide_viewport = False bpy.ops.object.mode_set(mode="OBJECT") if current_active_object.hide_viewport != current_active_object_hide_viewport: current_active_object.hide_viewport = current_active_object_hide_viewport # Restore the hide_viewport of the activated object if active_object_name is not None: active_object = context.blend_data.objects.get(active_object_name) if active_object and active_object.hide_viewport != active_object_hide_viewport: active_object.hide_viewport = active_object_hide_viewport # Return to the originally active object previous_object = None if previous_object_name is not None: previous_object = context.blend_data.objects.get(previous_object_name) if context.view_layer.objects.active != previous_object: context.view_layer.objects.active = previous_object context.view_layer.update() # Restore the mode of the originally active object if ( previous_object and previous_object_mode is not None and previous_object_mode != previous_object.mode ): # If hide_viewport is True, mode_set may fail if left as is # Restore hide_viewport after mode_set is completed previous_object_hide_viewport = previous_object.hide_viewport if previous_object_hide_viewport: previous_object.hide_viewport = False bpy.ops.object.mode_set(mode=previous_object_mode) if previous_object.hide_viewport != previous_object_hide_viewport: previous_object.hide_viewport = previous_object_hide_viewport # Restore the 3D cursor position context.scene.cursor.matrix = cursor_matrix @contextmanager def save_workspace( context: Context, obj: Optional[Object] = None, *, mode: str = "OBJECT" ) -> Iterator[None]: saved_workspace = enter_save_workspace(context, obj, mode=mode) try: yield # After yield, bpy native objects may be deleted or become invalid # as frames advance. Accessing them in this state can cause crashes, # so be careful not to access potentially invalid native objects # after yield finally: exit_save_workspace(context, saved_workspace) def wm_append_without_library( context: Context, blend_path: Path, *, append_filepath: str, append_filename: str, append_directory: str, ) -> set[str]: """Call wm.append and remove the added libraries. Used to work around the issue where wm.append adding libraries causes asset library addition to fail. https://github.com/saturday06/VRM-Addon-for-Blender/issues/631 https://github.com/saturday06/VRM-Addon-for-Blender/issues/646 """ # https://projects.blender.org/blender/blender/src/tag/v2.93.18/source/blender/windowmanager/intern/wm_files_link.c#L85-L90 with save_workspace(context): # List of pointers for library addition detection. # Used only for addition detection. Be careful not to dereference, # as it is dangerous. existing_library_pointers: list[int] = [ library.as_pointer() for library in context.blend_data.libraries ] result = bpy.ops.wm.append( filepath=append_filepath, filename=append_filename, directory=append_directory, link=False, ) if result != {"FINISHED"}: return result # Remove one added library. # Reverse order to handle recursive calls, but effectiveness is unconfirmed. for library in reversed(list(context.blend_data.libraries)): if not blend_path.samefile(library.filepath): continue if library.as_pointer() in existing_library_pointers: continue if bpy.app.version >= (3, 2) and library.use_extra_user: library.use_extra_user = False if library.users: if bpy.app.version >= (3, 2): logger.warning( 'Failed to remove "%s" with %d users' " while appending blend file:" ' filepath="%s" filename="%s" directory="%s"', library.name, library.users, append_filepath, append_filename, append_directory, ) else: context.blend_data.libraries.remove(library) break return result