""" Text Texture Generator - Utility Operators Handles preview refresh, font refresh, UI operations, overlay management, and positioning utilities. """ import bpy from bpy.types import Operator from bpy.props import StringProperty, IntProperty, EnumProperty import os # Import functions from parent module try: print("[TTG DEBUG] IMPORT TRACE: Attempting to import from ui.preview...") print("[TTG DEBUG] IMPORT TRACE: Looking for generate_preview in ui.preview module") # First, let's see what's actually available in ui.preview from .. import ui print(f"[TTG DEBUG] IMPORT TRACE: ui.preview module contents: {dir(ui.preview)}") # Now try the actual import that's failing from ..core.generation_engine import generate_preview print("[TTG DEBUG] IMPORT TRACE: SUCCESS - generate_preview imported from ui.preview") from ..utils.system import get_system_fonts, get_font_enum_items from ..presets.storage_system import refresh_presets_sync, ensure_presets_available from ..utils.constants import sync_margin_values except ImportError as e: print(f"[TTG DEBUG] IMPORT ERROR: Failed to import from ui.preview: {e}") print("[TTG DEBUG] IMPORT TRACE: This confirms generate_preview is NOT in ui.preview") # Let's check if it's available in core.generation_engine try: print("[TTG DEBUG] IMPORT TRACE: Attempting fallback import from core.generation_engine...") from ..core.generation_engine import generate_preview print("[TTG DEBUG] IMPORT TRACE: SUCCESS - generate_preview found in core.generation_engine!") from ..utils.system import get_system_fonts, get_font_enum_items from ..presets.storage_system import refresh_presets_sync, ensure_presets_available from ..utils.constants import sync_margin_values print("[TTG DEBUG] IMPORT TRACE: All imports successful with corrected path") except ImportError as fallback_error: print(f"[TTG DEBUG] IMPORT ERROR: Fallback import also failed: {fallback_error}") print(f"[TTG DEBUG] IMPORT ERROR: Original error was: {e}") # Fallback imports for development/testing print("[TTG DEBUG] IMPORT TRACE: Using fallback placeholder functions") def generate_preview(context): print("Fallback generate_preview called") return None def get_system_fonts(): return {} def get_font_enum_items(): return [("default", "Default Font", "Use system default font", 0)] def refresh_presets_sync(): return True def ensure_presets_available(): return True def sync_margin_values(props, changed_margin): pass class TEXT_TEXTURE_OT_refresh_preview(Operator): """Force refresh the preview""" bl_idname = "text_texture.refresh_preview" bl_label = "Refresh Preview" bl_description = "Force refresh the preview image" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): props = context.scene.text_texture_props # Clear existing preview first props.preview_image = None # Generate new preview result = generate_preview(context) if result: self.report({'INFO'}, f"Preview refreshed at {props.preview_size}px") else: self.report({'ERROR'}, "Failed to generate preview - check console for details") # Force UI update for area in context.screen.areas: area.tag_redraw() return {'FINISHED'} class TEXT_TEXTURE_OT_view_preview_zoom(Operator): """Open preview in new Image Editor window with zoom support""" bl_idname = "text_texture.view_preview_zoom" bl_label = "Enable Zoom" bl_description = "Open preview in new Image Editor window with zoom and pan controls" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): props = context.scene.text_texture_props # Generate preview image when button is clicked print("[TTG] Generating preview on button click...") result = generate_preview(context) if not result: self.report({'ERROR'}, "Failed to generate preview") return {'CANCELLED'} if not props.preview_image: self.report({'ERROR'}, "No preview image to display") return {'CANCELLED'} # Open a new window with Image Editor print(f"[ZOOM] Opening new window for preview image: {props.preview_image.name}") try: # Create a new window bpy.ops.screen.userpref_show('INVOKE_DEFAULT') # Wait a moment and then change the newly opened window to Image Editor def setup_image_window(): # Find the newest window (should be the one we just opened) newest_window = None for window in bpy.context.window_manager.windows: if window.screen.name.startswith("temp"): newest_window = window break if not newest_window: # Fallback: use any non-main window for window in bpy.context.window_manager.windows: if window != bpy.context.window: newest_window = window break if newest_window: # Set the area type to Image Editor for area in newest_window.screen.areas: if area.type == 'PREFERENCES': area.type = 'IMAGE_EDITOR' print("[ZOOM] Changed area to Image Editor") # Set the preview image for space in area.spaces: if space.type == 'IMAGE_EDITOR': space.image = props.preview_image print(f"[ZOOM] Set preview image: {props.preview_image.name}") # Set up proper viewing override = { 'window': newest_window, 'screen': newest_window.screen, 'area': area, 'region': area.regions[0] if area.regions else None } with bpy.context.temp_override(**override): try: bpy.ops.image.view_all(fit_view=True) print("[ZOOM] Applied fit to view") except Exception as e: print(f"[ZOOM] Error applying fit to view: {e}") break break self.report({'INFO'}, "✅ NEW WINDOW OPENED! Use mouse wheel to zoom, middle-mouse to pan") print("[ZOOM] New window zoom feature activated successfully") else: print("[ZOOM] Could not find new window") self.report({'ERROR'}, "Failed to setup new window") return None # Stop timer # Use a timer to set up the window after it opens bpy.app.timers.register(setup_image_window, first_interval=0.1) except Exception as e: print(f"[ZOOM] Error creating new window: {e}") # Fallback to old behavior if new window creation fails self.report({'WARNING'}, "New window failed, trying area split instead") # Find any area to split for area in context.screen.areas: if area.type in ['NODE_EDITOR', 'VIEW_3D', 'PROPERTIES']: print(f"[ZOOM] Fallback: Splitting {area.type} to create Image Editor") # Split horizontally to create a new area with context.temp_override(area=area): bpy.ops.screen.area_split(direction='HORIZONTAL', factor=0.6) # Find the newly created area for new_area in context.screen.areas: if new_area != area and new_area.y != area.y: new_area.type = 'IMAGE_EDITOR' # Set the image in the Image Editor for space in new_area.spaces: if space.type == 'IMAGE_EDITOR': space.image = props.preview_image print("[ZOOM] Fallback: Image set in split Image Editor") break # Tag for redraw and fit to view new_area.tag_redraw() override = context.copy() override['area'] = new_area override['region'] = new_area.regions[0] with context.temp_override(**override): try: bpy.ops.image.view_all(fit_view=True) print("[ZOOM] Fallback: Fit to view applied") except Exception as e: print(f"[ZOOM] Fallback: Error applying fit to view: {e}") self.report({'INFO'}, "✅ ZOOM ENABLED! Use mouse wheel to zoom, middle-mouse to pan") return {'FINISHED'} break self.report({'ERROR'}, "Failed to create Image Editor") return {'CANCELLED'} return {'FINISHED'} class TEXT_TEXTURE_OT_open_panel(Operator): """Open Text Texture Generator panel""" bl_idname = "text_texture.open_panel" bl_label = "Open Text Texture Panel" bl_description = "Open the Text Texture Generator panel in the sidebar" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): # Ensure the sidebar is visible for area in context.screen.areas: if area.type == 'VIEW_3D': for space in area.spaces: if space.type == 'VIEW_3D': space.show_region_ui = True # Switch to Tool tab area.tag_redraw() self.report({'INFO'}, "Text Texture panel opened in 3D View sidebar (press N if not visible)") return {'FINISHED'} elif area.type == 'NODE_EDITOR': for space in area.spaces: if space.type == 'NODE_EDITOR': space.show_region_ui = True area.tag_redraw() self.report({'INFO'}, "Text Texture panel opened in Shader Editor sidebar") return {'FINISHED'} self.report({'WARNING'}, "Please open a 3D View or Shader Editor first") return {'CANCELLED'} class TEXT_TEXTURE_OT_refresh_fonts(Operator): """Refresh fonts""" bl_idname = "text_texture.refresh_fonts" bl_label = "Refresh Fonts" bl_description = "Refresh font list" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): if hasattr(get_font_enum_items, 'cached_fonts'): del get_font_enum_items.cached_fonts get_font_enum_items.cached_fonts = get_system_fonts() self.report({'INFO'}, f"Found {len(get_font_enum_items.cached_fonts)} fonts") return {'FINISHED'} class TEXT_TEXTURE_OT_refresh_presets(Operator): """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): try: # Use the reliable synchronous refresh system success = refresh_presets_sync() if success: 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: self.report({'WARNING'}, "Preset refresh completed with issues - check console for details") # Force UI redraw for area in context.screen.areas: area.tag_redraw() return {'FINISHED'} except Exception as e: self.report({'ERROR'}, f"Failed to refresh presets: {str(e)}") import traceback traceback.print_exc() return {'CANCELLED'} class TEXT_TEXTURE_OT_add_overlay(Operator): """Add new image overlay""" bl_idname = "text_texture.add_overlay" bl_label = "Add Overlay" bl_description = "Add new image overlay" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): props = context.scene.text_texture_props overlay = props.image_overlays.add() overlay.name = f"Overlay {len(props.image_overlays)}" props.active_overlay_index = len(props.image_overlays) - 1 # Force UI redraw for area in context.screen.areas: area.tag_redraw() self.report({'INFO'}, f"Added overlay: {overlay.name}") return {'FINISHED'} class TEXT_TEXTURE_OT_remove_overlay(Operator): """Remove active image overlay""" bl_idname = "text_texture.remove_overlay" bl_label = "Remove Overlay" bl_description = "Remove active image overlay" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): props = context.scene.text_texture_props if props.image_overlays and props.active_overlay_index < len(props.image_overlays): overlay_name = props.image_overlays[props.active_overlay_index].name props.image_overlays.remove(props.active_overlay_index) # Update active index if props.active_overlay_index >= len(props.image_overlays) and props.image_overlays: props.active_overlay_index = len(props.image_overlays) - 1 # Force UI redraw for area in context.screen.areas: area.tag_redraw() self.report({'INFO'}, f"Removed overlay: {overlay_name}") else: self.report({'WARNING'}, "No overlay to remove") return {'FINISHED'} class TEXT_TEXTURE_OT_set_anchor_point(Operator): """Set anchor point for text positioning""" bl_idname = "text_texture.set_anchor_point" bl_label = "Set Anchor Point" bl_description = "Set text anchor position" bl_options = {'REGISTER', 'UNDO'} anchor_point: StringProperty() def execute(self, context): props = context.scene.text_texture_props props.anchor_point = self.anchor_point return {'FINISHED'} class TEXT_TEXTURE_OT_sync_margins(Operator): """Sync margin values when linked""" bl_idname = "text_texture.sync_margins" bl_label = "Sync Margins" bl_description = "Synchronize margin values" bl_options = {'REGISTER', 'UNDO'} margin_type: StringProperty() def execute(self, context): props = context.scene.text_texture_props sync_margin_values(props, self.margin_type) return {'FINISHED'} class TEXT_TEXTURE_OT_duplicate_overlay(Operator): """Duplicate image overlay""" bl_idname = "text_texture.duplicate_overlay" bl_label = "Duplicate Overlay" bl_description = "Duplicate this image overlay" bl_options = {'REGISTER', 'UNDO'} overlay_index: IntProperty() def execute(self, context): props = context.scene.text_texture_props if 0 <= self.overlay_index < len(props.image_overlays): source_overlay = props.image_overlays[self.overlay_index] # Debug logging print(f"[DUPLICATE DEBUG] Duplicating overlay '{source_overlay.name}'") print(f"[DUPLICATE DEBUG] Source properties:") print(f"[DUPLICATE DEBUG] image_path: '{source_overlay.image_path}'") print(f"[DUPLICATE DEBUG] enabled: {source_overlay.enabled}") print(f"[DUPLICATE DEBUG] positioning_mode: '{source_overlay.positioning_mode}'") print(f"[DUPLICATE DEBUG] scale: {source_overlay.scale}") print(f"[DUPLICATE DEBUG] z_index: {source_overlay.z_index}") new_overlay = props.image_overlays.add() # Copy all properties with explicit verification new_overlay.name = f"{source_overlay.name} Copy" new_overlay.image_path = source_overlay.image_path new_overlay.x_position = source_overlay.x_position new_overlay.y_position = source_overlay.y_position new_overlay.scale = source_overlay.scale new_overlay.rotation = source_overlay.rotation # Fix z-index assignment for duplicated overlays to ensure proper visibility # For PREPEND/APPEND overlays, increment z_index to ensure duplicate appears above original # This prevents the duplicate from being rendered behind the original overlay if source_overlay.positioning_mode in ['PREPEND', 'APPEND']: # Increment z_index to ensure proper layering order for duplicated overlay new_overlay.z_index = min(source_overlay.z_index + 1, 10) # Cap at max z_index value print(f"[DUPLICATE DEBUG] Fixed z_index for {source_overlay.positioning_mode} overlay: {source_overlay.z_index} -> {new_overlay.z_index}") else: # For ABSOLUTE overlays, keep same z_index (different positions so no overlap issue) new_overlay.z_index = source_overlay.z_index new_overlay.positioning_mode = source_overlay.positioning_mode new_overlay.text_spacing = source_overlay.text_spacing new_overlay.image_spacing = source_overlay.image_spacing new_overlay.enabled = source_overlay.enabled # Verify the copy was successful print(f"[DUPLICATE DEBUG] New overlay properties:") print(f"[DUPLICATE DEBUG] name: '{new_overlay.name}'") print(f"[DUPLICATE DEBUG] image_path: '{new_overlay.image_path}'") print(f"[DUPLICATE DEBUG] enabled: {new_overlay.enabled}") print(f"[DUPLICATE DEBUG] positioning_mode: '{new_overlay.positioning_mode}'") print(f"[DUPLICATE DEBUG] scale: {new_overlay.scale}") print(f"[DUPLICATE DEBUG] z_index: {new_overlay.z_index}") # Move to position after source new_index = len(props.image_overlays) - 1 target_index = min(self.overlay_index + 1, new_index) if new_index != target_index: props.image_overlays.move(new_index, target_index) print(f"[DUPLICATE DEBUG] Moved overlay from index {new_index} to {target_index}") props.active_overlay_index = target_index # Clear any cached images to force reload print(f"[DUPLICATE DEBUG] Clearing image cache and forcing preview update...") # Set the updating flag to prevent recursive updates props.is_updating = True try: # Force immediate preview regeneration print(f"[DUPLICATE DEBUG] Generating new preview...") preview_result = generate_preview(context) if preview_result: print(f"[DUPLICATE DEBUG] Preview generated successfully") else: print(f"[DUPLICATE DEBUG] Preview generation failed") # Force UI redraw for area in context.screen.areas: area.tag_redraw() except Exception as e: print(f"[DUPLICATE DEBUG] Error updating preview: {e}") import traceback traceback.print_exc() finally: props.is_updating = False print(f"[DUPLICATE DEBUG] Total overlays now: {len(props.image_overlays)}") print(f"[DUPLICATE DEBUG] Duplication operation completed") self.report({'INFO'}, f"✅ Duplicated overlay: {source_overlay.name}") else: self.report({'ERROR'}, "Invalid overlay index") return {'CANCELLED'} return {'FINISHED'} class TEXT_TEXTURE_OT_delete_overlay(Operator): """Delete image overlay""" bl_idname = "text_texture.delete_overlay" bl_label = "Delete Overlay" bl_description = "Delete this image overlay" bl_options = {'REGISTER', 'UNDO'} overlay_index: IntProperty() def execute(self, context): props = context.scene.text_texture_props print(f"[DELETE DEBUG] Deleting overlay at index {self.overlay_index}") if 0 <= self.overlay_index < len(props.image_overlays): overlay_name = props.image_overlays[self.overlay_index].name print(f"[DELETE DEBUG] Deleting overlay: '{overlay_name}'") props.image_overlays.remove(self.overlay_index) # Update active index if props.active_overlay_index >= len(props.image_overlays) and props.image_overlays: props.active_overlay_index = len(props.image_overlays) - 1 elif not props.image_overlays: props.active_overlay_index = 0 print(f"[DELETE DEBUG] Overlay deleted successfully. Total overlays now: {len(props.image_overlays)}") # Force preview update after deletion try: generate_preview(context) print(f"[DELETE DEBUG] Preview updated after deletion") except Exception as e: print(f"[DELETE DEBUG] Error updating preview: {e}") # Force UI redraw for area in context.screen.areas: area.tag_redraw() self.report({'INFO'}, f"Deleted overlay: {overlay_name}") else: print(f"[DELETE DEBUG] Invalid overlay index: {self.overlay_index}") self.report({'ERROR'}, "Invalid overlay index") return {'CANCELLED'} return {'FINISHED'} class TEXT_TEXTURE_OT_move_overlay(Operator): """Move image overlay up or down in the list""" bl_idname = "text_texture.move_overlay" bl_label = "Move Overlay" bl_description = "Move overlay up or down in the list" bl_options = {'REGISTER', 'UNDO'} overlay_index: IntProperty() direction: EnumProperty( items=[ ('UP', 'Up', 'Move overlay up in list'), ('DOWN', 'Down', 'Move overlay down in list') ] ) def execute(self, context): props = context.scene.text_texture_props print(f"[MOVE DEBUG] Moving overlay at index {self.overlay_index} direction: {self.direction}") if 0 <= self.overlay_index < len(props.image_overlays): current_index = self.overlay_index new_index = current_index if self.direction == 'UP' and current_index > 0: new_index = current_index - 1 elif self.direction == 'DOWN' and current_index < len(props.image_overlays) - 1: new_index = current_index + 1 if new_index != current_index: overlay_name = props.image_overlays[current_index].name props.image_overlays.move(current_index, new_index) props.active_overlay_index = new_index print(f"[MOVE DEBUG] Successfully moved overlay '{overlay_name}' from {current_index} to {new_index}") # Force preview update after moving (order affects rendering) try: generate_preview(context) print(f"[MOVE DEBUG] Preview updated after move operation") except Exception as e: print(f"[MOVE DEBUG] Error updating preview: {e}") self.report({'INFO'}, f"Moved overlay {self.direction.lower()}") else: print(f"[MOVE DEBUG] No movement needed - overlay is already at edge") self.report({'INFO'}, f"Overlay is already at {self.direction.lower()} edge") else: print(f"[MOVE DEBUG] Invalid overlay index: {self.overlay_index}") self.report({'ERROR'}, "Invalid overlay index") return {'CANCELLED'} return {'FINISHED'} # Export all operator classes for wildcard imports __all__ = [ 'TEXT_TEXTURE_OT_refresh_preview', 'TEXT_TEXTURE_OT_view_preview_zoom', 'TEXT_TEXTURE_OT_open_panel', 'TEXT_TEXTURE_OT_refresh_fonts', 'TEXT_TEXTURE_OT_refresh_presets', 'TEXT_TEXTURE_OT_add_overlay', 'TEXT_TEXTURE_OT_remove_overlay', 'TEXT_TEXTURE_OT_set_anchor_point', 'TEXT_TEXTURE_OT_sync_margins', 'TEXT_TEXTURE_OT_duplicate_overlay', 'TEXT_TEXTURE_OT_delete_overlay', 'TEXT_TEXTURE_OT_move_overlay', ]