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