Files
Text-Texture-Generator-for-…/src/operators/utility_ops.py
2025-09-11 14:07:55 +03:00

607 lines
26 KiB
Python

"""
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',
]