''' Copyright (C) 2023 CG Cookie http://cgcookie.com hello@cgcookie.com Created by Jonathan Denning, Jonathan Williamson This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import os import bpy import json import time from datetime import datetime from itertools import chain from mathutils import Matrix, Vector from bpy_extras.object_utils import object_data_add from bpy.app.handlers import persistent from ...config.options import options, sessionoptions, retopoflow_datablocks from ...addon_common.common.globals import Globals from ...addon_common.common.boundvar import BoundBool from ...addon_common.common.decorators import blender_version_wrapper from ...addon_common.common.blender import ( set_object_selection, set_active_object, toggle_screen_header, toggle_screen_toolbar, toggle_screen_properties, toggle_screen_lastop, show_error_message, BlenderSettings, get_view3d_space, ) from ...addon_common.common.blender_preferences import get_preferences from ...addon_common.common.maths import BBox from ...addon_common.common.debug import dprint from .rf_blender_objects import RetopoFlow_Blender_Objects @persistent def revert_auto_save_after_load(*_, **__): # remove recover handler bpy.app.handlers.load_post.remove(revert_auto_save_after_load) window = bpy.context.window screen = window.screen area = next((a for a in screen.areas if a.type == 'VIEW_3D'), None) assert area space_data = next((s for s in area.spaces if s.type == 'VIEW_3D'), None) assert space_data region = next((r for r in area.regions if r.type == 'WINDOW'), None) assert region with bpy.context.temp_override(window=window, screen=screen, area=area, space_data=space_data, region=region): RetopoFlow_Blender_Save.recovery_revert() class RetopoFlow_Blender_Save: ''' backup / restore methods ''' @staticmethod def can_recover(): if retopoflow_datablocks['rotate object'] in bpy.data.objects: return True if sessionoptions.has_active_session_data(): return True if RetopoFlow_Blender_Objects.any_marked_sources_target(): return True return False @staticmethod def recovery_revert(): print('RetopoFlow: recovering from auto save') # the rotate object should not exist, but just in case if retopoflow_datablocks['rotate object'] in bpy.data.objects: bpy.data.objects.remove( bpy.data.objects[retopoflow_datablocks['rotate object']], do_unlink=True, ) # restore blender settings if sessionoptions.has_session_data(): data = dict(sessionoptions['blender']) print(f'RetopoFlow: Restoring Session Data') print(f' {data}') bs = BlenderSettings(init_storage=data) bs.restore_all() space = get_view3d_space() r3d = space.region_3d normalize_opts = sessionoptions['normalize'] # scale view orig_view = normalize_opts['view'] r3d.view_distance = orig_view['distance'] r3d.view_location = Vector(orig_view['location']) # scale clip start and end orig_clip = normalize_opts['clip distances'] space.clip_start = orig_clip['start'] space.clip_end = orig_clip['end'] # scale meshes prev_factor = normalize_opts['mesh scaling factor'] M = (Matrix.Identity(3) * (1.0 / prev_factor)).to_4x4() sources = RetopoFlow_Blender_Objects.get_sources() target = RetopoFlow_Blender_Objects.get_target() for obj in chain(sources, [target]): if not obj: continue obj.matrix_world = M @ obj.matrix_world if target: try: # try to select object target.select_set(True) bpy.context.view_layer.objects.active = target bpy.ops.object.mode_set(mode='EDIT') except: pass sessionoptions.clear() # unmark all objects RetopoFlow_Blender_Objects.unmark_sources_target() # # grab previous blender state # if options['blender state'] in bpy.data.texts: # data = json.loads(bpy.data.texts[options['blender state']].as_string()) # # get target object and reset settings # tar_object = bpy.data.objects[data['active object']] # tar_object.hide_viewport = False # tar_object.hide_render = False # bpy.context.view_layer.objects.active = tar_object # tar_object.select_set(True) # RetopoFlow_Normalize.end_normalize(bpy.context) # bpy.data.texts.remove( # bpy.data.texts[options['blender state']], # do_unlink=True, # ) # restore window state (mostly tool, properties, header, etc.) # RetopoFlow_Blender_UI.restore_window_state( # ignore_panels=False, # ignore_mode=False, # ) @staticmethod def get_auto_save_settings(context): prefs = get_preferences(context) use_auto_save = prefs.filepaths.use_auto_save_temporary_files path_blend = getattr(bpy.data, 'filepath', '') path_autosave = options.get_auto_save_filepath() good_auto_save = (not options['check auto save']) or use_auto_save good_unsaved = (not options['check unsaved']) or path_blend return { 'auto save': good_auto_save, 'auto save path': path_autosave, 'saved': good_unsaved, } def check_auto_save_warnings(self): settings = RetopoFlow_Blender_Save.get_auto_save_settings(self.actions.context) save = self.actions.to_human_readable('blender save') good_auto_save = settings['auto save'] path_autosave = settings['auto save path'] good_unsaved = settings['saved'] if good_auto_save and good_unsaved: return message = [] if not good_auto_save: opt_autosave = '''options['check auto save']''' message += ['\n'.join([ 'The Auto Save option in Blender (Edit > Preferences > Save & Load > Auto Save) is currently disabled.', 'Your changes will _NOT_ be saved automatically!', '', '''''', ])] if not good_unsaved: opt_unsaved = '''options['check unsaved']''' message += ['\n'.join([ 'You are currently working on an _UNSAVED_ Blender file.', f'Your changes will be saved to `{path_autosave}` when you press `{save}`', '', '''''', ])] else: message += ['Press `%s` any time to save your changes.' % (save)] self.alert_user( title='Blender auto save / save file checker', message='\n\n'.join(message), level='warning', ) def handle_auto_save(self): prefs = get_preferences(self.actions.context) use_auto_save = prefs.filepaths.use_auto_save_temporary_files auto_save_time = prefs.filepaths.auto_save_time * 60 if not use_auto_save: return # Blender's auto save is disabled :( if not hasattr(self, 'time_to_save'): # RF just started, so do not save yet self.last_change_count = None # record the next time to save self.time_to_save = time.time() + auto_save_time elif time.time() > self.time_to_save: # it is time to save, but only if current tool is in main and changes were made! if self.rftool._fsm_in_main(): if self.save_backup(): # save was successful! # record the next time to save self.time_to_save = time.time() + auto_save_time else: # save was unsuccessful :( # try again in 10secs self.time_to_save = time.time() + 10 @staticmethod def has_auto_save(): filepath = options['last auto save path'] return filepath and os.path.exists(filepath) @staticmethod def get_auto_save_filename(): return options['last auto save path'] @staticmethod def recover_auto_save(): filepath = options['last auto save path'] print(f'backup recover: {filepath}') if not filepath or not os.path.exists(filepath): print(f' DOES NOT EXIST!') return bpy.app.handlers.load_post.append(revert_auto_save_after_load) bpy.ops.wm.open_mainfile(filepath=filepath) @staticmethod def delete_auto_save(): filepath = options['last auto save path'] print(f'backup delete: {filepath}') if not filepath or not os.path.exists(filepath): print(f' DOES NOT EXIST!') return os.remove(filepath) def save_emergency(self): try: filepath = options.get_auto_save_filepath(suffix='EMERGENCY', emergency=True) bpy.ops.wm.save_as_mainfile( filepath=filepath, compress=True, # write compressed file check_existing=False, # do not warn if file already exists copy=True, # does not make saved file active ) except: self.done(emergency_bail=True) show_error_message( '\n'.join([ 'RetopoFlow crashed unexpectedly.', 'Be sure to save your work, and report what happened so that we can try fixing it.', ]), title='RetopoFlow Error', ) def save_backup(self): if hasattr(self, '_backup_broken'): return if self.last_change_count == self.change_count: print(f'RetopoFlow: skipping backup save (no changes detected)') return True if not hasattr(self, '_backup_save_attempts'): self._backup_save_attempts = 0 filepath = options.get_auto_save_filepath() filepath1 = f'{filepath}1' print(f'RetopoFlow: saving backup to {filepath}') errors = {} if os.path.exists(filepath): if os.path.exists(filepath1): try: print(f' deleting old backup: {filepath1}') os.remove(filepath1) except Exception as e: print(f' caught exception: {e}') errors['delete old'] = e try: print(f' renaming prev backup: {filepath1}') os.rename(filepath, filepath1) except Exception as e: print(f' caught exception: {e}') errors['rename prev'] = e if 'rename prev' not in errors: try: print(f' saving...') bpy.ops.wm.save_as_mainfile( filepath=filepath, compress=True, # write compressed file check_existing=False, # do not warn if file already exists copy=True, # does not make saved file active ) options['last auto save path'] = filepath self.last_change_count = self.change_count except Exception as e: print(f' caught exception: {e}') errors['saving'] = e else: ''' skipping normal save, because we might lose data! ''' errors['skipped save'] = 'error while trying to rename prev' if not errors: # all went well! self._backup_save_attempts = 0 return True print(f' Something happened') print(f' {errors=}') self._backup_save_attempts += 1 if self._backup_save_attempts < 4: print(f' Trying again soon...') else: print(f' Alerting user...') self._backup_broken = True self.alert_user( title='Could not save backup', level='assert', message=( f'Could not save backup file. ' f'Temporarily preventing further backup attempts. ' f'You might try saving file manually.\n\n' f'File paths: `{filepath}`, `{filepath1}`\n\n' f'Errors: {errors}\n\n' ), ) return False def save_normal(self): with self.blender_ui_pause(): with sessionoptions.temp_disable(): try: bpy.ops.wm.save_mainfile() except Exception as e: # could not save for some reason; let the artist know! self.alert_user( title='Could not save', message=f'Could not save blend file.\n\nError message: "{e}"', level='warning', ) # note: filepath might not be set until after save filepath = os.path.abspath(bpy.data.filepath) print(f'RetopoFlow: saved to {filepath}')