update addons (for 5.2)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
### Incremental Auto-Save
|
||||
|
||||
[Incremental Auto-Save](https://extensions.blender.org/add-ons/incremental-auto-save/) makes Blender's autosaves more configurable and reliable.
|
||||
|
||||

|
||||
@@ -13,4 +15,4 @@
|
||||
- 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.
|
||||
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,10 +1,13 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
from bl_ui.generic_ui_list import draw_ui_list
|
||||
@@ -15,15 +18,23 @@ from bpy.props import (
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import AddonPreferences, PropertyGroup, UIList
|
||||
from bpy.types import (
|
||||
AddonPreferences,
|
||||
Context,
|
||||
PropertyGroup,
|
||||
UILayout,
|
||||
UIList,
|
||||
bpy_prop_collection,
|
||||
)
|
||||
from rna_prop_ui import IDPropertyGroup
|
||||
|
||||
|
||||
# The add-on is confirmed to work with Blender 3.5 in 2026.
|
||||
# As long as that remains true, there's no point removing this bl_info block.
|
||||
bl_info = {
|
||||
"name": "Incremental Autosave",
|
||||
"author": "Demeter Dzadik",
|
||||
"version": (1, 1, 1),
|
||||
"blender": (2, 90, 0),
|
||||
"version": (1, 1, 2),
|
||||
"blender": (3, 5, 0),
|
||||
"location": "blender",
|
||||
"description": "Autosaves in a way where subsequent autosaves don't overwrite previous ones",
|
||||
"category": "System",
|
||||
@@ -38,7 +49,14 @@ LAUNCH_TIME = datetime.now()
|
||||
|
||||
class INCSAVE_UL_file_paths(UIList):
|
||||
def draw_item(
|
||||
self, context, layout, data, item, icon_value, active_data, active_propname
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
item: AutoSavePath,
|
||||
_icon_value: int,
|
||||
_active_data: object,
|
||||
_active_propname: str,
|
||||
):
|
||||
filepath: AutoSavePath = item
|
||||
|
||||
@@ -51,12 +69,14 @@ class INCSAVE_UL_file_paths(UIList):
|
||||
row.prop(filepath, "path", text="")
|
||||
|
||||
|
||||
def get_addon_prefs(context=None):
|
||||
def get_addon_prefs(
|
||||
context: Context | None = None,
|
||||
) -> IncrementalAutoSavePreferences:
|
||||
context = context or bpy.context
|
||||
return context.preferences.addons[__name__].preferences
|
||||
|
||||
|
||||
def update_prefs_on_file(self=None, context=None):
|
||||
def update_prefs_on_file(_self=None, context: Context | None = None):
|
||||
prefs = get_addon_prefs(context)
|
||||
if not type(prefs).loading:
|
||||
prefs.save_prefs_to_file()
|
||||
@@ -71,7 +91,7 @@ class PrefsFileSaveLoadMixin:
|
||||
import bpy, json
|
||||
from pathlib import Path
|
||||
|
||||
class MyAddonPrefs(PrefsFileSaveLoadMixin, bpy.types.AddonPreferences):
|
||||
class MyAddonPrefs(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
some_prop: bpy.props.IntProperty(update=update_prefs_on_file)
|
||||
|
||||
def register():
|
||||
@@ -91,7 +111,7 @@ class PrefsFileSaveLoadMixin:
|
||||
|
||||
@staticmethod
|
||||
def register_autoload_from_file(delay=0.1):
|
||||
def timer_func(_scene=None):
|
||||
def timer_func(_scene: object = None):
|
||||
prefs = get_addon_prefs()
|
||||
prefs.load_prefs_from_file()
|
||||
|
||||
@@ -105,7 +125,7 @@ class PrefsFileSaveLoadMixin:
|
||||
ret = {}
|
||||
|
||||
rna_class = None
|
||||
if isinstance(propgroup, bpy.types.AddonPreferences):
|
||||
if isinstance(propgroup, AddonPreferences):
|
||||
prop_dict = {
|
||||
key: getattr(propgroup, key)
|
||||
for key in propgroup.bl_rna.properties.keys()
|
||||
@@ -113,9 +133,7 @@ class PrefsFileSaveLoadMixin:
|
||||
}
|
||||
else:
|
||||
property_group_class_name = type(propgroup).__name__
|
||||
rna_class = bpy.types.PropertyGroup.bl_rna_get_subclass_py(
|
||||
property_group_class_name
|
||||
)
|
||||
rna_class = PropertyGroup.bl_rna_get_subclass_py(property_group_class_name)
|
||||
if not hasattr(rna_class, "properties"):
|
||||
rna_class = None
|
||||
prop_dict = {
|
||||
@@ -127,7 +145,7 @@ class PrefsFileSaveLoadMixin:
|
||||
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):
|
||||
if type(value) is list or isinstance(value, bpy_prop_collection):
|
||||
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)
|
||||
@@ -143,7 +161,11 @@ class PrefsFileSaveLoadMixin:
|
||||
ret[key] = value
|
||||
return ret
|
||||
|
||||
def apply_prefs_from_dict_recursive(self, propgroup, data):
|
||||
def apply_prefs_from_dict_recursive(
|
||||
self,
|
||||
propgroup: IDPropertyGroup,
|
||||
data: dict[str, Any],
|
||||
):
|
||||
for key, value in data.items():
|
||||
if not hasattr(propgroup, key):
|
||||
# Property got removed or renamed in the implementation.
|
||||
@@ -165,7 +187,7 @@ class PrefsFileSaveLoadMixin:
|
||||
addon_name = __package__.split(".")[-1]
|
||||
return Path(bpy.utils.user_resource("CONFIG")) / Path(addon_name + ".txt")
|
||||
|
||||
def save_prefs_to_file(self, _context=None):
|
||||
def save_prefs_to_file(self, _context: Context | None = None):
|
||||
data_dict = self.prefs_to_dict_recursive(propgroup=self)
|
||||
|
||||
with open(self.get_prefs_filepath(), "w") as f:
|
||||
@@ -190,7 +212,11 @@ class PrefsFileSaveLoadMixin:
|
||||
|
||||
|
||||
class AutoSavePath(PropertyGroup):
|
||||
name: StringProperty(update=update_prefs_on_file)
|
||||
name: StringProperty(
|
||||
name="Autosave Path Identifier",
|
||||
description="Name of this autosave path",
|
||||
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",
|
||||
@@ -221,7 +247,7 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
|
||||
max_save_files: bpy.props.IntProperty(
|
||||
max_save_files: 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",
|
||||
default=10,
|
||||
@@ -229,7 +255,7 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
max=100,
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
compress_files: bpy.props.BoolProperty(
|
||||
compress_files: BoolProperty(
|
||||
name="Compress Files",
|
||||
description="Save backups with compression enabled",
|
||||
default=True,
|
||||
@@ -263,7 +289,7 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
assert bpy.data.filepath
|
||||
return os.sep.join((os.path.dirname(bpy.data.filepath), "Autosaves"))
|
||||
|
||||
def get_valid_autosave_path(self, context) -> str:
|
||||
def get_valid_autosave_path(self, context: 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:
|
||||
@@ -287,12 +313,19 @@ class IncrementalAutoSavePreferences(PrefsFileSaveLoadMixin, AddonPreferences):
|
||||
sys_temp = tempfile.gettempdir()
|
||||
return sys_temp
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout.column()
|
||||
layout.use_property_decorate = False
|
||||
layout.use_property_split = True
|
||||
|
||||
header, panel = layout.panel("Incremental Autosave: Paths", default_closed=True)
|
||||
if hasattr(layout, "panel"):
|
||||
header, panel = layout.panel(
|
||||
"Incremental Autosave: Paths", default_closed=True
|
||||
)
|
||||
else:
|
||||
header = layout.row()
|
||||
panel = layout
|
||||
|
||||
header.label(text="Autosave Paths")
|
||||
if panel:
|
||||
split = panel.split(factor=0.4)
|
||||
@@ -384,7 +417,7 @@ def save_file():
|
||||
if addon_prefs.save_images:
|
||||
save_images()
|
||||
if addon_prefs.save_sculpt:
|
||||
save_sculpt()
|
||||
realize_sculpt_or_edit_mesh()
|
||||
bpy.ops.wm.save_as_mainfile(
|
||||
filepath=backup_file, copy=True, compress=addon_prefs.compress_files
|
||||
)
|
||||
@@ -409,25 +442,36 @@ def save_images():
|
||||
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."""
|
||||
def realize_sculpt_or_edit_mesh():
|
||||
"""We just need to enter object mode and then go back to the original mode,
|
||||
to write the mesh data to the object.
|
||||
"""
|
||||
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")
|
||||
try:
|
||||
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")
|
||||
except RuntimeError:
|
||||
# This can happen if we happen to catch user during some modal operator that's
|
||||
# not accounted for by save_after_modal_operation().
|
||||
print(
|
||||
"Incremental Autosave Error: Mesh could not be written to Object due to running modal operators: ",
|
||||
[op.bl_idname for op in context.window.modal_operators],
|
||||
)
|
||||
print("You could report this as a bug!")
|
||||
|
||||
|
||||
@persistent
|
||||
def save_before_close(_dummy=None):
|
||||
def save_before_close(_dummy: object = None):
|
||||
# is_dirty means there are changes that haven't been saved to disk
|
||||
if bpy.data.is_dirty and get_addon_prefs().save_before_close:
|
||||
save_file()
|
||||
|
||||
|
||||
def create_autosave_on_timer():
|
||||
def create_autosave_on_timer() -> int:
|
||||
now = datetime.now()
|
||||
delta = now - LAUNCH_TIME
|
||||
prefs = get_addon_prefs()
|
||||
@@ -439,7 +483,7 @@ def create_autosave_on_timer():
|
||||
return prefs.save_interval * 60
|
||||
|
||||
|
||||
def save_after_modal_operation():
|
||||
def save_after_modal_operation() -> float | None:
|
||||
"""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.
|
||||
"""
|
||||
@@ -462,7 +506,7 @@ def save_after_modal_operation():
|
||||
|
||||
|
||||
@persistent
|
||||
def register_autosave_timer(_dummy=None):
|
||||
def register_autosave_timer(_dummy: object = None):
|
||||
bpy.app.timers.register(create_autosave_on_timer)
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "incremental_auto_save"
|
||||
version = "1.1.1"
|
||||
version = "1.1.2"
|
||||
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/incremental-autosave"
|
||||
website = "https://projects.blender.org/Mets/incremental-autosave#incremental-auto-save"
|
||||
tags = ["System"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
@@ -15,7 +15,7 @@ license = [
|
||||
"SPDX:GPL-3.0-or-later"
|
||||
]
|
||||
copyright = [
|
||||
"2019-2024 Demeter Dzadik"
|
||||
"2019-2026 Demeter Dzadik"
|
||||
]
|
||||
[permissions]
|
||||
files = "Save preferences & .blends in chosen directories"
|
||||
|
||||
Reference in New Issue
Block a user