462 lines
21 KiB
Python
462 lines
21 KiB
Python
import bpy
|
|
import os
|
|
import json
|
|
import time
|
|
import platform
|
|
|
|
# ============================================================================
|
|
# PRESET STORAGE SYSTEM - V2.0 (PERSISTENT STORAGE)
|
|
# ============================================================================
|
|
#
|
|
# The Text Texture Generator now uses a THREE-TIER preset storage system
|
|
# that ensures preset persistence across addon updates:
|
|
#
|
|
# 1. BLEND FILE STORAGE (Highest Priority)
|
|
# - Presets travel with the .blend file
|
|
# - Perfect for project-specific presets
|
|
# - Stored in scene custom properties
|
|
#
|
|
# 2. PERSISTENT FILE STORAGE (Medium Priority)
|
|
# - Survives addon updates and Blender upgrades
|
|
# - Stored in Blender's user config directory
|
|
# - Shared across all blend files
|
|
#
|
|
# 3. LEGACY FILE STORAGE (Lowest Priority)
|
|
# - Old system for backward compatibility
|
|
# - Vulnerable to addon updates
|
|
# - Will be automatically migrated to persistent storage
|
|
#
|
|
# MIGRATION SYSTEM:
|
|
# - Automatically detects addon version changes
|
|
# - Migrates legacy presets to persistent storage
|
|
# - Creates backups before migration
|
|
# - Provides detailed migration reports
|
|
#
|
|
# IMPORT/EXPORT SYSTEM:
|
|
# - Export all presets for manual backup
|
|
# - Import presets with conflict resolution
|
|
# - Supports cross-machine preset sharing
|
|
#
|
|
# ============================================================================
|
|
|
|
def get_preset_path():
|
|
"""Get path for storing presets (legacy local storage)
|
|
|
|
LEGACY SYSTEM: This storage location is vulnerable to addon updates.
|
|
Presets stored here will be lost when the addon is updated through
|
|
Blender's Add-ons preferences. This function is maintained for
|
|
backward compatibility only.
|
|
|
|
For new installations, use get_persistent_preset_path() instead.
|
|
"""
|
|
addon_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
presets_dir = os.path.join(addon_dir, "presets")
|
|
if not os.path.exists(presets_dir):
|
|
try:
|
|
os.makedirs(presets_dir)
|
|
except OSError as e:
|
|
print(f"Error creating presets directory: {e}")
|
|
return presets_dir
|
|
|
|
def get_persistent_preset_path():
|
|
"""Get persistent path for storing presets (survives addon updates)"""
|
|
import bpy
|
|
|
|
# Get Blender's user config directory
|
|
config_dir = bpy.utils.user_resource('CONFIG')
|
|
if not config_dir:
|
|
# Fallback to addon directory if config path unavailable
|
|
print("[TTG] Warning: Could not get user config directory, falling back to addon directory")
|
|
return get_preset_path()
|
|
|
|
# Create addon-specific subdirectory
|
|
persistent_dir = os.path.join(config_dir, "text_texture_generator", "presets")
|
|
|
|
try:
|
|
if not os.path.exists(persistent_dir):
|
|
os.makedirs(persistent_dir, exist_ok=True)
|
|
return persistent_dir
|
|
except OSError as e:
|
|
print(f"[TTG] Error creating persistent presets directory: {e}")
|
|
# Fallback to addon directory
|
|
return get_preset_path()
|
|
|
|
def get_addon_version():
|
|
"""Get current addon version for migration tracking"""
|
|
# Import bl_info from the main addon file
|
|
import sys
|
|
import importlib.util
|
|
|
|
# Get the main addon module
|
|
main_module_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
init_file = os.path.join(main_module_path, "__init__.py")
|
|
|
|
try:
|
|
spec = importlib.util.spec_from_file_location("main_addon", init_file)
|
|
main_addon = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(main_addon)
|
|
return main_addon.bl_info["version"]
|
|
except Exception as e:
|
|
print(f"[TTG] Error getting addon version: {e}")
|
|
return (2, 3, 2) # Fallback version
|
|
|
|
def get_version_file_path():
|
|
"""Get path to version tracking file"""
|
|
persistent_dir = os.path.dirname(get_persistent_preset_path())
|
|
return os.path.join(persistent_dir, "addon_version.json")
|
|
|
|
def get_stored_version():
|
|
"""Get previously stored addon version"""
|
|
version_file = get_version_file_path()
|
|
try:
|
|
if os.path.exists(version_file):
|
|
with open(version_file, 'r') as f:
|
|
data = json.load(f)
|
|
return tuple(data.get('version', (0, 0, 0)))
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
print(f"[TTG] Error reading version file: {e}")
|
|
return (0, 0, 0)
|
|
|
|
def save_current_version():
|
|
"""Save current addon version to persistent storage"""
|
|
version_file = get_version_file_path()
|
|
try:
|
|
version_data = {
|
|
'version': get_addon_version(),
|
|
'timestamp': time.time()
|
|
}
|
|
with open(version_file, 'w') as f:
|
|
json.dump(version_data, f, indent=2)
|
|
print(f"[TTG] Saved addon version {get_addon_version()} to persistent storage")
|
|
except (OSError, json.JSONEncodeError) as e:
|
|
print(f"[TTG] Error saving version file: {e}")
|
|
|
|
def get_blend_file_presets():
|
|
"""Get presets stored in the current blend file"""
|
|
try:
|
|
# Get custom properties from the scene
|
|
import bpy
|
|
scene = bpy.context.scene
|
|
|
|
# DETAILED LOGGING: Function entry
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] ==================== LOADING FROM BLEND FILE ==================")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Function called to get presets from blend file")
|
|
|
|
# Store presets in scene custom properties
|
|
if "text_texture_presets" not in scene:
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] No 'text_texture_presets' found in scene, creating empty")
|
|
scene["text_texture_presets"] = "{}"
|
|
else:
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Found 'text_texture_presets' in scene")
|
|
|
|
preset_data = scene.get("text_texture_presets", "{}")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data type: {type(preset_data)}")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data length: {len(str(preset_data))} characters")
|
|
|
|
if isinstance(preset_data, str):
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is string, attempting JSON parse...")
|
|
try:
|
|
import json
|
|
parsed_presets = json.loads(preset_data)
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Successfully parsed JSON")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets type: {type(parsed_presets)}")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets count: {len(parsed_presets) if isinstance(parsed_presets, dict) else 'N/A'}")
|
|
|
|
if isinstance(parsed_presets, dict):
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset names: {list(parsed_presets.keys())}")
|
|
|
|
# DETAILED VERIFICATION: Check shader properties in each preset
|
|
for preset_name, preset_content in parsed_presets.items():
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset '{preset_name}' analysis:")
|
|
if isinstance(preset_content, dict):
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Keys: {list(preset_content.keys())}")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_shadows: {preset_content.get('disable_shadows', 'NOT FOUND')}")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_reflections: {preset_content.get('disable_reflections', 'NOT FOUND')}")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_backfacing: {preset_content.get('disable_backfacing', 'NOT FOUND')}")
|
|
else:
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] ERROR: Preset content is not a dict: {type(preset_content)}")
|
|
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Returning parsed presets")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================")
|
|
return parsed_presets
|
|
|
|
except json.JSONDecodeError as json_err:
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] JSON decode error: {json_err}")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw data causing error: {preset_data[:200]}...") # First 200 chars
|
|
print("Error parsing blend file presets, resetting to empty")
|
|
scene["text_texture_presets"] = "{}"
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Reset to empty, returning empty dict")
|
|
return {}
|
|
else:
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is not string, returning empty dict")
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================")
|
|
return {}
|
|
except Exception as e:
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] EXCEPTION occurred: {e}")
|
|
import traceback
|
|
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Full traceback: {traceback.format_exc()}")
|
|
print(f"Error getting blend file presets: {e}")
|
|
return {}
|
|
|
|
def save_blend_file_preset(preset_name, preset_data):
|
|
"""Save a preset to the current blend file"""
|
|
try:
|
|
import bpy
|
|
import json
|
|
scene = bpy.context.scene
|
|
|
|
# DETAILED LOGGING: Function entry
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ==================== SAVING TO BLEND FILE ===================")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Function called with preset_name: '{preset_name}'")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data type: {type(preset_data)}")
|
|
if isinstance(preset_data, dict):
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data keys: {list(preset_data.keys())}")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Shader properties in preset_data:")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
|
|
else:
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: preset_data is not a dictionary!")
|
|
|
|
# Get existing presets
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Getting existing presets from blend file...")
|
|
presets = get_blend_file_presets()
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing presets count: {len(presets)}")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing preset names: {list(presets.keys())}")
|
|
|
|
# Add or update the preset
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Adding/updating preset '{preset_name}'...")
|
|
presets[preset_name] = preset_data
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Presets now contains {len(presets)} items")
|
|
|
|
# VERIFY: Check that shader properties are in the preset we're about to save
|
|
if preset_name in presets and isinstance(presets[preset_name], dict):
|
|
saved_preset = presets[preset_name]
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] VERIFICATION - Shader properties in final preset:")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {saved_preset.get('disable_shadows', 'NOT FOUND')}")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {saved_preset.get('disable_reflections', 'NOT FOUND')}")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {saved_preset.get('disable_backfacing', 'NOT FOUND')}")
|
|
|
|
# Save back to scene custom properties
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Converting presets to JSON...")
|
|
json_data = json.dumps(presets, indent=2)
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] JSON data length: {len(json_data)} characters")
|
|
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Saving to scene custom properties...")
|
|
scene["text_texture_presets"] = json_data
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Successfully saved to scene['text_texture_presets']")
|
|
|
|
# FINAL VERIFICATION: Read back from scene to confirm save
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] FINAL VERIFICATION - Reading back from scene...")
|
|
readback_data = scene.get("text_texture_presets", "{}")
|
|
if readback_data:
|
|
try:
|
|
readback_presets = json.loads(readback_data)
|
|
if preset_name in readback_presets:
|
|
readback_preset = readback_presets[preset_name]
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] READBACK VERIFICATION - Shader properties:")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {readback_preset.get('disable_shadows', 'NOT FOUND')}")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {readback_preset.get('disable_reflections', 'NOT FOUND')}")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {readback_preset.get('disable_backfacing', 'NOT FOUND')}")
|
|
else:
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Preset '{preset_name}' not found in readback data!")
|
|
except json.JSONDecodeError as json_err:
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Could not parse readback JSON: {json_err}")
|
|
else:
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: No data in scene['text_texture_presets'] after save!")
|
|
|
|
print(f"Saved preset '{preset_name}' to blend file")
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ================================================================")
|
|
return True
|
|
except Exception as e:
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] EXCEPTION occurred: {e}")
|
|
import traceback
|
|
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Full traceback: {traceback.format_exc()}")
|
|
print(f"Error saving preset to blend file: {e}")
|
|
return False
|
|
|
|
def delete_blend_file_preset(preset_name):
|
|
"""Delete a preset from the current blend file"""
|
|
try:
|
|
import bpy
|
|
import json
|
|
scene = bpy.context.scene
|
|
|
|
# Get existing presets
|
|
presets = get_blend_file_presets()
|
|
|
|
# Remove the preset if it exists
|
|
if preset_name in presets:
|
|
del presets[preset_name]
|
|
scene["text_texture_presets"] = json.dumps(presets, indent=2)
|
|
print(f"Deleted preset '{preset_name}' from blend file")
|
|
return True
|
|
else:
|
|
print(f"Preset '{preset_name}' not found in blend file")
|
|
return False
|
|
except Exception as e:
|
|
print(f"Error deleting preset from blend file: {e}")
|
|
return False
|
|
|
|
def initialize_presets():
|
|
"""Initialize presets with simplified, reliable synchronization"""
|
|
try:
|
|
import bpy
|
|
if not hasattr(bpy.context, 'scene') or not bpy.context.scene:
|
|
return
|
|
|
|
props = bpy.context.scene.text_texture_props
|
|
if not props:
|
|
return
|
|
|
|
print(f"[TTG] Starting preset initialization...")
|
|
|
|
# Clear existing presets to avoid duplicates
|
|
props.presets.clear()
|
|
|
|
# Collect all presets from all sources
|
|
all_presets = {}
|
|
|
|
# 1. Load from blend file (highest priority - travels with file)
|
|
try:
|
|
blend_presets = get_blend_file_presets()
|
|
for name, data in blend_presets.items():
|
|
all_presets[name] = {
|
|
'data': data,
|
|
'source': 'BLEND_FILE'
|
|
}
|
|
print(f"[TTG] Found {len(blend_presets)} presets in blend file")
|
|
except Exception as e:
|
|
print(f"[TTG] Error loading blend file presets: {e}")
|
|
|
|
# 2. Load from persistent storage (survives addon updates)
|
|
try:
|
|
persistent_dir = get_persistent_preset_path()
|
|
if os.path.exists(persistent_dir):
|
|
for filename in os.listdir(persistent_dir):
|
|
if filename.endswith('.json'):
|
|
preset_name = os.path.splitext(filename)[0]
|
|
# Only add if not already in blend file (blend file has priority)
|
|
if preset_name not in all_presets:
|
|
try:
|
|
with open(os.path.join(persistent_dir, filename), 'r') as f:
|
|
preset_data = json.load(f)
|
|
all_presets[preset_name] = {
|
|
'data': preset_data,
|
|
'source': 'PERSISTENT_FILE'
|
|
}
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
print(f"[TTG] Error reading persistent preset {preset_name}: {e}")
|
|
print(f"[TTG] Found {len([p for p in all_presets.values() if p['source'] == 'PERSISTENT_FILE'])} persistent presets")
|
|
except Exception as e:
|
|
print(f"[TTG] Error loading persistent presets: {e}")
|
|
|
|
# 3. Load from legacy storage (backward compatibility)
|
|
try:
|
|
legacy_dir = get_preset_path()
|
|
if os.path.exists(legacy_dir) and legacy_dir != get_persistent_preset_path():
|
|
for filename in os.listdir(legacy_dir):
|
|
if filename.endswith('.json'):
|
|
preset_name = os.path.splitext(filename)[0]
|
|
# Only add if not already found in higher priority sources
|
|
if preset_name not in all_presets:
|
|
try:
|
|
with open(os.path.join(legacy_dir, filename), 'r') as f:
|
|
preset_data = json.load(f)
|
|
all_presets[preset_name] = {
|
|
'data': preset_data,
|
|
'source': 'LOCAL_FILE'
|
|
}
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
print(f"[TTG] Error reading legacy preset {preset_name}: {e}")
|
|
print(f"[TTG] Found {len([p for p in all_presets.values() if p['source'] == 'LOCAL_FILE'])} legacy presets")
|
|
except Exception as e:
|
|
print(f"[TTG] Error loading legacy presets: {e}")
|
|
|
|
# Add all presets to UI list
|
|
for preset_name, preset_info in all_presets.items():
|
|
preset = props.presets.add()
|
|
preset.name = preset_name
|
|
preset.source = preset_info['source']
|
|
|
|
# Report results
|
|
blend_count = len([p for p in props.presets if p.source == 'BLEND_FILE'])
|
|
persistent_count = len([p for p in props.presets if p.source == 'PERSISTENT_FILE'])
|
|
legacy_count = len([p for p in props.presets if p.source == 'LOCAL_FILE'])
|
|
|
|
print(f"[TTG] ✅ Initialized {len(props.presets)} presets ({blend_count} blend file, {persistent_count} persistent, {legacy_count} legacy)")
|
|
|
|
except Exception as e:
|
|
print(f"[TTG] Critical error in initialize_presets: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
def refresh_presets():
|
|
"""Refresh the preset list by rescanning files (legacy function)"""
|
|
import bpy
|
|
refresh_presets_sync()
|
|
|
|
# Force UI update
|
|
try:
|
|
if bpy.context.area:
|
|
bpy.context.area.tag_redraw()
|
|
except:
|
|
pass
|
|
|
|
def refresh_presets_sync():
|
|
"""Centralized, synchronous preset refresh - guarantees completion"""
|
|
try:
|
|
import bpy
|
|
|
|
print("[TTG] 🔄 Synchronizing presets...")
|
|
|
|
# Ensure we have valid context
|
|
if not hasattr(bpy.context, 'scene') or not bpy.context.scene:
|
|
print("[TTG] ⚠️ No valid scene context for preset refresh")
|
|
return False
|
|
|
|
props = bpy.context.scene.text_texture_props
|
|
if not props:
|
|
print("[TTG] ⚠️ No text_texture_props found")
|
|
return False
|
|
|
|
# Run the simplified initialization
|
|
initialize_presets()
|
|
|
|
# Validate that presets are actually loaded
|
|
preset_count = len(props.presets)
|
|
if preset_count == 0:
|
|
print("[TTG] ⚠️ Warning: No presets found after refresh")
|
|
else:
|
|
print(f"[TTG] ✅ Preset sync complete - {preset_count} presets available")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"[TTG] ❌ Error in refresh_presets_sync: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
def ensure_presets_available():
|
|
"""Defensive programming - ensure presets are loaded before UI operations"""
|
|
try:
|
|
import bpy
|
|
|
|
if not hasattr(bpy.context, 'scene') or not bpy.context.scene:
|
|
return False
|
|
|
|
props = bpy.context.scene.text_texture_props
|
|
if not props:
|
|
return False
|
|
|
|
# If no presets loaded, try to load them
|
|
if len(props.presets) == 0:
|
|
print("[TTG] 🛡️ No presets available - attempting to load...")
|
|
return refresh_presets_sync()
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"[TTG] Error in ensure_presets_available: {e}")
|
|
return False |