Files
blender-portable-repo/scripts/addons/RetopoFlow/retopoflow/rf/rf_blender_save.py
T
2026-03-17 14:30:01 -06:00

378 lines
14 KiB
Python

'''
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 <http://www.gnu.org/licenses/>.
'''
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!',
'',
'''<label><input type="checkbox" checked="BoundBool(opt_autosave)">Check Auto Save option when RetopoFlow starts</label>''',
])]
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}`',
'',
'''<label><input type="checkbox" checked="BoundBool(opt_unsaved)">Run check for unsaved .blend file when RetopoFlow starts</label>''',
])]
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}')