887 lines
37 KiB
Python
887 lines
37 KiB
Python
import bpy
|
|
from bpy.types import Operator
|
|
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
|
import os
|
|
import json
|
|
import time
|
|
|
|
# Import utility functions from main module
|
|
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
|
|
|
|
class TEXT_TEXTURE_OT_save_preset(Operator):
|
|
pass
|
|
"""Save current settings as preset"""
|
|
bl_idname = "text_texture.save_preset"
|
|
bl_label = "Save Preset"
|
|
bl_description = "Save current settings as preset"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
overwrite: bpy.props.BoolProperty(
|
|
name="Overwrite",
|
|
description="Overwrite existing preset",
|
|
default=False
|
|
)
|
|
|
|
preset_name: bpy.props.StringProperty(
|
|
name="Preset Name",
|
|
description="Name of preset to save/overwrite",
|
|
default=""
|
|
)
|
|
|
|
def execute(self, context):
|
|
pass
|
|
props = context.scene.text_texture_props
|
|
|
|
# Use provided preset name or fallback to input field
|
|
preset_name = self.preset_name.strip() if self.preset_name else props.new_preset_name.strip()
|
|
if not preset_name:
|
|
pass
|
|
self.report({'ERROR'}, "Please enter a preset name")
|
|
return {'CANCELLED'}
|
|
|
|
# When overwriting from button, update the input field for consistency
|
|
if self.preset_name and self.preset_name.strip():
|
|
pass
|
|
props.new_preset_name = preset_name
|
|
|
|
# Validate preset name characters (avoid filesystem issues)
|
|
import re
|
|
# Allow Unicode letters, numbers, spaces, hyphens, underscores, and common accented characters
|
|
# This pattern supports international characters like ö, ü, ñ, etc.
|
|
if not re.match(r'^[\w\s\-]+$', preset_name, re.UNICODE):
|
|
pass
|
|
self.report({'ERROR'}, "Preset name contains invalid characters for filesystem compatibility")
|
|
return {'CANCELLED'}
|
|
|
|
# Additional check for filesystem-unsafe characters
|
|
invalid_chars = ['<', '>', ':', '"', '|', '?', '*', '/', '\\']
|
|
if any(char in preset_name for char in invalid_chars):
|
|
pass
|
|
self.report({'ERROR'}, f"Preset name cannot contain: {' '.join(invalid_chars)}")
|
|
return {'CANCELLED'}
|
|
|
|
# Check for duplicate names
|
|
preset_file = os.path.join(get_preset_path(), f"{preset_name}.json")
|
|
if os.path.exists(preset_file) and not self.overwrite:
|
|
pass
|
|
# Suggest alternative names
|
|
base_name = preset_name
|
|
counter = 1
|
|
suggested_name = f"{base_name}_{counter}"
|
|
while os.path.exists(os.path.join(get_preset_path(), f"{suggested_name}.json")):
|
|
pass
|
|
counter += 1
|
|
suggested_name = f"{base_name}_{counter}"
|
|
|
|
self.report({'ERROR'}, f"Preset '{preset_name}' already exists. Suggestion: try '{suggested_name}' or delete the existing preset first.")
|
|
return {'CANCELLED'}
|
|
|
|
# DETAILED LOGGING: Current shader properties before saving
|
|
|
|
preset_data = {
|
|
'text': props.text,
|
|
'texture_width': props.texture_width,
|
|
'texture_height': props.texture_height,
|
|
'font_size': props.font_size,
|
|
'padding': props.padding,
|
|
'text_color': list(props.text_color),
|
|
'background_transparent': props.background_transparent,
|
|
'background_color': list(props.background_color),
|
|
'font_path': props.font_path,
|
|
'custom_font_path': props.custom_font_path,
|
|
'use_custom_font': props.use_custom_font,
|
|
'text_align': props.text_align,
|
|
'prepend_text': props.prepend_text,
|
|
'append_text': props.append_text,
|
|
'prepend_margin': props.prepend_margin,
|
|
'append_margin': props.append_margin,
|
|
'prepend_append_layout': props.prepend_append_layout,
|
|
'shader_type': props.shader_type,
|
|
'shader_connection': props.shader_connection,
|
|
'use_alpha': props.use_alpha,
|
|
'emission_strength': props.emission_strength,
|
|
'auto_assign_material': props.auto_assign_material,
|
|
'disable_shadows': props.disable_shadows,
|
|
'disable_reflections': props.disable_reflections,
|
|
'disable_backfacing': props.disable_backfacing,
|
|
'generate_normal_map': props.generate_normal_map,
|
|
'normal_map_strength': props.normal_map_strength,
|
|
'normal_map_blur_radius': props.normal_map_blur_radius,
|
|
'normal_map_invert': props.normal_map_invert,
|
|
'enable_stroke': props.enable_stroke,
|
|
'stroke_width': props.stroke_width,
|
|
'stroke_position': props.stroke_position,
|
|
'stroke_color': list(props.stroke_color),
|
|
'enable_shadow': props.enable_shadow,
|
|
'shadow_offset_x': props.shadow_offset_x,
|
|
'shadow_offset_y': props.shadow_offset_y,
|
|
'shadow_blur': props.shadow_blur,
|
|
'shadow_color': list(props.shadow_color),
|
|
'shadow_spread': getattr(props, 'shadow_spread', 1.0),
|
|
'enable_glow': props.enable_glow,
|
|
'glow_blur': props.glow_blur,
|
|
'glow_color': list(props.glow_color),
|
|
'enable_blur': props.enable_blur,
|
|
'blur_radius': props.blur_radius,
|
|
'image_overlays': []
|
|
}
|
|
|
|
# DETAILED LOGGING: Verify shader properties in preset_data
|
|
|
|
# Save overlay data
|
|
for overlay in props.image_overlays:
|
|
pass
|
|
overlay_data = {
|
|
'name': overlay.name,
|
|
'component_type': overlay.component_type,
|
|
'placement_mode': overlay.placement_mode,
|
|
'text_content': overlay.text_content,
|
|
'text_spacing': overlay.text_spacing,
|
|
'image_spacing': overlay.image_spacing,
|
|
'image_path': overlay.image_path,
|
|
'x_position': overlay.x_position,
|
|
'y_position': overlay.y_position,
|
|
'scale': overlay.scale,
|
|
'rotation': overlay.rotation,
|
|
'z_index': overlay.z_index,
|
|
'enabled': overlay.enabled
|
|
}
|
|
preset_data['image_overlays'].append(overlay_data)
|
|
|
|
try:
|
|
pass
|
|
# Save based on checkbox setting - single destination only
|
|
success = save_preset_unified(preset_data, preset_name, props.save_with_blend_file)
|
|
|
|
if not success:
|
|
pass
|
|
self.report({'ERROR'}, f"Failed to save preset '{preset_name}'")
|
|
return {'CANCELLED'}
|
|
|
|
# Check if preset already exists in the collection and update it
|
|
existing_preset = None
|
|
for preset in props.presets:
|
|
pass
|
|
if preset.name == preset_name:
|
|
pass
|
|
existing_preset = preset
|
|
break
|
|
|
|
target_index = None
|
|
|
|
if not existing_preset:
|
|
pass
|
|
preset = props.presets.add()
|
|
preset.name = preset_name
|
|
preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE'
|
|
target_index = len(props.presets) - 1
|
|
else:
|
|
pass
|
|
# Update source based on where it was saved
|
|
existing_preset.source = 'BLEND_FILE' if props.save_with_blend_file else 'PERSISTENT_FILE'
|
|
for idx, collection_preset in enumerate(props.presets):
|
|
pass
|
|
if collection_preset.name == preset_name:
|
|
pass
|
|
target_index = idx
|
|
break
|
|
|
|
# Provide clear feedback
|
|
location = "with .blend file" if props.save_with_blend_file else "to persistent storage"
|
|
|
|
if self.overwrite:
|
|
pass
|
|
self.report({'INFO'}, f"✅ Preset '{preset_name}' updated {location}")
|
|
else:
|
|
pass
|
|
self.report({'INFO'}, f"✅ Preset '{preset_name}' saved {location}")
|
|
|
|
if target_index is not None and hasattr(props, "active_preset_index"):
|
|
pass
|
|
props.active_preset_index = target_index
|
|
if hasattr(props, "last_loaded_preset"):
|
|
pass
|
|
props.last_loaded_preset = preset_name
|
|
|
|
except (OSError, IOError, json.JSONEncodeError) as e:
|
|
pass
|
|
self.report({'ERROR'}, f"Failed to save preset: {str(e)}")
|
|
return {'CANCELLED'}
|
|
except Exception as e:
|
|
pass
|
|
self.report({'ERROR'}, f"Unexpected error saving preset: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
pass
|
|
# Check if preset exists and prompt for overwrite
|
|
props = context.scene.text_texture_props
|
|
preset_name = props.new_preset_name.strip()
|
|
|
|
if preset_name and preset_name != "New Preset":
|
|
pass
|
|
preset_file = os.path.join(get_preset_path(), f"{preset_name}.json")
|
|
if os.path.exists(preset_file):
|
|
pass
|
|
return context.window_manager.invoke_confirm(self, event)
|
|
|
|
return self.execute(context)
|
|
|
|
def draw(self, context):
|
|
pass
|
|
layout = self.layout
|
|
props = context.scene.text_texture_props
|
|
preset_name = props.new_preset_name.strip()
|
|
layout.label(text=f"Overwrite existing preset '{preset_name}'?")
|
|
|
|
class TEXT_TEXTURE_OT_save_preset_enter(Operator):
|
|
pass
|
|
"""Save preset when Enter key is pressed in name field"""
|
|
bl_idname = "text_texture.save_preset_enter"
|
|
bl_label = "Save Preset (Enter)"
|
|
bl_description = "Save current settings as preset when Enter is pressed"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
pass
|
|
# Delegate to the main save preset operator
|
|
return bpy.ops.text_texture.save_preset('INVOKE_DEFAULT')
|
|
|
|
class TEXT_TEXTURE_OT_clear_preset_search(Operator):
|
|
pass
|
|
"""Clear the preset search filter"""
|
|
bl_idname = "text_texture.clear_preset_search"
|
|
bl_label = "Clear Preset Search"
|
|
bl_description = "Reset the preset search field"
|
|
bl_options = {'INTERNAL'}
|
|
|
|
def execute(self, context):
|
|
pass
|
|
props = context.scene.text_texture_props
|
|
if hasattr(props, "preset_search_query"):
|
|
pass
|
|
props.preset_search_query = ""
|
|
return {'FINISHED'}
|
|
|
|
class TEXT_TEXTURE_OT_load_preset(Operator):
|
|
pass
|
|
"""Load preset"""
|
|
bl_idname = "text_texture.load_preset"
|
|
bl_label = "Load Preset"
|
|
bl_description = "Load selected preset"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
preset_name: StringProperty()
|
|
|
|
def execute(self, context):
|
|
pass
|
|
props = context.scene.text_texture_props
|
|
|
|
# DEFENSIVE PROGRAMMING: Ensure presets are available before loading
|
|
if not ensure_presets_available():
|
|
pass
|
|
self.report({'ERROR'}, "Failed to ensure presets are loaded - cannot load preset")
|
|
return {'CANCELLED'}
|
|
|
|
# DETAILED LOGGING: Current shader properties before loading
|
|
|
|
try:
|
|
pass
|
|
# Get unified preset list and find the one to load
|
|
all_presets = get_unified_preset_list()
|
|
|
|
if self.preset_name not in all_presets:
|
|
pass
|
|
self.report({'ERROR'}, f"Preset '{self.preset_name}' not found")
|
|
return {'CANCELLED'}
|
|
|
|
preset_data, source = all_presets[self.preset_name]
|
|
|
|
|
|
props.is_updating = True
|
|
|
|
# Skip loading text to prevent overwriting current text
|
|
# props.text = preset_data.get('text', props.text)
|
|
props.texture_width = preset_data.get('texture_width', props.texture_width)
|
|
props.texture_height = preset_data.get('texture_height', props.texture_height)
|
|
props.font_size = preset_data.get('font_size', props.font_size)
|
|
props.padding = preset_data.get('padding', props.padding)
|
|
props.text_color = preset_data.get('text_color', props.text_color)
|
|
props.background_transparent = preset_data.get('background_transparent', props.background_transparent)
|
|
props.background_color = preset_data.get('background_color', props.background_color)
|
|
props.font_path = preset_data.get('font_path', props.font_path)
|
|
props.custom_font_path = preset_data.get('custom_font_path', props.custom_font_path)
|
|
props.use_custom_font = preset_data.get('use_custom_font', props.use_custom_font)
|
|
props.text_align = preset_data.get('text_align', props.text_align)
|
|
props.prepend_text = preset_data.get('prepend_text', props.prepend_text)
|
|
props.append_text = preset_data.get('append_text', props.append_text)
|
|
props.prepend_margin = preset_data.get('prepend_margin', props.prepend_margin)
|
|
props.append_margin = preset_data.get('append_margin', props.append_margin)
|
|
props.prepend_append_layout = preset_data.get('prepend_append_layout', props.prepend_append_layout)
|
|
|
|
# DETAILED LOGGING: Shader property loading with before/after values
|
|
|
|
old_shader_type = props.shader_type
|
|
props.shader_type = preset_data.get('shader_type', props.shader_type)
|
|
|
|
old_shader_connection = props.shader_connection
|
|
props.shader_connection = preset_data.get('shader_connection', props.shader_connection)
|
|
|
|
old_use_alpha = props.use_alpha
|
|
props.use_alpha = preset_data.get('use_alpha', props.use_alpha)
|
|
|
|
old_emission_strength = props.emission_strength
|
|
props.emission_strength = preset_data.get('emission_strength', props.emission_strength)
|
|
|
|
old_auto_assign = props.auto_assign_material
|
|
props.auto_assign_material = preset_data.get('auto_assign_material', props.auto_assign_material)
|
|
|
|
# CRITICAL SHADER PROPERTIES - Most detailed logging
|
|
|
|
old_disable_shadows = props.disable_shadows
|
|
new_disable_shadows = preset_data.get('disable_shadows', props.disable_shadows)
|
|
props.disable_shadows = new_disable_shadows
|
|
|
|
old_disable_reflections = props.disable_reflections
|
|
new_disable_reflections = preset_data.get('disable_reflections', props.disable_reflections)
|
|
props.disable_reflections = new_disable_reflections
|
|
|
|
old_disable_backfacing = props.disable_backfacing
|
|
new_disable_backfacing = preset_data.get('disable_backfacing', props.disable_backfacing)
|
|
props.disable_backfacing = new_disable_backfacing
|
|
|
|
|
|
props.generate_normal_map = preset_data.get('generate_normal_map', props.generate_normal_map)
|
|
props.normal_map_strength = preset_data.get('normal_map_strength', props.normal_map_strength)
|
|
props.normal_map_blur_radius = preset_data.get('normal_map_blur_radius', props.normal_map_blur_radius)
|
|
props.normal_map_invert = preset_data.get('normal_map_invert', props.normal_map_invert)
|
|
|
|
# Load stroke settings
|
|
props.enable_stroke = preset_data.get('enable_stroke', props.enable_stroke)
|
|
props.stroke_width = preset_data.get('stroke_width', props.stroke_width)
|
|
props.stroke_position = preset_data.get('stroke_position', props.stroke_position)
|
|
props.stroke_color = preset_data.get('stroke_color', props.stroke_color)
|
|
|
|
# Load shadow/glow settings
|
|
props.enable_shadow = preset_data.get('enable_shadow', props.enable_shadow)
|
|
props.shadow_offset_x = preset_data.get('shadow_offset_x', props.shadow_offset_x)
|
|
props.shadow_offset_y = preset_data.get('shadow_offset_y', props.shadow_offset_y)
|
|
props.shadow_blur = preset_data.get('shadow_blur', props.shadow_blur)
|
|
props.shadow_color = preset_data.get('shadow_color', props.shadow_color)
|
|
# Handle new shadow_spread property with backward compatibility
|
|
if hasattr(props, 'shadow_spread'):
|
|
pass
|
|
props.shadow_spread = preset_data.get('shadow_spread', 1.0)
|
|
props.enable_glow = preset_data.get('enable_glow', props.enable_glow)
|
|
props.glow_blur = preset_data.get('glow_blur', props.glow_blur)
|
|
props.glow_color = preset_data.get('glow_color', props.glow_color)
|
|
|
|
# Load blur settings
|
|
props.enable_blur = preset_data.get('enable_blur', props.enable_blur)
|
|
props.blur_radius = preset_data.get('blur_radius', props.blur_radius)
|
|
|
|
# Load text fitting settings
|
|
props.enable_text_fitting = preset_data.get('enable_text_fitting', props.enable_text_fitting)
|
|
props.text_fitting_mode = preset_data.get('text_fitting_mode', props.text_fitting_mode)
|
|
props.text_fitting_margin = preset_data.get('text_fitting_margin', props.text_fitting_margin)
|
|
props.min_font_size = preset_data.get('min_font_size', props.min_font_size)
|
|
props.max_font_size = preset_data.get('max_font_size', props.max_font_size)
|
|
|
|
# Load overlay data
|
|
props.image_overlays.clear()
|
|
overlay_data_list = preset_data.get('image_overlays', [])
|
|
for overlay_data in overlay_data_list:
|
|
pass
|
|
overlay = props.image_overlays.add()
|
|
overlay.name = overlay_data.get('name', 'Overlay')
|
|
overlay.component_type = overlay_data.get('component_type', overlay.component_type)
|
|
overlay.placement_mode = overlay_data.get('placement_mode', 'ABSOLUTE')
|
|
overlay.text_content = overlay_data.get('text_content', '')
|
|
overlay.text_spacing = overlay_data.get('text_spacing', overlay.text_spacing)
|
|
overlay.image_spacing = overlay_data.get('image_spacing', overlay.image_spacing)
|
|
overlay.image_path = overlay_data.get('image_path', '')
|
|
overlay.x_position = overlay_data.get('x_position', 0.5)
|
|
overlay.y_position = overlay_data.get('y_position', 0.5)
|
|
overlay.scale = overlay_data.get('scale', 1.0)
|
|
overlay.rotation = overlay_data.get('rotation', 0.0)
|
|
overlay.z_index = overlay_data.get('z_index', 1)
|
|
overlay.enabled = overlay_data.get('enabled', True)
|
|
|
|
props.is_updating = False
|
|
|
|
# FINAL VERIFICATION: Check what the properties actually are after loading
|
|
|
|
# Always generate preview after loading preset
|
|
# Import preview generation function
|
|
from ..core.generation_engine import generate_preview
|
|
generate_preview(props, props.preview_size if hasattr(props, 'preview_size') else 512, props.preview_size if hasattr(props, 'preview_size') else 512)
|
|
|
|
if hasattr(props, "last_loaded_preset"):
|
|
pass
|
|
props.last_loaded_preset = self.preset_name
|
|
if hasattr(props, "active_preset_index"):
|
|
pass
|
|
for idx, preset in enumerate(props.presets):
|
|
pass
|
|
if preset.name == self.preset_name:
|
|
pass
|
|
props.active_preset_index = idx
|
|
break
|
|
|
|
source_info = {
|
|
'BLEND_FILE': "from blend file",
|
|
'PERSISTENT_FILE': "from persistent storage",
|
|
'LOCAL_FILE': "from legacy storage"
|
|
}.get(source, "from unknown source")
|
|
self.report({'INFO'}, f"✅ Loaded preset '{self.preset_name}' {source_info}")
|
|
|
|
except (OSError, IOError) as e:
|
|
pass
|
|
props.is_updating = False
|
|
self.report({'ERROR'}, f"Failed to read preset file: {str(e)}")
|
|
return {'CANCELLED'}
|
|
except json.JSONDecodeError as e:
|
|
pass
|
|
props.is_updating = False
|
|
self.report({'ERROR'}, f"Invalid preset file format: {str(e)}")
|
|
return {'CANCELLED'}
|
|
except Exception as e:
|
|
pass
|
|
props.is_updating = False
|
|
self.report({'ERROR'}, f"Unexpected error loading preset: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
class TEXT_TEXTURE_OT_delete_preset(Operator):
|
|
pass
|
|
"""Delete preset"""
|
|
bl_idname = "text_texture.delete_preset"
|
|
bl_label = "Delete Preset"
|
|
bl_description = "Delete selected preset"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
preset_name: StringProperty()
|
|
|
|
def execute(self, context):
|
|
pass
|
|
props = context.scene.text_texture_props
|
|
|
|
try:
|
|
pass
|
|
# Delete from both blend file and local storage
|
|
blend_deleted = delete_blend_file_preset(self.preset_name)
|
|
|
|
preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json")
|
|
local_deleted = False
|
|
if os.path.exists(preset_file):
|
|
pass
|
|
os.remove(preset_file)
|
|
local_deleted = True
|
|
|
|
if not blend_deleted and not local_deleted:
|
|
pass
|
|
self.report({'WARNING'}, f"Preset '{self.preset_name}' not found in blend file or local storage")
|
|
|
|
# Remove from UI list
|
|
removed_index = None
|
|
for i, preset in enumerate(props.presets):
|
|
pass
|
|
if preset.name == self.preset_name:
|
|
pass
|
|
props.presets.remove(i)
|
|
removed_index = i
|
|
break
|
|
|
|
if hasattr(props, "last_loaded_preset") and props.last_loaded_preset == self.preset_name:
|
|
pass
|
|
props.last_loaded_preset = ""
|
|
if removed_index is not None and hasattr(props, "active_preset_index"):
|
|
pass
|
|
if len(props.presets) == 0:
|
|
pass
|
|
props.active_preset_index = 0
|
|
else:
|
|
pass
|
|
props.active_preset_index = min(removed_index, len(props.presets) - 1)
|
|
|
|
delete_info = []
|
|
if blend_deleted:
|
|
pass
|
|
delete_info.append("blend file")
|
|
if local_deleted:
|
|
pass
|
|
delete_info.append("local storage")
|
|
|
|
location_text = " and ".join(delete_info) if delete_info else "nowhere (not found)"
|
|
self.report({'INFO'}, f"🗑️ Deleted preset '{self.preset_name}' from {location_text}")
|
|
|
|
except (OSError, IOError) as e:
|
|
pass
|
|
self.report({'ERROR'}, f"Failed to delete preset file: {str(e)}")
|
|
return {'CANCELLED'}
|
|
except Exception as e:
|
|
pass
|
|
self.report({'ERROR'}, f"Unexpected error deleting preset: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
pass
|
|
# Add confirmation dialog for better UX
|
|
return context.window_manager.invoke_confirm(self, event)
|
|
|
|
def draw(self, context):
|
|
pass
|
|
layout = self.layout
|
|
layout.label(text=f"Delete preset '{self.preset_name}'?")
|
|
layout.label(text="This action cannot be undone.", icon='ERROR')
|
|
|
|
class TEXT_TEXTURE_OT_refresh_presets(Operator):
|
|
pass
|
|
"""Refresh presets from blend file and local storage"""
|
|
bl_idname = "text_texture.refresh_presets"
|
|
bl_label = "Refresh Presets"
|
|
bl_description = "Reload presets from blend file and local storage"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
pass
|
|
try:
|
|
pass
|
|
# Use the reliable synchronous refresh system
|
|
success = refresh_presets_sync()
|
|
|
|
if success:
|
|
pass
|
|
props = context.scene.text_texture_props
|
|
total_presets = len(props.presets)
|
|
|
|
# Count by source for detailed reporting
|
|
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'])
|
|
local_count = len([p for p in props.presets if p.source == 'LOCAL_FILE'])
|
|
|
|
self.report({'INFO'}, f"✅ Refreshed {total_presets} presets ({blend_count} blend file, {persistent_count} persistent, {local_count} local)")
|
|
else:
|
|
pass
|
|
self.report({'WARNING'}, "Preset refresh completed with issues - check console for details")
|
|
|
|
# Force UI redraw
|
|
for area in context.screen.areas:
|
|
pass
|
|
area.tag_redraw()
|
|
|
|
return {'FINISHED'}
|
|
|
|
except Exception as e:
|
|
pass
|
|
self.report({'ERROR'}, f"Failed to refresh presets: {str(e)}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return {'CANCELLED'}
|
|
|
|
class TEXT_TEXTURE_OT_export_presets(Operator):
|
|
pass
|
|
"""Export all presets to a file for backup"""
|
|
bl_idname = "text_texture.export_presets"
|
|
bl_label = "Export Presets"
|
|
bl_description = "Export all presets to a file for backup"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
filepath: StringProperty(
|
|
name="File Path",
|
|
description="Path to save preset backup file",
|
|
subtype='FILE_PATH',
|
|
default="text_texture_presets_backup.json"
|
|
)
|
|
|
|
def execute(self, context):
|
|
pass
|
|
try:
|
|
pass
|
|
props = context.scene.text_texture_props
|
|
|
|
# Collect all presets from all sources
|
|
all_presets = {}
|
|
|
|
# Get blend file presets
|
|
blend_presets = get_blend_file_presets()
|
|
for name, data in blend_presets.items():
|
|
pass
|
|
all_presets[name] = {
|
|
"data": data,
|
|
"source": "BLEND_FILE"
|
|
}
|
|
|
|
# Get persistent presets
|
|
persistent_dir = get_persistent_preset_path()
|
|
if os.path.exists(persistent_dir):
|
|
pass
|
|
for filename in os.listdir(persistent_dir):
|
|
pass
|
|
if filename.endswith('.json'):
|
|
pass
|
|
preset_name = os.path.splitext(filename)[0]
|
|
if preset_name not in all_presets: # Don't override blend file presets
|
|
try:
|
|
pass
|
|
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:
|
|
pass
|
|
|
|
# Get legacy presets
|
|
legacy_dir = get_preset_path()
|
|
if os.path.exists(legacy_dir) and legacy_dir != persistent_dir:
|
|
pass
|
|
for filename in os.listdir(legacy_dir):
|
|
pass
|
|
if filename.endswith('.json'):
|
|
pass
|
|
preset_name = os.path.splitext(filename)[0]
|
|
if preset_name not in all_presets: # Don't override higher priority presets
|
|
try:
|
|
pass
|
|
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:
|
|
pass
|
|
|
|
if not all_presets:
|
|
pass
|
|
self.report({'WARNING'}, "No presets found to export")
|
|
return {'CANCELLED'}
|
|
|
|
# Create export data with metadata
|
|
export_data = {
|
|
"format_version": "1.0",
|
|
"addon_version": bl_info["version"],
|
|
"export_timestamp": time.time(),
|
|
"presets": all_presets
|
|
}
|
|
|
|
# Write to file
|
|
with open(self.filepath, 'w') as f:
|
|
json.dump(export_data, f, indent=2)
|
|
|
|
self.report({'INFO'}, f"✅ Exported {len(all_presets)} presets to {os.path.basename(self.filepath)}")
|
|
return {'FINISHED'}
|
|
|
|
except Exception as e:
|
|
pass
|
|
self.report({'ERROR'}, f"Failed to export presets: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
def invoke(self, context, event):
|
|
pass
|
|
# Set default filename with timestamp
|
|
import datetime
|
|
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
self.filepath = f"text_texture_presets_backup_{timestamp}.json"
|
|
|
|
context.window_manager.fileselect_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
class TEXT_TEXTURE_OT_import_presets(Operator):
|
|
pass
|
|
"""Import presets from a backup file"""
|
|
bl_idname = "text_texture.import_presets"
|
|
bl_label = "Import Presets"
|
|
bl_description = "Import presets from a backup file"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
filepath: StringProperty(
|
|
name="File Path",
|
|
description="Path to preset backup file",
|
|
subtype='FILE_PATH'
|
|
)
|
|
|
|
import_mode: EnumProperty(
|
|
name="Import Mode",
|
|
description="How to handle existing presets with same names",
|
|
items=[
|
|
('SKIP', 'Skip Existing', 'Skip presets that already exist'),
|
|
('OVERWRITE', 'Overwrite', 'Overwrite existing presets'),
|
|
('RENAME', 'Rename New', 'Rename imported presets if they conflict'),
|
|
],
|
|
default='SKIP'
|
|
)
|
|
|
|
import_to_blend: BoolProperty(
|
|
name="Import to Blend File",
|
|
description="Import presets to blend file (recommended for portability)",
|
|
default=True
|
|
)
|
|
|
|
def execute(self, context):
|
|
pass
|
|
try:
|
|
pass
|
|
if not os.path.exists(self.filepath):
|
|
pass
|
|
self.report({'ERROR'}, "Backup file not found")
|
|
return {'CANCELLED'}
|
|
|
|
props = context.scene.text_texture_props
|
|
|
|
# Read backup file
|
|
with open(self.filepath, 'r') as f:
|
|
import_data = json.load(f)
|
|
|
|
# Validate format
|
|
if "presets" not in import_data:
|
|
pass
|
|
self.report({'ERROR'}, "Invalid backup file format")
|
|
return {'CANCELLED'}
|
|
|
|
imported_presets = import_data["presets"]
|
|
if not imported_presets:
|
|
pass
|
|
self.report({'WARNING'}, "No presets found in backup file")
|
|
return {'CANCELLED'}
|
|
|
|
# Get existing presets to check for conflicts
|
|
existing_blend_presets = get_blend_file_presets()
|
|
existing_preset_names = set(existing_blend_presets.keys())
|
|
|
|
# Also check persistent storage
|
|
persistent_dir = get_persistent_preset_path()
|
|
if os.path.exists(persistent_dir):
|
|
pass
|
|
for filename in os.listdir(persistent_dir):
|
|
pass
|
|
if filename.endswith('.json'):
|
|
pass
|
|
existing_preset_names.add(os.path.splitext(filename)[0])
|
|
|
|
imported_count = 0
|
|
skipped_count = 0
|
|
errors = []
|
|
|
|
for preset_name, preset_info in imported_presets.items():
|
|
pass
|
|
preset_data = preset_info.get("data", {})
|
|
|
|
# Handle conflicts
|
|
final_name = preset_name
|
|
if preset_name in existing_preset_names:
|
|
pass
|
|
if self.import_mode == 'SKIP':
|
|
pass
|
|
skipped_count += 1
|
|
continue
|
|
elif self.import_mode == 'RENAME':
|
|
counter = 1
|
|
base_name = preset_name
|
|
while final_name in existing_preset_names:
|
|
pass
|
|
final_name = f"{base_name}_imported_{counter}"
|
|
counter += 1
|
|
# OVERWRITE mode uses original name
|
|
|
|
try:
|
|
pass
|
|
# Import to blend file or persistent storage based on setting
|
|
if self.import_to_blend:
|
|
pass
|
|
success = save_blend_file_preset(final_name, preset_data)
|
|
if not success:
|
|
pass
|
|
raise Exception("Failed to save to blend file")
|
|
else:
|
|
pass
|
|
# Save to persistent storage
|
|
persistent_file = os.path.join(get_persistent_preset_path(), f"{final_name}.json")
|
|
with open(persistent_file, 'w') as f:
|
|
json.dump(preset_data, f, indent=2)
|
|
|
|
imported_count += 1
|
|
|
|
# Add to UI list if not already present
|
|
existing_preset = None
|
|
for preset in props.presets:
|
|
pass
|
|
if preset.name == final_name:
|
|
pass
|
|
existing_preset = preset
|
|
break
|
|
|
|
if not existing_preset:
|
|
pass
|
|
new_preset = props.presets.add()
|
|
new_preset.name = final_name
|
|
new_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE'
|
|
else:
|
|
pass
|
|
existing_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE'
|
|
|
|
except Exception as e:
|
|
pass
|
|
errors.append(f"{preset_name}: {str(e)}")
|
|
|
|
# Report results
|
|
if imported_count > 0:
|
|
pass
|
|
storage_location = "blend file" if self.import_to_blend else "persistent storage"
|
|
self.report({'INFO'}, f"✅ Imported {imported_count} presets to {storage_location}")
|
|
if skipped_count > 0:
|
|
pass
|
|
self.report({'INFO'}, f"⏭️ Skipped {skipped_count} existing presets")
|
|
else:
|
|
pass
|
|
self.report({'WARNING'}, "No presets were imported")
|
|
|
|
if errors:
|
|
pass
|
|
self.report({'WARNING'}, f"Errors importing {len(errors)} presets - check console")
|
|
for error in errors[:5]: # Show first 5 errors
|
|
print(f"Import error: {error}")
|
|
|
|
return {'FINISHED'}
|
|
|
|
except Exception as e:
|
|
pass
|
|
self.report({'ERROR'}, f"Failed to import presets: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
def invoke(self, context, event):
|
|
pass
|
|
context.window_manager.fileselect_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def draw(self, context):
|
|
pass
|
|
layout = self.layout
|
|
layout.prop(self, "import_mode")
|
|
layout.prop(self, "import_to_blend")
|
|
|
|
# Export all operator classes for wildcard imports
|
|
__all__ = [
|
|
'TEXT_TEXTURE_OT_save_preset',
|
|
'TEXT_TEXTURE_OT_save_preset_enter',
|
|
'TEXT_TEXTURE_OT_clear_preset_search',
|
|
'TEXT_TEXTURE_OT_load_preset',
|
|
'TEXT_TEXTURE_OT_delete_preset',
|
|
'TEXT_TEXTURE_OT_refresh_presets',
|
|
'TEXT_TEXTURE_OT_export_presets',
|
|
'TEXT_TEXTURE_OT_import_presets',
|
|
]
|