presets
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,7 @@ try:
|
|||||||
from .preset_ops import (
|
from .preset_ops import (
|
||||||
TEXT_TEXTURE_OT_save_preset,
|
TEXT_TEXTURE_OT_save_preset,
|
||||||
TEXT_TEXTURE_OT_save_preset_enter,
|
TEXT_TEXTURE_OT_save_preset_enter,
|
||||||
|
TEXT_TEXTURE_OT_clear_preset_search,
|
||||||
TEXT_TEXTURE_OT_load_preset,
|
TEXT_TEXTURE_OT_load_preset,
|
||||||
TEXT_TEXTURE_OT_delete_preset,
|
TEXT_TEXTURE_OT_delete_preset,
|
||||||
TEXT_TEXTURE_OT_refresh_presets,
|
TEXT_TEXTURE_OT_refresh_presets,
|
||||||
@@ -31,6 +32,7 @@ try:
|
|||||||
PRESET_OPERATORS = [
|
PRESET_OPERATORS = [
|
||||||
'TEXT_TEXTURE_OT_save_preset',
|
'TEXT_TEXTURE_OT_save_preset',
|
||||||
'TEXT_TEXTURE_OT_save_preset_enter',
|
'TEXT_TEXTURE_OT_save_preset_enter',
|
||||||
|
'TEXT_TEXTURE_OT_clear_preset_search',
|
||||||
'TEXT_TEXTURE_OT_load_preset',
|
'TEXT_TEXTURE_OT_load_preset',
|
||||||
'TEXT_TEXTURE_OT_delete_preset',
|
'TEXT_TEXTURE_OT_delete_preset',
|
||||||
'TEXT_TEXTURE_OT_refresh_presets',
|
'TEXT_TEXTURE_OT_refresh_presets',
|
||||||
@@ -108,4 +110,4 @@ __all__.extend(PRESET_OPERATORS)
|
|||||||
|
|
||||||
# Add text editor operators if available
|
# Add text editor operators if available
|
||||||
__all__.extend(TEXT_EDITOR_OPERATORS)
|
__all__.extend(TEXT_EDITOR_OPERATORS)
|
||||||
__all__.extend(UTILITY_OPERATORS)
|
__all__.extend(UTILITY_OPERATORS)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -168,15 +168,24 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
|
|||||||
existing_preset = preset
|
existing_preset = preset
|
||||||
break
|
break
|
||||||
|
|
||||||
|
target_index = None
|
||||||
|
|
||||||
if not existing_preset:
|
if not existing_preset:
|
||||||
pass
|
pass
|
||||||
preset = props.presets.add()
|
preset = props.presets.add()
|
||||||
preset.name = preset_name
|
preset.name = preset_name
|
||||||
preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE'
|
preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE'
|
||||||
|
target_index = len(props.presets) - 1
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
# Update source based on where it was saved
|
# Update source based on where it was saved
|
||||||
existing_preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE'
|
existing_preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE'
|
||||||
|
for idx, collection_preset in enumerate(props.presets):
|
||||||
|
pass
|
||||||
|
if collection_preset.name == preset_name:
|
||||||
|
pass
|
||||||
|
target_index = idx
|
||||||
|
break
|
||||||
|
|
||||||
# Provide clear feedback
|
# Provide clear feedback
|
||||||
location = "with .blend file" if props.save_with_blend_file else "to persistent storage"
|
location = "with .blend file" if props.save_with_blend_file else "to persistent storage"
|
||||||
@@ -188,6 +197,13 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
|
|||||||
pass
|
pass
|
||||||
self.report({'INFO'}, f"✅ Preset '{preset_name}' saved {location}")
|
self.report({'INFO'}, f"✅ Preset '{preset_name}' saved {location}")
|
||||||
|
|
||||||
|
if target_index is not None and hasattr(props, "active_preset_index"):
|
||||||
|
pass
|
||||||
|
props.active_preset_index = target_index
|
||||||
|
if hasattr(props, "last_loaded_preset"):
|
||||||
|
pass
|
||||||
|
props.last_loaded_preset = preset_name
|
||||||
|
|
||||||
except (OSError, IOError, json.JSONEncodeError) as e:
|
except (OSError, IOError, json.JSONEncodeError) as e:
|
||||||
pass
|
pass
|
||||||
self.report({'ERROR'}, f"Failed to save preset: {str(e)}")
|
self.report({'ERROR'}, f"Failed to save preset: {str(e)}")
|
||||||
@@ -234,6 +250,22 @@ class TEXT_TEXTURE_OT_save_preset_enter(Operator):
|
|||||||
# Delegate to the main save preset operator
|
# Delegate to the main save preset operator
|
||||||
return bpy.ops.text_texture.save_preset('INVOKE_DEFAULT')
|
return bpy.ops.text_texture.save_preset('INVOKE_DEFAULT')
|
||||||
|
|
||||||
|
class TEXT_TEXTURE_OT_clear_preset_search(Operator):
|
||||||
|
pass
|
||||||
|
"""Clear the preset search filter"""
|
||||||
|
bl_idname = "text_texture.clear_preset_search"
|
||||||
|
bl_label = "Clear Preset Search"
|
||||||
|
bl_description = "Reset the preset search field"
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
pass
|
||||||
|
props = context.scene.text_texture_props
|
||||||
|
if hasattr(props, "preset_search_query"):
|
||||||
|
pass
|
||||||
|
props.preset_search_query = ""
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
class TEXT_TEXTURE_OT_load_preset(Operator):
|
class TEXT_TEXTURE_OT_load_preset(Operator):
|
||||||
pass
|
pass
|
||||||
"""Load preset"""
|
"""Load preset"""
|
||||||
@@ -382,6 +414,18 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
|
|||||||
from ..core.generation_engine import generate_preview
|
from ..core.generation_engine import generate_preview
|
||||||
generate_preview(props, props.preview_size if hasattr(props, 'preview_size') else 512, props.preview_size if hasattr(props, 'preview_size') else 512)
|
generate_preview(props, props.preview_size if hasattr(props, 'preview_size') else 512, props.preview_size if hasattr(props, 'preview_size') else 512)
|
||||||
|
|
||||||
|
if hasattr(props, "last_loaded_preset"):
|
||||||
|
pass
|
||||||
|
props.last_loaded_preset = self.preset_name
|
||||||
|
if hasattr(props, "active_preset_index"):
|
||||||
|
pass
|
||||||
|
for idx, preset in enumerate(props.presets):
|
||||||
|
pass
|
||||||
|
if preset.name == self.preset_name:
|
||||||
|
pass
|
||||||
|
props.active_preset_index = idx
|
||||||
|
break
|
||||||
|
|
||||||
source_info = {
|
source_info = {
|
||||||
'BLEND_FILE': "from blend file",
|
'BLEND_FILE': "from blend file",
|
||||||
'PERSISTENT_FILE': "from persistent storage",
|
'PERSISTENT_FILE': "from persistent storage",
|
||||||
@@ -438,13 +482,27 @@ class TEXT_TEXTURE_OT_delete_preset(Operator):
|
|||||||
self.report({'WARNING'}, f"Preset '{self.preset_name}' not found in blend file or local storage")
|
self.report({'WARNING'}, f"Preset '{self.preset_name}' not found in blend file or local storage")
|
||||||
|
|
||||||
# Remove from UI list
|
# Remove from UI list
|
||||||
|
removed_index = None
|
||||||
for i, preset in enumerate(props.presets):
|
for i, preset in enumerate(props.presets):
|
||||||
pass
|
pass
|
||||||
if preset.name == self.preset_name:
|
if preset.name == self.preset_name:
|
||||||
pass
|
pass
|
||||||
props.presets.remove(i)
|
props.presets.remove(i)
|
||||||
|
removed_index = i
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if hasattr(props, "last_loaded_preset") and props.last_loaded_preset == self.preset_name:
|
||||||
|
pass
|
||||||
|
props.last_loaded_preset = ""
|
||||||
|
if removed_index is not None and hasattr(props, "active_preset_index"):
|
||||||
|
pass
|
||||||
|
if len(props.presets) == 0:
|
||||||
|
pass
|
||||||
|
props.active_preset_index = 0
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
props.active_preset_index = min(removed_index, len(props.presets) - 1)
|
||||||
|
|
||||||
delete_info = []
|
delete_info = []
|
||||||
if blend_deleted:
|
if blend_deleted:
|
||||||
pass
|
pass
|
||||||
@@ -809,6 +867,7 @@ class TEXT_TEXTURE_OT_import_presets(Operator):
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'TEXT_TEXTURE_OT_save_preset',
|
'TEXT_TEXTURE_OT_save_preset',
|
||||||
'TEXT_TEXTURE_OT_save_preset_enter',
|
'TEXT_TEXTURE_OT_save_preset_enter',
|
||||||
|
'TEXT_TEXTURE_OT_clear_preset_search',
|
||||||
'TEXT_TEXTURE_OT_load_preset',
|
'TEXT_TEXTURE_OT_load_preset',
|
||||||
'TEXT_TEXTURE_OT_delete_preset',
|
'TEXT_TEXTURE_OT_delete_preset',
|
||||||
'TEXT_TEXTURE_OT_refresh_presets',
|
'TEXT_TEXTURE_OT_refresh_presets',
|
||||||
|
|||||||
Binary file not shown.
@@ -606,6 +606,18 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
|
|||||||
description="Name for new preset (Press Enter to save)",
|
description="Name for new preset (Press Enter to save)",
|
||||||
default="New Preset"
|
default="New Preset"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
preset_search_query: StringProperty(
|
||||||
|
name="Preset Search",
|
||||||
|
description="Filter presets by name",
|
||||||
|
default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
last_loaded_preset: StringProperty(
|
||||||
|
name="Last Loaded Preset",
|
||||||
|
description="Tracks the most recently loaded preset name for highlighting",
|
||||||
|
default=""
|
||||||
|
)
|
||||||
|
|
||||||
# Property to track when user wants to save with Enter
|
# Property to track when user wants to save with Enter
|
||||||
preset_save_requested: BoolProperty(
|
preset_save_requested: BoolProperty(
|
||||||
|
|||||||
Binary file not shown.
118
src/ui/panels.py
118
src/ui/panels.py
@@ -498,78 +498,104 @@ def draw_presets_section(layout, props):
|
|||||||
draw_feature_lock_indicator(layout, 'presets')
|
draw_feature_lock_indicator(layout, 'presets')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Save preset section - use safe operator calls
|
col = layout.column(align=True)
|
||||||
save_row = layout.row(align=True)
|
|
||||||
|
# Save preset controls
|
||||||
|
save_row = col.row(align=True)
|
||||||
save_row.prop(props, "new_preset_name", text="", placeholder="Preset Name")
|
save_row.prop(props, "new_preset_name", text="", placeholder="Preset Name")
|
||||||
|
|
||||||
premium_lock = create_premium_lock_function()
|
premium_lock = create_premium_lock_function()
|
||||||
safe_operator_call(save_row, "text_texture.save_preset", premium_lock,
|
safe_operator_call(save_row, "text_texture.save_preset", premium_lock,
|
||||||
text="Save", icon='FILE_NEW')
|
text="Save", icon='FILE_NEW')
|
||||||
|
|
||||||
# Save with blend file checkbox
|
save_location_row = col.row(align=True)
|
||||||
layout.prop(props, "save_with_blend_file", text="Project-specific (only available in this .blend file)")
|
save_location_row.prop(props, "save_with_blend_file", text="Project-specific (saved inside this .blend)")
|
||||||
|
|
||||||
layout.separator()
|
col.separator()
|
||||||
|
|
||||||
# Import/Export and refresh buttons - use safe operator calls
|
|
||||||
backup_row = layout.row(align=True)
|
|
||||||
|
|
||||||
|
# Import/export/refresh controls
|
||||||
|
backup_row = col.row(align=True)
|
||||||
export_lock = create_premium_lock_function()
|
export_lock = create_premium_lock_function()
|
||||||
import_lock = create_premium_lock_function()
|
import_lock = create_premium_lock_function()
|
||||||
|
|
||||||
safe_operator_call(backup_row, "text_texture.export_presets", export_lock,
|
safe_operator_call(backup_row, "text_texture.export_presets", export_lock,
|
||||||
text="Export Backup", icon='EXPORT')
|
text="Export Backup", icon='EXPORT')
|
||||||
safe_operator_call(backup_row, "text_texture.import_presets", import_lock,
|
safe_operator_call(backup_row, "text_texture.import_presets", import_lock,
|
||||||
text="Import Backup", icon='IMPORT')
|
text="Import Backup", icon='IMPORT')
|
||||||
|
|
||||||
utility_row = layout.row(align=True)
|
utility_row = col.row(align=True)
|
||||||
|
|
||||||
refresh_lock = create_premium_lock_function()
|
refresh_lock = create_premium_lock_function()
|
||||||
|
|
||||||
safe_operator_call(utility_row, "text_texture.refresh_presets", refresh_lock,
|
safe_operator_call(utility_row, "text_texture.refresh_presets", refresh_lock,
|
||||||
text="Refresh", icon='FILE_REFRESH')
|
text="Refresh List", icon='FILE_REFRESH')
|
||||||
|
|
||||||
layout.separator()
|
col.separator()
|
||||||
|
|
||||||
# Load presets section - unified list
|
total_presets = len(props.presets)
|
||||||
if props.presets:
|
|
||||||
|
if total_presets:
|
||||||
pass
|
pass
|
||||||
layout.label(text="Presets", icon='PRESET')
|
blend_count = len([preset for preset in props.presets if preset.source == 'BLEND_FILE'])
|
||||||
|
persistent_count = total_presets - blend_count
|
||||||
|
|
||||||
# Sort presets alphabetically for consistent display
|
summary_row = col.row(align=True)
|
||||||
|
summary_row.label(text=f"{total_presets} presets", icon='PRESET_HLT')
|
||||||
|
summary_row.label(text=f"{blend_count} project", icon='FILE_BLEND')
|
||||||
|
summary_row.label(text=f"{persistent_count} global", icon='FILE_CACHE')
|
||||||
|
|
||||||
|
search_row = col.row(align=True)
|
||||||
|
search_row.prop(props, "preset_search_query", text="", icon='VIEWZOOM')
|
||||||
|
if getattr(props, "preset_search_query", "").strip():
|
||||||
|
search_row.operator("text_texture.clear_preset_search", text="", icon='PANEL_CLOSE')
|
||||||
|
|
||||||
|
# Prepare sorted and filtered presets for display
|
||||||
|
search_query = getattr(props, "preset_search_query", "").strip().lower()
|
||||||
sorted_presets = sorted(props.presets, key=lambda p: p.name.lower())
|
sorted_presets = sorted(props.presets, key=lambda p: p.name.lower())
|
||||||
|
if search_query:
|
||||||
|
visible_presets = [preset for preset in sorted_presets if search_query in preset.name.lower()]
|
||||||
|
else:
|
||||||
|
visible_presets = sorted_presets
|
||||||
|
|
||||||
for preset in sorted_presets:
|
if visible_presets:
|
||||||
pass
|
pass
|
||||||
row = layout.row(align=True)
|
for preset in visible_presets:
|
||||||
row.scale_y = 0.9
|
pass
|
||||||
|
row = col.row(align=True)
|
||||||
# Load button - use safe operator call
|
row.scale_y = 0.95
|
||||||
load_lock = create_premium_lock_function()
|
|
||||||
load_btn = safe_operator_call(row, "text_texture.load_preset", load_lock,
|
is_active = getattr(props, "last_loaded_preset", "") == preset.name
|
||||||
text=preset.name)
|
load_icon = 'CHECKMARK' if is_active else 'PRESET'
|
||||||
if load_btn:
|
|
||||||
load_btn.preset_name = preset.name
|
load_lock = create_premium_lock_function()
|
||||||
|
load_btn = safe_operator_call(row, "text_texture.load_preset", load_lock,
|
||||||
# Quick overwrite button - use safe operator call
|
text=preset.name, icon=load_icon)
|
||||||
overwrite_lock = create_premium_lock_function()
|
if load_btn:
|
||||||
overwrite_btn = safe_operator_call(row, "text_texture.save_preset", overwrite_lock,
|
load_btn.preset_name = preset.name
|
||||||
text="", icon='FILE_REFRESH')
|
|
||||||
if overwrite_btn:
|
source_icon = 'FILE_BLEND' if preset.source == 'BLEND_FILE' else 'FILE_CACHE'
|
||||||
overwrite_btn.overwrite = True
|
source_label = "Project" if preset.source == 'BLEND_FILE' else "Global"
|
||||||
overwrite_btn.preset_name = preset.name
|
row.label(text=source_label, icon=source_icon)
|
||||||
|
|
||||||
# Delete button - use safe operator call
|
actions = row.row(align=True)
|
||||||
delete_lock = create_premium_lock_function()
|
actions.alignment = 'RIGHT'
|
||||||
delete_btn = safe_operator_call(row, "text_texture.delete_preset", delete_lock,
|
|
||||||
text="", icon='TRASH')
|
overwrite_lock = create_premium_lock_function()
|
||||||
if delete_btn:
|
overwrite_btn = safe_operator_call(actions, "text_texture.save_preset", overwrite_lock,
|
||||||
delete_btn.preset_name = preset.name
|
text="", icon='FILE_REFRESH')
|
||||||
|
if overwrite_btn:
|
||||||
|
overwrite_btn.overwrite = True
|
||||||
|
overwrite_btn.preset_name = preset.name
|
||||||
|
|
||||||
|
delete_lock = create_premium_lock_function()
|
||||||
|
delete_btn = safe_operator_call(actions, "text_texture.delete_preset", delete_lock,
|
||||||
|
text="", icon='TRASH')
|
||||||
|
if delete_btn:
|
||||||
|
delete_btn.preset_name = preset.name
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
col.label(text="No presets match your search", icon='INFO')
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
layout.label(text="No presets yet")
|
col.label(text="No presets yet", icon='INFO')
|
||||||
layout.label(text="Save your settings above to create your first preset.")
|
col.label(text="Save your current settings to start your preset library.")
|
||||||
|
|
||||||
def draw_shader_section(layout, props):
|
def draw_shader_section(layout, props):
|
||||||
pass
|
pass
|
||||||
|
|||||||
Binary file not shown.
17
tests/unit/operators/test_preset_ui_controls.py
Normal file
17
tests/unit/operators/test_preset_ui_controls.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from src.operators.preset_ops import TEXT_TEXTURE_OT_clear_preset_search
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_preset_search_resets_query():
|
||||||
|
props = Mock()
|
||||||
|
props.preset_search_query = "Glow"
|
||||||
|
|
||||||
|
context = Mock()
|
||||||
|
context.scene.text_texture_props = props
|
||||||
|
|
||||||
|
op = TEXT_TEXTURE_OT_clear_preset_search()
|
||||||
|
result = op.execute(context)
|
||||||
|
|
||||||
|
assert result == {'FINISHED'}
|
||||||
|
assert props.preset_search_query == ""
|
||||||
1
tests/unit/presets/__init__.py
Normal file
1
tests/unit/presets/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
263
tests/unit/presets/test_storage_system.py
Normal file
263
tests/unit/presets/test_storage_system.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import src.presets.storage_system as storage_system
|
||||||
|
|
||||||
|
|
||||||
|
class MockPresetItem:
|
||||||
|
"""Simple container for preset collection entries."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
self.source = ""
|
||||||
|
|
||||||
|
|
||||||
|
class MockPresetCollection:
|
||||||
|
"""Minimal stand-in for Blender's CollectionProperty."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._items = []
|
||||||
|
self.cleared = False
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.cleared = True
|
||||||
|
self._items.clear()
|
||||||
|
|
||||||
|
def add(self):
|
||||||
|
item = MockPresetItem()
|
||||||
|
self._items.append(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def remove(self, index):
|
||||||
|
self._items.pop(index)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._items)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._items)
|
||||||
|
|
||||||
|
|
||||||
|
class MockProps:
|
||||||
|
"""Mock text texture properties containing preset collection."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.presets = MockPresetCollection()
|
||||||
|
self.save_with_blend_file = False
|
||||||
|
self.new_preset_name = ""
|
||||||
|
self.active_preset_index = 0
|
||||||
|
self.preset_search_query = ""
|
||||||
|
self.last_loaded_preset = ""
|
||||||
|
|
||||||
|
|
||||||
|
class MockScene(dict):
|
||||||
|
"""Dictionary-like scene object supporting custom properties."""
|
||||||
|
|
||||||
|
def __init__(self, props=None):
|
||||||
|
super().__init__()
|
||||||
|
self.text_texture_props = props
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
# Scene should be truthy even when no custom properties are set
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def make_scene(mock_bpy_fixture):
|
||||||
|
"""Factory for assigning a mock scene to bpy.context."""
|
||||||
|
|
||||||
|
def _make_scene(props=None):
|
||||||
|
scene = MockScene(props)
|
||||||
|
mock_bpy_fixture.context.scene = scene
|
||||||
|
return scene
|
||||||
|
|
||||||
|
return _make_scene
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_persistent_preset_path_creates_directory(tmp_path, mock_bpy_fixture):
|
||||||
|
config_dir = tmp_path / "config"
|
||||||
|
config_dir.mkdir()
|
||||||
|
mock_bpy_fixture.utils.user_resource.return_value = str(config_dir)
|
||||||
|
|
||||||
|
path = storage_system.get_persistent_preset_path()
|
||||||
|
expected = config_dir / "text_texture_generator" / "presets"
|
||||||
|
|
||||||
|
assert Path(path) == expected
|
||||||
|
assert expected.is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_preset_unified_persistent_writes_file(tmp_path, monkeypatch):
|
||||||
|
persistent_dir = tmp_path / "persistent"
|
||||||
|
persistent_dir.mkdir()
|
||||||
|
monkeypatch.setattr(storage_system, "get_persistent_preset_path", lambda: str(persistent_dir))
|
||||||
|
|
||||||
|
preset_data = {"text": "hello", "font_size": 24}
|
||||||
|
result = storage_system.save_preset_unified(preset_data, "SamplePreset", save_to_blend_file=False)
|
||||||
|
|
||||||
|
saved_path = persistent_dir / "SamplePreset.json"
|
||||||
|
assert result is True
|
||||||
|
assert saved_path.exists()
|
||||||
|
with saved_path.open("r") as handle:
|
||||||
|
assert json.load(handle) == preset_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_preset_unified_blend_delegates(monkeypatch):
|
||||||
|
mock_save = Mock(return_value=True)
|
||||||
|
monkeypatch.setattr(storage_system, "save_blend_file_preset", mock_save)
|
||||||
|
|
||||||
|
payload = {"text": "blend storage"}
|
||||||
|
result = storage_system.save_preset_unified(payload, "BlendPreset", save_to_blend_file=True)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
mock_save.assert_called_once_with("BlendPreset", payload)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_blend_file_presets_initializes_empty_collection(make_scene):
|
||||||
|
scene = make_scene()
|
||||||
|
|
||||||
|
presets = storage_system.get_blend_file_presets()
|
||||||
|
|
||||||
|
assert presets == {}
|
||||||
|
assert scene["text_texture_presets"] == "{}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_blend_file_preset_stores_json_in_scene(make_scene):
|
||||||
|
scene = make_scene()
|
||||||
|
preset_content = {"text": "stored", "font_size": 18}
|
||||||
|
|
||||||
|
result = storage_system.save_blend_file_preset("StoredPreset", preset_content)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
stored = json.loads(scene["text_texture_presets"])
|
||||||
|
assert stored["StoredPreset"] == preset_content
|
||||||
|
roundtrip = storage_system.get_blend_file_presets()
|
||||||
|
assert roundtrip["StoredPreset"] == preset_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_blend_file_preset_removes_entry(make_scene):
|
||||||
|
scene = make_scene()
|
||||||
|
storage_system.save_blend_file_preset("TempPreset", {"value": 1})
|
||||||
|
|
||||||
|
removed = storage_system.delete_blend_file_preset("TempPreset")
|
||||||
|
assert removed is True
|
||||||
|
assert json.loads(scene["text_texture_presets"]) == {}
|
||||||
|
|
||||||
|
removed_again = storage_system.delete_blend_file_preset("TempPreset")
|
||||||
|
assert removed_again is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_unified_preset_list_prioritizes_blend_presets(tmp_path, monkeypatch):
|
||||||
|
persistent_dir = tmp_path / "persistent"
|
||||||
|
persistent_dir.mkdir()
|
||||||
|
monkeypatch.setattr(storage_system, "get_persistent_preset_path", lambda: str(persistent_dir))
|
||||||
|
|
||||||
|
with (persistent_dir / "alpha.json").open("w") as handle:
|
||||||
|
json.dump({"source": "persistent"}, handle)
|
||||||
|
with (persistent_dir / "shared.json").open("w") as handle:
|
||||||
|
json.dump({"source": "persistent-shared"}, handle)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
storage_system,
|
||||||
|
"get_blend_file_presets",
|
||||||
|
lambda: {
|
||||||
|
"shared": {"source": "blend"},
|
||||||
|
"blend_only": {"source": "blend-only"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
presets = storage_system.get_unified_preset_list()
|
||||||
|
|
||||||
|
assert presets["alpha"][1] == "PERSISTENT_FILE"
|
||||||
|
assert presets["alpha"][0]["source"] == "persistent"
|
||||||
|
assert presets["shared"][1] == "BLEND_FILE"
|
||||||
|
assert presets["shared"][0]["source"] == "blend"
|
||||||
|
assert presets["blend_only"][1] == "BLEND_FILE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_from_legacy_system_migrates_files_and_creates_backup(tmp_path, monkeypatch):
|
||||||
|
legacy_dir = tmp_path / "legacy"
|
||||||
|
persistent_dir = tmp_path / "persistent"
|
||||||
|
legacy_dir.mkdir()
|
||||||
|
persistent_dir.mkdir()
|
||||||
|
|
||||||
|
legacy_payload = {"example": True}
|
||||||
|
with (legacy_dir / "legacy_preset.json").open("w") as handle:
|
||||||
|
json.dump(legacy_payload, handle)
|
||||||
|
with (persistent_dir / "existing.json").open("w") as handle:
|
||||||
|
json.dump({"existing": True}, handle)
|
||||||
|
|
||||||
|
monkeypatch.setattr(storage_system, "get_preset_path", lambda: str(legacy_dir))
|
||||||
|
monkeypatch.setattr(storage_system, "get_persistent_preset_path", lambda: str(persistent_dir))
|
||||||
|
|
||||||
|
result = storage_system.migrate_from_legacy_system()
|
||||||
|
|
||||||
|
migrated_file = persistent_dir / "legacy_preset.json"
|
||||||
|
backup_file = persistent_dir / "legacy_backup" / "legacy_preset.json"
|
||||||
|
marker_file = persistent_dir / ".migration_complete"
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert migrated_file.exists()
|
||||||
|
assert json.loads(migrated_file.read_text()) == legacy_payload
|
||||||
|
assert backup_file.exists()
|
||||||
|
assert marker_file.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_presets_populates_props_collection(make_scene, monkeypatch):
|
||||||
|
props = MockProps()
|
||||||
|
# Seed with existing entry to ensure clear() is invoked
|
||||||
|
props.presets.add().name = "stale"
|
||||||
|
migrate_mock = Mock(return_value=True)
|
||||||
|
monkeypatch.setattr(storage_system, "migrate_from_legacy_system", migrate_mock)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
storage_system,
|
||||||
|
"get_unified_preset_list",
|
||||||
|
lambda: {
|
||||||
|
"BlendPreset": ({"value": "blend"}, "BLEND_FILE"),
|
||||||
|
"PersistentPreset": ({"value": "persistent"}, "PERSISTENT_FILE"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
scene = make_scene(props)
|
||||||
|
storage_system.initialize_presets()
|
||||||
|
|
||||||
|
assert migrate_mock.called
|
||||||
|
assert props.presets.cleared is True
|
||||||
|
assert len(props.presets) == 2
|
||||||
|
entries = {(item.name, item.source) for item in props.presets}
|
||||||
|
assert ("BlendPreset", "BLEND_FILE") in entries
|
||||||
|
assert ("PersistentPreset", "PERSISTENT_FILE") in entries
|
||||||
|
assert "text_texture_presets" not in scene # ensure scene custom data untouched
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_presets_available_refreshes_when_empty(make_scene, monkeypatch):
|
||||||
|
props = MockProps()
|
||||||
|
make_scene(props)
|
||||||
|
refresh_mock = Mock(return_value=True)
|
||||||
|
monkeypatch.setattr(storage_system, "refresh_presets_sync", refresh_mock)
|
||||||
|
|
||||||
|
result = storage_system.ensure_presets_available()
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
refresh_mock.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_presets_available_returns_true_when_presets_loaded(make_scene, monkeypatch):
|
||||||
|
props = MockProps()
|
||||||
|
props.presets.add().name = "cached"
|
||||||
|
make_scene(props)
|
||||||
|
refresh_mock = Mock(return_value=True)
|
||||||
|
monkeypatch.setattr(storage_system, "refresh_presets_sync", refresh_mock)
|
||||||
|
|
||||||
|
result = storage_system.ensure_presets_available()
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
refresh_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_presets_sync_returns_false_without_scene(mock_bpy_fixture):
|
||||||
|
mock_bpy_fixture.context.scene = None
|
||||||
|
assert storage_system.refresh_presets_sync() is False
|
||||||
Reference in New Issue
Block a user