diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..7b42e9a --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,105 @@ +# Deployment Guide + +Quick instructions for deploying a new version of the Text Texture Generator addon. + +## Prerequisites + +- Git repository with proper tags +- Python 3.10+ +- All changes committed and pushed to main branch + +## Deployment Steps + +### 1. Create Version Tag + +```bash +# Create and push a version tag +git tag v1.1.0 +git push origin v1.1.0 +``` + +### 2. Generate Changelog (Optional) + +```bash +# Generate changelog for the new version +python3 build/generate_changelog.py --version 1.1.0 --output CHANGELOG_v1.1.0.md + +# Or generate full changelog +python3 build/generate_changelog.py --full --output CHANGELOG.md +``` + +### 3. Build Packages + +```bash +# Build both versions (recommended) +python3 build/build_addon.py --type all --version 1.1.0 + +# Or build individually: +# python3 build/build_addon.py --type full --version 1.1.0 +# python3 build/build_addon.py --type free --version 1.1.0 +``` + +### 4. Validate Packages + +```bash +# Validate the generated packages +python3 build/validate_package.py dist/text_texture_generator_v1.1.0.zip --type full +python3 build/validate_package.py dist/text_texture_generator_v1.1.0_free.zip --type free +``` + +### 5. Deploy + +Upload the validated packages from the `dist/` directory: + +- `text_texture_generator_v1.1.0.zip` (Full version) +- `text_texture_generator_v1.1.0_free.zip` (Free version) + +## Build System Overview + +The deployment uses a sophisticated build-time file exclusion system: + +- **Version Sync**: [`sync_version.py`](build/sync_version.py) automatically updates version information from git tags +- **Package Build**: [`build_addon.py`](build/build_addon.py) creates separate packages with different feature sets +- **Validation**: [`validate_package.py`](build/validate_package.py) ensures package integrity +- **Changelog**: [`generate_changelog.py`](build/generate_changelog.py) creates release notes from git history + +## Configuration Files + +- [`build/config.json`](build/config.json) - Project metadata and build settings +- [`build/file_exclusions.json`](build/file_exclusions.json) - Defines which files are excluded from free builds + +## Troubleshooting + +### No Git Tags Found + +If version sync fails, ensure you have created and pushed the version tag: + +```bash +git tag -l # List existing tags +git tag v1.1.0 # Create new tag +git push origin v1.1.0 # Push to remote +``` + +### Build Validation Fails + +Check the validation output for specific errors: + +```bash +python3 build/validate_package.py dist/your_package.zip --type full +``` + +### Version Mismatch + +The build system automatically syncs versions from git tags. If you need to override: + +```bash +python3 build/sync_version.py --version 1.1.0 --type full +``` + +## Quick Deploy Command + +For experienced users, deploy in one command: + +```bash +git tag v1.1.0 && git push origin v1.1.0 && python3 build/build_addon.py --type all --version 1.1.0 && python3 build/validate_package.py dist/text_texture_generator_v1.1.0.zip && python3 build/validate_package.py dist/text_texture_generator_v1.1.0_free.zip +``` diff --git a/README.md b/README.md index 0bc1039..9a862bd 100644 --- a/README.md +++ b/README.md @@ -57,12 +57,12 @@ A modern Blender addon that generates image textures from text with extensive cu - **Preset Management System**: - - Three-tier storage: Blend File, Persistent, Local presets - - All preset types now display properly in the interface - - Presets travel with .blend files (Blend File type) + - Simplified two-tier storage: Persistent File (default) + Blend File (optional) + - Unified preset list with clear storage indicators (🏠/📄) + - Presets travel with .blend files when "Save with .blend file" is checked - Import/export functionality for backup and sharing - - Automatic migration system for addon updates - - Conflict resolution for preset imports + - Automatic migration from legacy three-tier system + - Conflict resolution with blend file presets taking priority - **Preview System**: @@ -254,33 +254,39 @@ The addon automatically installs required dependencies: ## Preset System -The addon features a sophisticated three-tier preset system: +The addon features a streamlined two-tier preset system designed for simplicity and reliability: ### Storage Locations (Priority Order) -1. **Blend File Storage** (Highest Priority) +1. **Blend File Storage** (Higher Priority) - Presets stored in scene custom properties - - Travel with your .blend file + - Travel with your .blend file automatically - Perfect for project-specific presets + - Display with 📄 indicator in UI -2. **Persistent File Storage** (Medium Priority) +2. **Persistent File Storage** (Default Storage) - Survives addon updates and Blender upgrades - - Stored in Blender's user config directory + - Stored in Blender's user config directory (`~/.config/blender/text_texture_generator/presets/`) - Shared across all blend files + - Display with 🏠 indicator in UI -3. **Local File Storage** (Lowest Priority) - - Local addon-specific presets - - Automatically migrated to persistent storage when needed +### Key Features -### Preset Features - -- **Improved UI Display**: Recent bug fix ensures all three preset types (Blend File, Persistent, Local) display properly in the interface -- **Auto-Migration**: Seamlessly upgrades presets during addon updates +- **Simplified UI**: Single unified preset list with clear storage indicators +- **Easy Storage Selection**: Simple "Save with .blend file" checkbox controls destination +- **Priority System**: Blend file presets automatically override persistent ones with same name +- **Auto-Migration**: Seamlessly migrates old three-tier presets to new system - **Import/Export**: Backup and share presets across installations -- **Conflict Resolution**: Smart handling of duplicate preset names -- **Migration Reports**: Detailed logs of preset migration operations +- **User-Friendly Messages**: Clear, non-technical confirmation messages + +### How to Use + +- **Default Behavior**: Presets save to persistent storage (🏠) - survives addon updates +- **Project-Specific**: Check "Save with .blend file" to save with your project (📄) +- **Loading**: All presets appear in one list, blend file presets take priority +- **Migration**: Old presets are automatically migrated on first use ## Build System @@ -393,7 +399,7 @@ Marc Mintel - Normal map generation from text alpha channels with strength/blur controls - Advanced shader generation with Principled BSDF, Emission, and Transparent types - Light path controls for shadows, reflections, and backfacing geometry -- Three-tier preset system (blend file → persistent → legacy) with auto-migration +- Simplified two-tier preset system (persistent + blend file) with priority handling - Live preview with zoomable Image Editor integration and multiple backgrounds - Prepend/append text support with horizontal/vertical layouts - Custom font support with mixed-case validation and fallback systems @@ -416,7 +422,7 @@ Marc Mintel #### Full Version Features (Additional): - Normal map generation from text alpha channels -- Complete preset management system with import/export +- Complete preset management system with simplified two-tier storage - Advanced utility operations and batch processing - Extended UI components and advanced styling options - **File Count**: 23 files (60.1 KB package size) @@ -433,6 +439,6 @@ Marc Mintel - `core/normal_maps.py` - Normal map generation algorithms - `operators/preset_ops.py` - Preset management operations - `operators/utility_ops.py` - Batch processing and utility functions -- `presets/` - Entire preset storage and migration system +- `presets/` - Entire simplified preset storage and migration system This approach ensures the free version provides genuine value while the full version offers substantially more capabilities through additional source code modules. diff --git a/marketing/BLENDERMARKET_LISTING.md b/marketing/BLENDERMARKET_LISTING.md index fa4ceca..4243a45 100644 --- a/marketing/BLENDERMARKET_LISTING.md +++ b/marketing/BLENDERMARKET_LISTING.md @@ -43,21 +43,22 @@ Cross-platform compatibility across Windows, macOS, and Linux environments, comb - **Memory Management**: Efficient processing for large textures without system overload - **Batch Processing**: Multiple texture generation with consistent settings - ### Version Strategy #### **🆓 Free Version - Full Core Experience** + - **Complete Text Rendering Pipeline**: Professional typography with up to 1024×1024 resolution -- **Essential Positioning System**: 9-point anchor positioning with pixel-perfect alignment +- **Essential Positioning System**: 9-point anchor positioning with pixel-perfect alignment - **Seamless Blender Integration**: Automatic material node creation and assignment - **Professional Font Support**: System font detection and custom font file compatibility - **Full Color Control**: Complete RGBA customization with transparency support - **No Time Limits**: Use forever - perfect for testing and smaller projects #### **⭐ Full Version - Unleash Professional Power** + - **Ultra-High Resolution**: Generate stunning 4096×4096 textures for commercial work - **Advanced 3D Effects**: Automatic normal map generation with customizable depth and blur -- **Complete Preset Ecosystem**: Three-tier preset management with import/export for team workflows +- **Complete Preset Ecosystem**: Simplified two-tier preset management with import/export for team workflows - **Professional Batch Processing**: Advanced utility operations and workflow automation tools - **Extended Material Pipeline**: Advanced styling options and specialized production tools - **Future-Proof Investment**: All upcoming premium features included with purchase @@ -66,12 +67,14 @@ Cross-platform compatibility across Windows, macOS, and Linux environments, comb ### Smart Preset Management -- **Three-Tier Preset System**: Blend File, Persistent, and Local preset storage with proper UI display +- **Simplified Two-Tier System**: Streamlined Persistent (default) and Blend File storage with unified UI +- **Intuitive Storage Selection**: Simple "Save with .blend file" checkbox controls destination +- **Clear Visual Indicators**: 🏠 (persistent) and 📄 (blend file) icons for instant recognition +- **Priority System**: Blend file presets automatically override persistent ones with same name - **User-Defined Presets**: Save and organize custom configurations with improved interface - **Project-Specific Presets**: Maintain consistent styling across project assets - **Preset Import/Export**: Share configurations between team members -- **Version Control**: Preset versioning for workflow tracking -- **Enhanced UI Display**: Recent improvements ensure all preset types display correctly in the interface +- **Auto-Migration**: Seamlessly migrates old three-tier presets to new streamlined system ### Material Integration diff --git a/marketing/GUMROAD_LISTING.md b/marketing/GUMROAD_LISTING.md index e3e0406..8f1e0cb 100644 --- a/marketing/GUMROAD_LISTING.md +++ b/marketing/GUMROAD_LISTING.md @@ -44,7 +44,7 @@ From indie game developers to 3D artists and creative hobbyists, this tool saves - **Extended Resolution**: Generate up to 4096×4096 high-resolution textures - **Normal Map Generation**: Automatic height-to-normal conversion for realistic 3D depth and surface detail -- **Advanced Preset Management**: Complete preset system with import/export, sharing across three storage types (Blend File, Persistent, Local) +- **Advanced Preset Management**: Simplified two-tier preset system with import/export, easy storage selection with clear visual indicators - **Batch Processing**: Process multiple textures with consistent settings and advanced utility operations - **Professional Effects Suite**: Extended styling options and advanced material integration - **Premium Modules**: Additional specialized tools for professional workflows diff --git a/src/operators/__pycache__/preset_ops.cpython-311.pyc b/src/operators/__pycache__/preset_ops.cpython-311.pyc new file mode 100644 index 0000000..b275f0f Binary files /dev/null and b/src/operators/__pycache__/preset_ops.cpython-311.pyc differ diff --git a/src/operators/preset_ops.py b/src/operators/preset_ops.py index c86a215..fef9e55 100644 --- a/src/operators/preset_ops.py +++ b/src/operators/preset_ops.py @@ -6,10 +6,11 @@ import json import time # Import utility functions from main module -from ..presets import ( - get_preset_path, get_persistent_preset_path, get_addon_version, - get_blend_file_presets, save_blend_file_preset, delete_blend_file_preset, - refresh_presets_sync, ensure_presets_available +from ..presets.storage_system import ( + save_preset_unified, get_unified_preset_list, + get_blend_file_presets, delete_blend_file_preset, + refresh_presets_sync, ensure_presets_available, + get_preset_path, get_persistent_preset_path ) from ..utils import bl_info @@ -161,46 +162,11 @@ class TEXT_TEXTURE_OT_save_preset(Operator): preset_data['image_overlays'].append(overlay_data) try: - # Primary: Save to blend file (new system) - print(f"[PRESET SAVE DEBUG] Attempting to save to blend file...") - blend_success = save_blend_file_preset(preset_name, preset_data) - print(f"[PRESET SAVE DEBUG] Blend file save result: {blend_success}") + # Save based on checkbox setting - single destination only + success = save_preset_unified(preset_data, preset_name, props.save_with_blend_file) - # ALWAYS save to persistent storage for cross-blend-file sharing and addon update survival - persistent_file = os.path.join(get_persistent_preset_path(), f"{preset_name}.json") - print(f"[PRESET SAVE DEBUG] Attempting to save to persistent file: {persistent_file}") - persistent_success = True - try: - with open(persistent_file, 'w') as f: - json.dump(preset_data, f, indent=2) - print(f"[PRESET SAVE DEBUG] Successfully saved preset '{preset_name}' to persistent storage") - - # VERIFY: Read back the persistent file to confirm shader properties were saved - print(f"[PRESET SAVE DEBUG] Verifying persistent file contents...") - with open(persistent_file, 'r') as f: - saved_data = json.load(f) - print(f"[PRESET SAVE DEBUG] Persistent file shader properties verification:") - print(f"[PRESET SAVE DEBUG] disable_shadows: {saved_data.get('disable_shadows', 'NOT FOUND')}") - print(f"[PRESET SAVE DEBUG] disable_reflections: {saved_data.get('disable_reflections', 'NOT FOUND')}") - print(f"[PRESET SAVE DEBUG] disable_backfacing: {saved_data.get('disable_backfacing', 'NOT FOUND')}") - - except Exception as persistent_error: - print(f"[PRESET SAVE DEBUG] Warning: Failed to save persistent preset file: {persistent_error}") - persistent_success = False - - # Also save to legacy location for backward compatibility (optional) - legacy_success = True - try: - legacy_file = os.path.join(get_preset_path(), f"{preset_name}.json") - with open(legacy_file, 'w') as f: - json.dump(preset_data, f, indent=2) - print(f"[PRESET SAVE DEBUG] Also saved to legacy location for backward compatibility") - except Exception as legacy_error: - print(f"[PRESET SAVE DEBUG] Note: Could not save to legacy location: {legacy_error}") - legacy_success = False - - if not blend_success and not persistent_success: - self.report({'ERROR'}, f"Failed to save preset to both blend file and persistent storage") + if not success: + self.report({'ERROR'}, f"Failed to save preset '{preset_name}'") return {'CANCELLED'} # Check if preset already exists in the collection and update it @@ -213,28 +179,18 @@ class TEXT_TEXTURE_OT_save_preset(Operator): if not existing_preset: preset = props.presets.add() preset.name = preset_name - # Priority: Blend file > Persistent file - preset.source = 'BLEND_FILE' if blend_success else 'PERSISTENT_FILE' + preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE' else: - # Update source based on where it was successfully saved - existing_preset.source = 'BLEND_FILE' if blend_success else 'PERSISTENT_FILE' - - # Don't clear the input field - keep the name for easy re-saving - # props.new_preset_name = "New Preset" + # Update source based on where it was saved + existing_preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE' # Provide clear feedback - storage_info = "" - if blend_success and persistent_success: - storage_info = " (saved to blend file + persistent storage)" - elif blend_success: - storage_info = " (saved to blend file)" - elif persistent_success: - storage_info = " (saved to persistent storage)" + location = "with .blend file" if props.save_with_blend_file else "to persistent storage" if self.overwrite: - self.report({'INFO'}, f"✅ Preset '{preset_name}' updated successfully{storage_info}") + self.report({'INFO'}, f"✅ Preset '{preset_name}' updated {location}") else: - self.report({'INFO'}, f"✅ Preset '{preset_name}' saved successfully{storage_info}") + self.report({'INFO'}, f"✅ Preset '{preset_name}' saved {location}") except (OSError, IOError, json.JSONEncodeError) as e: self.report({'ERROR'}, f"Failed to save preset: {str(e)}") @@ -301,57 +257,17 @@ class TEXT_TEXTURE_OT_load_preset(Operator): print(f"[PRESET LOAD DEBUG] shader_connection: {props.shader_connection}") try: - # Three-tier loading priority: Blend File > Persistent File > Legacy File - blend_presets = get_blend_file_presets() - preset_data = None - source = 'BLEND_FILE' + # Get unified preset list and find the one to load + all_presets = get_unified_preset_list() - print(f"[PRESET LOAD DEBUG] Available blend file presets: {list(blend_presets.keys())}") - - if self.preset_name in blend_presets: - preset_data = blend_presets[self.preset_name] - source = 'BLEND_FILE' - print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from blend file") - print(f"[PRESET LOAD DEBUG] Blend file preset shader properties:") - print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") - print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") - print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") - else: - # Try persistent storage next - persistent_file = os.path.join(get_persistent_preset_path(), f"{self.preset_name}.json") - print(f"[PRESET LOAD DEBUG] Preset not in blend file, trying persistent file: {persistent_file}") - if os.path.exists(persistent_file): - with open(persistent_file, 'r') as f: - preset_data = json.load(f) - source = 'PERSISTENT_FILE' - print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from persistent file") - print(f"[PRESET LOAD DEBUG] Persistent file preset shader properties:") - print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") - print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") - print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") - else: - # Fallback to legacy local file - legacy_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") - print(f"[PRESET LOAD DEBUG] Preset not in persistent storage, trying legacy file: {legacy_file}") - if os.path.exists(legacy_file): - with open(legacy_file, 'r') as f: - preset_data = json.load(f) - source = 'LOCAL_FILE' - print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from legacy file") - print(f"[PRESET LOAD DEBUG] Legacy file preset shader properties:") - print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") - print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") - print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") - else: - print(f"[PRESET LOAD DEBUG] Legacy file does not exist: {legacy_file}") - self.report({'ERROR'}, f"Preset not found in any storage location: {self.preset_name}") - return {'CANCELLED'} - - if not preset_data: - print(f"[PRESET LOAD DEBUG] No preset data found!") - self.report({'ERROR'}, f"No data found for preset: {self.preset_name}") + if self.preset_name not in all_presets: + self.report({'ERROR'}, f"Preset '{self.preset_name}' not found") return {'CANCELLED'} + preset_data, source = all_presets[self.preset_name] + + print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from {source}") + props.is_updating = True # Skip loading text to prevent overwriting current text diff --git a/src/presets/__pycache__/storage_system.cpython-311.pyc b/src/presets/__pycache__/storage_system.cpython-311.pyc new file mode 100644 index 0000000..cc08d7b Binary files /dev/null and b/src/presets/__pycache__/storage_system.cpython-311.pyc differ diff --git a/src/presets/storage_system.py b/src/presets/storage_system.py index ddf4856..a705733 100644 --- a/src/presets/storage_system.py +++ b/src/presets/storage_system.py @@ -5,37 +5,31 @@ import time import platform # ============================================================================ -# PRESET STORAGE SYSTEM - V2.0 (PERSISTENT STORAGE) +# PRESET STORAGE SYSTEM - V3.0 (SIMPLIFIED TWO-TIER) # ============================================================================ # -# The Text Texture Generator now uses a THREE-TIER preset storage system -# that ensures preset persistence across addon updates: +# The Text Texture Generator uses a SIMPLIFIED TWO-TIER preset storage system: # -# 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) +# 1. PERSISTENT FILE STORAGE (Default) # - Survives addon updates and Blender upgrades # - Stored in Blender's user config directory # - Shared across all blend files +# - Default behavior when checkbox is unchecked # -# 3. LEGACY FILE STORAGE (Lowest Priority) -# - Old system for backward compatibility -# - Vulnerable to addon updates -# - Will be automatically migrated to persistent storage +# 2. BLEND FILE STORAGE (Optional) +# - Presets travel with the .blend file +# - Perfect for project-specific presets +# - Stored in scene custom properties +# - Used when "Save with .blend file" checkbox is checked +# +# PRIORITY SYSTEM: +# - Blend file presets ALWAYS take priority over persistent ones +# - Name conflicts show only the blend file version # # MIGRATION SYSTEM: -# - Automatically detects addon version changes -# - Migrates legacy presets to persistent storage +# - Automatically migrates LOCAL_FILE presets to PERSISTENT_FILE 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 +# - One-time process with safety checks # # ============================================================================ @@ -298,8 +292,157 @@ def delete_blend_file_preset(preset_name): print(f"Error deleting preset from blend file: {e}") return False +def migrate_from_legacy_system(): + """ + One-time migration from old three-tier system to simplified two-tier system. + Moves LOCAL_FILE presets to PERSISTENT_FILE and creates backup. + """ + try: + legacy_dir = get_preset_path() + persistent_dir = get_persistent_preset_path() + + # Check if legacy directory exists and is different from persistent + if not os.path.exists(legacy_dir) or legacy_dir == persistent_dir: + print("[TTG] No separate legacy presets directory found, migration not needed") + return True + + # Check for migration marker file to avoid duplicate migrations + migration_marker = os.path.join(persistent_dir, ".migration_complete") + if os.path.exists(migration_marker): + print("[TTG] Migration already completed previously") + return True + + # Find legacy preset files + legacy_files = [] + for filename in os.listdir(legacy_dir): + if filename.endswith('.json'): + legacy_files.append(filename) + + if not legacy_files: + print("[TTG] No legacy presets to migrate") + # Create marker even if no files to migrate + with open(migration_marker, 'w') as f: + f.write("Migration completed - no legacy presets found") + return True + + print(f"[TTG] Found {len(legacy_files)} legacy presets to migrate") + + # Create backup directory in persistent location + backup_dir = os.path.join(persistent_dir, "legacy_backup") + os.makedirs(backup_dir, exist_ok=True) + + # Load existing persistent presets to avoid conflicts + existing_persistent = set() + if os.path.exists(persistent_dir): + for filename in os.listdir(persistent_dir): + if filename.endswith('.json'): + existing_persistent.add(filename) + + migrated_count = 0 + skipped_count = 0 + + # Migrate each legacy preset + for filename in legacy_files: + preset_name = os.path.splitext(filename)[0] + legacy_file = os.path.join(legacy_dir, filename) + persistent_file = os.path.join(persistent_dir, filename) + backup_file = os.path.join(backup_dir, filename) + + try: + # Create backup copy first + import shutil + shutil.copy2(legacy_file, backup_file) + + # If preset doesn't exist in persistent storage, migrate it + if filename not in existing_persistent: + shutil.copy2(legacy_file, persistent_file) + migrated_count += 1 + print(f"[TTG] Migrated preset '{preset_name}' to persistent storage") + else: + skipped_count += 1 + print(f"[TTG] Skipped preset '{preset_name}' (already exists in persistent storage)") + + except Exception as e: + print(f"[TTG] Error migrating preset '{preset_name}': {e}") + + # Create migration completion marker + with open(migration_marker, 'w') as f: + migration_info = { + "migration_date": time.time(), + "migrated_count": migrated_count, + "skipped_count": skipped_count, + "backup_location": backup_dir + } + import json + json.dump(migration_info, f, indent=2) + + print(f"[TTG] ✅ Migration completed: {migrated_count} migrated, {skipped_count} skipped") + print(f"[TTG] Backup created at: {backup_dir}") + return True + + except Exception as e: + print(f"[TTG] ❌ Error during migration: {e}") + return False + +def get_unified_preset_list(): + """ + Get unified preset list with two-tier loading and priority rules. + Returns dict with preset names as keys and (preset_data, source) as values. + + Priority order: BLEND_FILE > PERSISTENT_FILE + Blend file presets always take priority over persistent ones. + """ + all_presets = {} + + try: + # Load persistent presets first (lower priority) + persistent_dir = get_persistent_preset_path() + if os.path.exists(persistent_dir): + for filename in os.listdir(persistent_dir): + if filename.endswith('.json') and not filename.startswith('.'): + preset_name = os.path.splitext(filename)[0] + try: + with open(os.path.join(persistent_dir, filename), 'r') as f: + preset_data = json.load(f) + all_presets[preset_name] = (preset_data, 'PERSISTENT_FILE') + except (json.JSONDecodeError, OSError) as e: + print(f"[TTG] Error reading persistent preset {preset_name}: {e}") + + # Load blend file presets and override any conflicts (higher priority) + blend_presets = get_blend_file_presets() + for name, data in blend_presets.items(): + all_presets[name] = (data, 'BLEND_FILE') + + except Exception as e: + print(f"[TTG] Error in get_unified_preset_list: {e}") + + return all_presets + +def save_preset_unified(preset_data, name, save_to_blend_file=False): + """ + Save a preset to either persistent file or blend file based on user choice. + Default behavior saves to persistent file. + """ + if save_to_blend_file: + return save_blend_file_preset(name, preset_data) + else: + # Save to persistent file + try: + persistent_dir = get_persistent_preset_path() + persistent_file = os.path.join(persistent_dir, f"{name}.json") + + with open(persistent_file, 'w') as f: + json.dump(preset_data, f, indent=2) + + print(f"[TTG] Saved preset '{name}' to persistent storage") + return True + + except Exception as e: + print(f"[TTG] Error saving preset to persistent storage: {e}") + return False + def initialize_presets(): - """Initialize presets with simplified, reliable synchronization""" + """Initialize the simplified preset system and run migration if needed.""" try: import bpy if not hasattr(bpy.context, 'scene') or not bpy.context.scene: @@ -311,80 +454,26 @@ def initialize_presets(): print(f"[TTG] Starting preset initialization...") + # Run one-time migration from old system + migrate_from_legacy_system() + # 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}") + # Get unified preset list + all_presets = get_unified_preset_list() # Add all presets to UI list - for preset_name, preset_info in all_presets.items(): + for preset_name, (preset_data, source) in all_presets.items(): preset = props.presets.add() preset.name = preset_name - preset.source = preset_info['source'] + preset.source = 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)") + print(f"[TTG] ✅ Initialized {len(props.presets)} presets ({blend_count} blend file, {persistent_count} persistent)") except Exception as e: print(f"[TTG] Critical error in initialize_presets: {e}") diff --git a/src/properties/__pycache__/property_groups.cpython-311.pyc b/src/properties/__pycache__/property_groups.cpython-311.pyc index 0721841..944b756 100644 Binary files a/src/properties/__pycache__/property_groups.cpython-311.pyc and b/src/properties/__pycache__/property_groups.cpython-311.pyc differ diff --git a/src/properties/property_groups.py b/src/properties/property_groups.py index c5894b2..0cc2d05 100644 --- a/src/properties/property_groups.py +++ b/src/properties/property_groups.py @@ -180,10 +180,9 @@ class TEXT_TEXTURE_Preset(PropertyGroup): description="Where this preset is stored", items=[ ('BLEND_FILE', 'Blend File', 'Preset stored in the blend file (travels with the file)'), - ('PERSISTENT_FILE', 'Persistent File', 'Preset stored in persistent location (survives addon updates)'), - ('LOCAL_FILE', 'Local File', 'Preset stored locally (legacy system)') + ('PERSISTENT_FILE', 'Persistent File', 'Preset stored in persistent location (survives addon updates)') ], - default='BLEND_FILE' + default='PERSISTENT_FILE' ) class TEXT_TEXTURE_Properties(PropertyGroup): @@ -628,6 +627,13 @@ class TEXT_TEXTURE_Properties(PropertyGroup): default=False ) + # Save with blend file checkbox + save_with_blend_file: BoolProperty( + name="Save with .blend file", + description="Save this preset with the current .blend file instead of persistent storage", + default=False + ) + # ============================================================================ # SHADER GENERATION PROPERTIES # ============================================================================ diff --git a/src/ui/__pycache__/panels.cpython-311.pyc b/src/ui/__pycache__/panels.cpython-311.pyc index 775f114..020f68b 100644 Binary files a/src/ui/__pycache__/panels.cpython-311.pyc and b/src/ui/__pycache__/panels.cpython-311.pyc differ diff --git a/src/ui/panels.py b/src/ui/panels.py index da49df9..c32736d 100644 --- a/src/ui/panels.py +++ b/src/ui/panels.py @@ -242,12 +242,15 @@ def draw_preview_options_section(layout, props): layout.prop(props, "preview_bg_color2", text="Color 2") def draw_presets_section(layout, props): - """Draw presets save/load controls""" + """Draw unified presets save/load controls""" # Save preset section save_row = layout.row(align=True) save_row.prop(props, "new_preset_name", text="", placeholder="Preset Name") save_row.operator("text_texture.save_preset", text="Save", icon='FILE_NEW') + # Save with blend file checkbox + layout.prop(props, "save_with_blend_file") + layout.separator() # Import/Export and refresh buttons @@ -261,76 +264,51 @@ def draw_presets_section(layout, props): layout.separator() - # Load presets section + # Load presets section - unified list if props.presets: - layout.label(text=f"📁 Saved Presets ({len(props.presets)}):", icon='PRESET') + layout.label(text=f"📁 Available Presets ({len(props.presets)}):", icon='PRESET') - # Separate presets by source - blend_presets = [p for p in props.presets if p.source == 'BLEND_FILE'] - persistent_presets = [p for p in props.presets if p.source == 'PERSISTENT_FILE'] - local_presets = [p for p in props.presets if p.source == 'LOCAL_FILE'] + # Sort presets alphabetically for consistent display + sorted_presets = sorted(props.presets, key=lambda p: p.name.lower()) - # Show blend file presets first - if blend_presets: - layout.label(text="đŸŽ¯ Blend File Presets (travel with file):", icon='FILE_BLEND') - for preset in blend_presets: - row = layout.row() - load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}") - load_btn.preset_name = preset.name - - # Quick overwrite button - overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH') - overwrite_btn.overwrite = True - # Set the preset name so it can be overwritten - overwrite_btn.preset_name = preset.name - - delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') - delete_btn.preset_name = preset.name + for preset in sorted_presets: + row = layout.row(align=True) + row.scale_y = 0.9 + + # Storage indicator icon + if preset.source == 'BLEND_FILE': + icon = '📄' # Blend file indicator + else: + icon = '🏠' # Persistent storage indicator + + # Load button with indicator + load_btn = row.operator("text_texture.load_preset", text=f"{icon} {preset.name}") + load_btn.preset_name = preset.name + + # Quick overwrite button + overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH') + overwrite_btn.overwrite = True + overwrite_btn.preset_name = preset.name + + # Delete button + delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') + delete_btn.preset_name = preset.name - # Show persistent presets second - if persistent_presets: - if blend_presets: - layout.separator() - layout.label(text="đŸ›Ąī¸ Persistent Presets (survive addon updates):", icon='PREFERENCES') - for preset in persistent_presets: - row = layout.row() - load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}") - load_btn.preset_name = preset.name - - # Quick overwrite button - overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH') - overwrite_btn.overwrite = True - # Set the preset name so it can be overwritten - overwrite_btn.preset_name = preset.name - - delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') - delete_btn.preset_name = preset.name + # Legend for storage indicators + layout.separator() + legend_box = layout.box() + legend_box.scale_y = 0.8 + legend_box.label(text="Storage Indicators:", icon='INFO') + legend_box.label(text="🏠 Persistent (survives addon updates)") + legend_box.label(text="📄 Blend file (travels with .blend file)") - # Show local presets third - if local_presets: - if blend_presets or persistent_presets: - layout.separator() - layout.label(text="đŸ’ģ Local Presets (this machine only):", icon='DISK_DRIVE') - for preset in local_presets: - row = layout.row() - load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}") - load_btn.preset_name = preset.name - - # Quick overwrite button - overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH') - overwrite_btn.overwrite = True - # Set the preset name so it can be overwritten - overwrite_btn.preset_name = preset.name - - delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') - delete_btn.preset_name = preset.name else: layout.label(text="📂 No presets saved yet", icon='INFO') layout.label(text="💡 Save your first preset above!", icon='LIGHT_DATA') layout.separator() info_box = layout.box() - info_box.label(text="â„šī¸ Presets are now saved in the blend file", icon='INFO') - info_box.label(text="They will travel with your .blend file!") + info_box.label(text="â„šī¸ Presets: Persistent by default, blend file optional", icon='INFO') + info_box.label(text="Check the box to save with your .blend file!") def draw_shader_section(layout, props): """Draw shader generation controls"""