2026-02-16
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
This add-on makes Blender's autosaves a bit more powerful and reliable.
|
||||
[Incremental Auto-Save](https://extensions.blender.org/add-ons/incremental-auto-save/) makes Blender's autosaves more configurable and reliable.
|
||||
|
||||

|
||||
|
||||
I specifically created this add-on to be able to share my user preferences across multiple computers. Normally, when you specify an autosave path in your Blender preferences, and that path does not exist, autosaves will simply not work at all. Also, Blender's autosaves overwrite each other, so you only have one backup per file, making shorter autosave intervals risky, since you might want to go back more than a few minutes in your saves.
|
||||
|
||||
### Features:
|
||||
- Incremental autosave: Automatically save in configurable time intervals, without each autosave overwriting the previous one.
|
||||
- Incremental auto-save: Automatically save in configurable time intervals, without each autosave overwriting the previous one.
|
||||
- Customizable configuration: Set the maximum number of saves per file and the interval between saves according to your preferences.
|
||||
- Autosave on file switch: Automatically save the current file when opening another, ensuring your progress is always backed up.
|
||||
- Autosave for unsaved files: Automatically save files that have not been saved before, giving them the name "Unnamed.blend".
|
||||
- Support multiple computers: Specify a list of file paths, where the first valid one will be used for saving backups. Useful when you share your user preferences across multiple computers.
|
||||
- Invalid path fallback: If no valid specified file is found, create backups next to the current .blend, or the OS temp folder.
|
||||
- Save next to .blend: Instead of a global auto-save directory, your autosaves can go to an "Autosave" folder next to your .blend.
|
||||
- Invalid path fallback: If none of the given paths are valid, create backups next to the current .blend, or the OS temp folder.
|
||||
- Save modified images: For texture painting workflows, this will auto-save your images as well as the .blend file.
|
||||
- Save Mesh/Sculpt: If you spend a long time in Edit/Sculpt mode and Blender crashes, you will lose your work, because the mesh data is not written back into the object until you go to Object Mode. The add-on will periodically enter object mode for you, without interrupting any Sculpt/Paint/Transform operations.
|
||||
|
||||
It goes without saying that this add-on can introduce periodic lag. This is simply the price of saving data to a disk. A slow disk or a large file will exacerbate this, and all you can do is adjust your save interval according to what you can put up with.
|
||||
@@ -1,26 +1,36 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
from bl_ui.generic_ui_list import draw_ui_list
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import AddonPreferences, PropertyGroup, UIList
|
||||
from rna_prop_ui import IDPropertyGroup
|
||||
|
||||
|
||||
bl_info = {
|
||||
"name": "Incremental Autosave",
|
||||
"author": "Demeter Dzadik",
|
||||
"version": (1, 1, 0),
|
||||
"version": (1, 1, 1),
|
||||
"blender": (2, 90, 0),
|
||||
"location": "blender",
|
||||
"description": "Autosaves in a way where subsequent autosaves don't overwrite previous ones",
|
||||
"category": "System",
|
||||
}
|
||||
|
||||
import os, tempfile, json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, IntProperty, StringProperty, CollectionProperty
|
||||
from bpy.types import PropertyGroup, AddonPreferences, UIList
|
||||
from bpy.app.handlers import persistent
|
||||
from bl_ui.generic_ui_list import draw_ui_list
|
||||
|
||||
# Timestamp format for prefixing autosave file names.
|
||||
TIME_FMT_STR = '%Y-%M-%d_%H-%M'
|
||||
TIME_FMT_STR = "%Y-%m-%d_%H-%M"
|
||||
|
||||
# Timestamp of when Blender is launched. Used to avoid creating an autosave when opening Blender.
|
||||
LAUNCH_TIME = datetime.now()
|
||||
@@ -30,47 +40,36 @@ class INCSAVE_UL_file_paths(UIList):
|
||||
def draw_item(
|
||||
self, context, layout, data, item, icon_value, active_data, active_propname
|
||||
):
|
||||
filepath = item
|
||||
filepath: AutoSavePath = item
|
||||
|
||||
row = layout.row()
|
||||
split = row.split(factor=0.2)
|
||||
split.prop(item, 'name', text="")
|
||||
split.prop(filepath, "name", text="")
|
||||
row = split.row()
|
||||
if not os.path.exists(item.path):
|
||||
if not os.path.exists(filepath.path):
|
||||
row.alert = True
|
||||
row.prop(item, 'path', text="")
|
||||
row.prop(filepath, "path", text="")
|
||||
|
||||
|
||||
def get_addon_prefs(context=None):
|
||||
context = context or bpy.context
|
||||
return context.preferences.addons[__name__].preferences
|
||||
|
||||
def update_prefs_on_file(self, context):
|
||||
|
||||
def update_prefs_on_file(self=None, context=None):
|
||||
prefs = get_addon_prefs(context)
|
||||
if not type(prefs).loading:
|
||||
prefs.save_prefs_to_file()
|
||||
|
||||
|
||||
class AutoSavePath(PropertyGroup):
|
||||
name: StringProperty(
|
||||
update=update_prefs_on_file
|
||||
)
|
||||
path: StringProperty(
|
||||
name="Autosave Path",
|
||||
description="An autosave path. If this filepath doesn't exist, the next existing one will be used. If none are valid, the Blender auto-save path will be used. If that's not valid either, the OS default temp folder will be used",
|
||||
subtype='FILE_PATH',
|
||||
default="",
|
||||
update=update_prefs_on_file
|
||||
)
|
||||
|
||||
|
||||
class PrefsFileSaveLoadMixin:
|
||||
"""Mix-in class that can be used by any add-on to store their preferences in a file,
|
||||
so that they don't get lost when the add-on is disabled.
|
||||
To use it, just do this:
|
||||
To use it, copy this class and the function above it, and do this in your code:
|
||||
|
||||
```
|
||||
import bpy
|
||||
import bpy, json
|
||||
from pathlib import Path
|
||||
|
||||
class MyAddonPrefs(PrefsFileSaveLoadMixin, bpy.types.AddonPreferences):
|
||||
some_prop: bpy.props.IntProperty(update=update_prefs_on_file)
|
||||
@@ -78,10 +77,16 @@ class PrefsFileSaveLoadMixin:
|
||||
def register():
|
||||
bpy.utils.register_class(MyAddonPrefs)
|
||||
MyAddonPrefs.register_autoload_from_file()
|
||||
|
||||
def unregister():
|
||||
update_prefs_on_file()
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
# List of property names to not write to disk.
|
||||
omit_from_disk: list[str] = []
|
||||
|
||||
loading = False
|
||||
|
||||
@staticmethod
|
||||
@@ -89,33 +94,48 @@ class PrefsFileSaveLoadMixin:
|
||||
def timer_func(_scene=None):
|
||||
prefs = get_addon_prefs()
|
||||
prefs.load_prefs_from_file()
|
||||
|
||||
bpy.app.timers.register(timer_func, first_interval=delay)
|
||||
|
||||
@staticmethod
|
||||
def prefs_to_dict_recursive(propgroup: 'IDPropertyGroup') -> dict:
|
||||
def prefs_to_dict_recursive(self, propgroup: IDPropertyGroup) -> dict:
|
||||
"""Recursively convert AddonPreferences to a dictionary.
|
||||
Note that AddonPreferences don't support PointerProperties,
|
||||
so this function doesn't either."""
|
||||
from rna_prop_ui import IDPropertyGroup
|
||||
|
||||
ret = {}
|
||||
|
||||
if hasattr(propgroup, 'bl_rna'):
|
||||
rna_class = propgroup.bl_rna
|
||||
|
||||
rna_class = None
|
||||
if isinstance(propgroup, bpy.types.AddonPreferences):
|
||||
prop_dict = {
|
||||
key: getattr(propgroup, key)
|
||||
for key in propgroup.bl_rna.properties.keys()
|
||||
if key not in ("rna_type", "bl_idname")
|
||||
}
|
||||
else:
|
||||
property_group_class_name = type(propgroup).__name__
|
||||
rna_class = bpy.types.PropertyGroup.bl_rna_get_subclass_py(property_group_class_name)
|
||||
rna_class = bpy.types.PropertyGroup.bl_rna_get_subclass_py(
|
||||
property_group_class_name
|
||||
)
|
||||
if not hasattr(rna_class, "properties"):
|
||||
rna_class = None
|
||||
prop_dict = {
|
||||
key: getattr(propgroup, key)
|
||||
for key in propgroup.bl_rna.properties.keys()
|
||||
if key not in ("rna_type")
|
||||
}
|
||||
|
||||
this_func = PrefsFileSaveLoadMixin.prefs_to_dict_recursive
|
||||
for key, value in propgroup.items():
|
||||
if type(value) == list:
|
||||
ret[key] = [this_func(elem) for elem in value]
|
||||
elif type(value) == IDPropertyGroup:
|
||||
ret[key] = this_func(value)
|
||||
for key, value in prop_dict.items():
|
||||
if key in type(self).omit_from_disk:
|
||||
continue
|
||||
if type(value) in (list, bpy.types.bpy_prop_collection_idprop):
|
||||
ret[key] = [self.prefs_to_dict_recursive(elem) for elem in value]
|
||||
elif type(value) is IDPropertyGroup:
|
||||
ret[key] = self.prefs_to_dict_recursive(value)
|
||||
else:
|
||||
if (
|
||||
rna_class and
|
||||
key in rna_class.properties and
|
||||
hasattr(rna_class.properties[key], 'enum_items')
|
||||
rna_class
|
||||
and key in rna_class.properties
|
||||
and hasattr(rna_class.properties[key], "enum_items")
|
||||
):
|
||||
# Save enum values as string, not int.
|
||||
ret[key] = rna_class.properties[key].enum_items[value].identifier
|
||||
@@ -123,32 +143,30 @@ class PrefsFileSaveLoadMixin:
|
||||
ret[key] = value
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def apply_prefs_from_dict_recursive(propgroup, data):
|
||||
this_func = PrefsFileSaveLoadMixin.apply_prefs_from_dict_recursive
|
||||
def apply_prefs_from_dict_recursive(self, propgroup, data):
|
||||
for key, value in data.items():
|
||||
if not hasattr(propgroup, key):
|
||||
# Property got removed or renamed in the implementation.
|
||||
continue
|
||||
if type(value) == list:
|
||||
if type(value) is list:
|
||||
for elem in value:
|
||||
collprop = getattr(propgroup, key)
|
||||
entry = collprop.get(elem['name'])
|
||||
entry = collprop.get(elem["name"])
|
||||
if not entry:
|
||||
entry = collprop.add()
|
||||
this_func(entry, elem)
|
||||
elif type(value) == dict:
|
||||
this_func(getattr(propgroup, key), value)
|
||||
self.apply_prefs_from_dict_recursive(entry, elem)
|
||||
elif type(value) is dict:
|
||||
self.apply_prefs_from_dict_recursive(getattr(propgroup, key), value)
|
||||
else:
|
||||
setattr(propgroup, key, value)
|
||||
|
||||
@staticmethod
|
||||
def get_prefs_filepath() -> Path:
|
||||
addon_name = __package__.split(".")[-1]
|
||||
return Path(bpy.utils.user_resource('CONFIG')) / Path(addon_name + ".txt")
|
||||
return Path(bpy.utils.user_resource("CONFIG")) / Path(addon_name + ".txt")
|
||||
|
||||
def save_prefs_to_file(self, _context=None):
|
||||
data_dict = self.prefs_to_dict_recursive(self)
|
||||
data_dict = self.prefs_to_dict_recursive(propgroup=self)
|
||||
|
||||
with open(self.get_prefs_filepath(), "w") as f:
|
||||
json.dump(data_dict, f, indent=4)
|
||||
@@ -163,24 +181,38 @@ class PrefsFileSaveLoadMixin:
|
||||
type(self).loading = True
|
||||
try:
|
||||
self.apply_prefs_from_dict_recursive(self, addon_data)
|
||||
except Exception as exc:
|
||||
except Exception:
|
||||
# If we get an error raised here, and it isn't handled,
|
||||
# the add-on seems to break.
|
||||
print(f"Failed to load {__package__} preferences from file.")
|
||||
# raise exc
|
||||
type(self).loading = False
|
||||
|
||||
|
||||
class AutoSavePath(PropertyGroup):
|
||||
name: StringProperty(update=update_prefs_on_file)
|
||||
path: StringProperty(
|
||||
name="Autosave Path",
|
||||
description="An autosave path. If this filepath doesn't exist, the next existing one will be used. If none are valid, the Blender auto-save path will be used. If that's not valid either, the OS default temp folder will be used",
|
||||
subtype="FILE_PATH",
|
||||
default="",
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
|
||||
|
||||
class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
bl_idname = __name__
|
||||
|
||||
omit_from_disk = ["active_index"]
|
||||
|
||||
save_before_close: BoolProperty(
|
||||
name='Save Before File Open',
|
||||
description='Save the current file before opening another file',
|
||||
name="Save Before File Open",
|
||||
description="Save the current file before opening another file",
|
||||
default=True,
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
save_interval: IntProperty(
|
||||
name='Save Interval (Minutes)',
|
||||
name="Save Interval (Minutes)",
|
||||
description="Number of minutes between each save while the add-on is enabled",
|
||||
default=5,
|
||||
min=1,
|
||||
@@ -190,25 +222,53 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
)
|
||||
|
||||
max_save_files: bpy.props.IntProperty(
|
||||
name='Max Backups Per File',
|
||||
description='Maximum number of backups to save for each file, 0 means unlimited. Otherwise, the oldest file will be deleted after reaching the limit',
|
||||
name="Max Backups Per File",
|
||||
description="Maximum number of backups to save for each file, 0 means unlimited. Otherwise, the oldest file will be deleted after reaching the limit",
|
||||
default=10,
|
||||
min=0,
|
||||
max=100,
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
compress_files: bpy.props.BoolProperty(
|
||||
name='Compress Files',
|
||||
description='Save backups with compression enabled',
|
||||
name="Compress Files",
|
||||
description="Save backups with compression enabled",
|
||||
default=True,
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
|
||||
autosave_paths: CollectionProperty(type=AutoSavePath)
|
||||
relative_to_blend: BoolProperty(
|
||||
name="Save Next To Blend",
|
||||
description='When the .blend file is saved, save auto-saves next to it, into an "Autosave" folder.\nIf the .blend is not saved, it will fall back to the list of filepaths provided above, or system temp folder.',
|
||||
default=False,
|
||||
)
|
||||
save_images: BoolProperty(
|
||||
name="Save Images",
|
||||
description="Useful for long periods of texture painting. This will auto-save images - both packed AND UNPACKED! Best to use in conjunction with a version control system like GitLFS/SVN!",
|
||||
default=True,
|
||||
)
|
||||
save_sculpt: BoolProperty(
|
||||
name="Save Mesh/Sculpt",
|
||||
description="Useful for long periods of mesh editing/sculpting. Normally if Blender crashes during mesh edit/sculpt, you lose your changes since the last time you entered Object mode. If this is enabled, the add-on will periodically switch to Object mode for you, and back. This will only trigger after dependency graph updates, so it doesn't happen in the middle of a stroke.",
|
||||
default=True,
|
||||
)
|
||||
print_saves: BoolProperty(
|
||||
name="Print Saves",
|
||||
description="Print in the System Console (aka Terminal) on each autosave.",
|
||||
default=True,
|
||||
)
|
||||
active_index: IntProperty()
|
||||
|
||||
def get_valid_autosave_path(self, context):
|
||||
def get_relative_dir(self) -> str:
|
||||
assert bpy.data.filepath
|
||||
return os.sep.join((os.path.dirname(bpy.data.filepath), "Autosaves"))
|
||||
|
||||
def get_valid_autosave_path(self, context) -> str:
|
||||
"""Return an autosave path that will always actually exist, no matter how desperate."""
|
||||
prefs = get_addon_prefs(context)
|
||||
if bpy.data.filepath and prefs.relative_to_blend:
|
||||
return self.get_relative_dir()
|
||||
|
||||
# Try the native autosave path first.
|
||||
default_path = bpy.context.preferences.filepaths.temporary_directory
|
||||
if os.path.exists(default_path):
|
||||
@@ -219,9 +279,9 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
if os.path.exists(path.path):
|
||||
return path.path
|
||||
|
||||
# If none of those exist, return the .blend's directory.
|
||||
# If none of those exist, return Autosave dir next to .blend.
|
||||
if bpy.data.filepath:
|
||||
return os.path.dirname(bpy.data.filepath)
|
||||
return self.get_relative_dir()
|
||||
|
||||
# And if that doesn't exist either, fall back to the sys temp dir.
|
||||
sys_temp = tempfile.gettempdir()
|
||||
@@ -232,53 +292,60 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
layout.use_property_decorate = False
|
||||
layout.use_property_split = True
|
||||
|
||||
split = layout.split(factor=0.4)
|
||||
split.row()
|
||||
split.label(text="First valid path will be used:")
|
||||
header, panel = layout.panel("Incremental Autosave: Paths", default_closed=True)
|
||||
header.label(text="Autosave Paths")
|
||||
if panel:
|
||||
split = panel.split(factor=0.4)
|
||||
split.row()
|
||||
split.label(text="First valid path will be used:")
|
||||
|
||||
native_row = layout.row()
|
||||
if not os.path.exists(context.preferences.filepaths.temporary_directory):
|
||||
native_row.alert = True
|
||||
native_row.prop(
|
||||
context.preferences.filepaths,
|
||||
'temporary_directory',
|
||||
text="Native Autosave Path",
|
||||
)
|
||||
native_row = panel.row()
|
||||
if not os.path.exists(context.preferences.filepaths.temporary_directory):
|
||||
native_row.alert = True
|
||||
native_row.prop(
|
||||
context.preferences.filepaths,
|
||||
"temporary_directory",
|
||||
text="Native Autosave Path",
|
||||
)
|
||||
|
||||
draw_ui_list(
|
||||
layout,
|
||||
context,
|
||||
class_name='INCSAVE_UL_file_paths',
|
||||
list_path=f'preferences.addons["{__package__}"].preferences.autosave_paths',
|
||||
active_index_path=f'preferences.addons["{__package__}"].preferences.active_index',
|
||||
insertion_operators=True,
|
||||
move_operators=True,
|
||||
unique_id='Incremental Autosave Path List',
|
||||
)
|
||||
draw_ui_list(
|
||||
panel,
|
||||
context,
|
||||
class_name="INCSAVE_UL_file_paths",
|
||||
list_path=f'preferences.addons["{__package__}"].preferences.autosave_paths',
|
||||
active_index_path=f'preferences.addons["{__package__}"].preferences.active_index',
|
||||
insertion_operators=True,
|
||||
move_operators=True,
|
||||
unique_id="Incremental Autosave Path List",
|
||||
)
|
||||
|
||||
layout.separator()
|
||||
panel.prop(self, "relative_to_blend")
|
||||
|
||||
split = layout.split(factor=0.4)
|
||||
split.row()
|
||||
split.label(
|
||||
text="Current autosave path: " + str(self.get_valid_autosave_path(context))
|
||||
)
|
||||
row = split.row()
|
||||
row.alignment = "RIGHT"
|
||||
row.label(text="Current Autosave Path")
|
||||
split.label(text=str(self.get_valid_autosave_path(context)))
|
||||
|
||||
layout.separator()
|
||||
|
||||
layout.prop(self, 'save_interval')
|
||||
layout.prop(self, 'max_save_files')
|
||||
layout.prop(self, 'save_before_close')
|
||||
layout.prop(self, "save_interval")
|
||||
layout.prop(self, "max_save_files")
|
||||
layout.prop(self, "save_before_close")
|
||||
layout.separator()
|
||||
layout.prop(self, 'compress_files')
|
||||
layout.prop(self, "compress_files")
|
||||
layout.prop(self, "print_saves")
|
||||
layout.separator()
|
||||
layout.prop(self, "save_images")
|
||||
layout.prop(self, "save_sculpt")
|
||||
|
||||
|
||||
def save_file():
|
||||
addon_prefs = get_addon_prefs()
|
||||
|
||||
basename = bpy.data.filepath
|
||||
if basename == '':
|
||||
basename = 'Unnamed.blend'
|
||||
if basename == "":
|
||||
basename = "Unnamed.blend"
|
||||
else:
|
||||
basename = bpy.path.basename(basename)
|
||||
|
||||
@@ -286,8 +353,10 @@ def save_file():
|
||||
save_dir = bpy.path.abspath(addon_prefs.get_valid_autosave_path(bpy.context))
|
||||
if not os.path.isdir(save_dir):
|
||||
os.mkdir(save_dir)
|
||||
except:
|
||||
print("Incremental Autosave: Error creating auto save directory.")
|
||||
except PermissionError:
|
||||
print(
|
||||
f"Incremental Autosave: No permission to create auto save directory:\n{save_dir}"
|
||||
)
|
||||
return
|
||||
|
||||
# Delete old files, to limit the number of saves.
|
||||
@@ -304,20 +373,51 @@ def save_file():
|
||||
old_file = os.path.join(save_dir, otherfiles[0])
|
||||
os.remove(old_file)
|
||||
otherfiles.pop(0)
|
||||
except:
|
||||
except PermissionError:
|
||||
print("Incremental Autosave: Unable to remove old files.")
|
||||
|
||||
# Save the copy.
|
||||
time = datetime.now()
|
||||
filename = time.strftime(TIME_FMT_STR) + '_' + basename
|
||||
filename = time.strftime(TIME_FMT_STR) + "_" + basename
|
||||
backup_file = os.path.join(save_dir, filename)
|
||||
try:
|
||||
if addon_prefs.save_images:
|
||||
save_images()
|
||||
if addon_prefs.save_sculpt:
|
||||
save_sculpt()
|
||||
bpy.ops.wm.save_as_mainfile(
|
||||
filepath=backup_file, copy=True, compress=addon_prefs.compress_files
|
||||
)
|
||||
print("Incremental Autosave: Saved file: ", backup_file)
|
||||
except:
|
||||
print('Incremental Autosave: Error auto saving file.')
|
||||
if addon_prefs.print_saves:
|
||||
print(f"Incremental Autosave: Saved file: {backup_file}")
|
||||
except PermissionError:
|
||||
print(f"Incremental Autosave: Error auto saving file: {backup_file}")
|
||||
|
||||
|
||||
def save_images():
|
||||
for img in bpy.data.images:
|
||||
if not img.is_dirty:
|
||||
continue
|
||||
try:
|
||||
if img.source == "GENERATED" or img.packed_file:
|
||||
img.pack()
|
||||
else:
|
||||
img.save()
|
||||
except Exception:
|
||||
# Should never happen, but I don't trust this API.
|
||||
# https://projects.blender.org/blender/blender/issues/152638
|
||||
print(f"Incremental Autosave: Failed to save dirty image: {img.name}")
|
||||
|
||||
|
||||
def save_sculpt():
|
||||
"""We just need to enter object mode and then go back to the original mode."""
|
||||
context = bpy.context
|
||||
if context.mode == "EDIT_MESH":
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
if context.mode == "SCULPT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.mode_set(mode="SCULPT")
|
||||
|
||||
|
||||
@persistent
|
||||
@@ -327,20 +427,43 @@ def save_before_close(_dummy=None):
|
||||
save_file()
|
||||
|
||||
|
||||
def create_autosave():
|
||||
def create_autosave_on_timer():
|
||||
now = datetime.now()
|
||||
delta = now - LAUNCH_TIME
|
||||
prefs = get_addon_prefs()
|
||||
if delta.seconds < 5:
|
||||
return get_addon_prefs().save_interval * 60
|
||||
return prefs.save_interval * 60
|
||||
|
||||
if bpy.data.is_dirty:
|
||||
save_file()
|
||||
return get_addon_prefs().save_interval * 60
|
||||
bpy.app.timers.register(save_after_modal_operation)
|
||||
return prefs.save_interval * 60
|
||||
|
||||
|
||||
def save_after_modal_operation():
|
||||
"""Mesh edits, sculpt strokes, and texture paint strokes trigger a depsgraph update,
|
||||
so it's better to make a save after one of those operations, rather than on a timer.
|
||||
"""
|
||||
|
||||
context = bpy.context
|
||||
|
||||
for modal in context.window.modal_operators:
|
||||
# We probably don't want to save in the middle of a transform/sculpt operation.
|
||||
if any(
|
||||
modal.bl_idname.startswith(prefix)
|
||||
for prefix in [
|
||||
"TRANSFORM_OT_",
|
||||
"SCULPT_OT_",
|
||||
"PAINT_OT_",
|
||||
]
|
||||
):
|
||||
return 0.5
|
||||
|
||||
save_file()
|
||||
|
||||
|
||||
@persistent
|
||||
def register_autosave_timer(_dummy=None):
|
||||
bpy.app.timers.register(create_autosave)
|
||||
bpy.app.timers.register(create_autosave_on_timer)
|
||||
|
||||
|
||||
def register():
|
||||
@@ -348,16 +471,17 @@ def register():
|
||||
bpy.utils.register_class(AutoSavePath)
|
||||
bpy.utils.register_class(IncrementalAutoSavePreferences)
|
||||
IncrementalAutoSavePreferences.register_autoload_from_file()
|
||||
bpy.app.timers.register(create_autosave)
|
||||
bpy.app.timers.register(create_autosave_on_timer)
|
||||
bpy.app.handlers.load_pre.append(save_before_close)
|
||||
bpy.app.handlers.load_post.append(register_autosave_timer)
|
||||
|
||||
|
||||
def unregister():
|
||||
save_before_close()
|
||||
update_prefs_on_file()
|
||||
bpy.app.handlers.load_pre.remove(save_before_close)
|
||||
bpy.app.handlers.load_post.remove(register_autosave_timer)
|
||||
bpy.app.timers.unregister(create_autosave)
|
||||
bpy.app.timers.unregister(create_autosave_on_timer)
|
||||
bpy.utils.unregister_class(IncrementalAutoSavePreferences)
|
||||
bpy.utils.unregister_class(AutoSavePath)
|
||||
bpy.utils.unregister_class(INCSAVE_UL_file_paths)
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "incremental_auto_save"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
name = "Incremental Auto-Save"
|
||||
tagline = "Improvements to Blender's Autosave"
|
||||
maintainer = "Demeter Dzadik <demeter@blender.org>"
|
||||
type = "add-on"
|
||||
website = "https://projects.blender.org/Mets/CloudRig"
|
||||
website = "https://projects.blender.org/Mets/incremental-autosave"
|
||||
tags = ["System"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
|
||||
license = [
|
||||
"SPDX:GPL-3.0-or-later",
|
||||
"SPDX:GPL-3.0-or-later"
|
||||
]
|
||||
copyright = [
|
||||
"2019-2024 Demeter Dzadik",
|
||||
"2019-2024 Demeter Dzadik"
|
||||
]
|
||||
[permissions]
|
||||
files = "Save preferences & .blends in chosen directories"
|
||||
|
||||
Reference in New Issue
Block a user