deployment

This commit is contained in:
2025-09-11 17:35:33 +03:00
parent 9c2c879483
commit 7c40714d48
12 changed files with 387 additions and 284 deletions

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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}")

View File

@@ -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
# ============================================================================

View File

@@ -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"""