2303 lines
90 KiB
Python
2303 lines
90 KiB
Python
import bpy
|
|
import bmesh
|
|
from bpy.types import Operator, Panel, PropertyGroup, UIList
|
|
from bpy.props import StringProperty, IntProperty, FloatVectorProperty, FloatProperty, BoolProperty, EnumProperty, CollectionProperty, PointerProperty
|
|
import os
|
|
import tempfile
|
|
import subprocess
|
|
import sys
|
|
import json
|
|
import platform
|
|
import time
|
|
|
|
# Addon info
|
|
bl_info = {
|
|
"name": "Text Texture Generator",
|
|
"author": "Marc Mintel <marc@mintel.me>",
|
|
"version": (2, 3, 2),
|
|
"blender": (4, 0, 0),
|
|
"location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab | Properties > Material > Tool",
|
|
"description": "Generate image textures from text with accurate dimensions and instant live preview",
|
|
"category": "Material",
|
|
}
|
|
|
|
# ============================================================================
|
|
# UTILITY FUNCTIONS
|
|
# ============================================================================
|
|
|
|
def install_pillow():
|
|
"""Install Pillow if not available"""
|
|
try:
|
|
import PIL
|
|
except ImportError:
|
|
print("Installing Pillow...")
|
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "pillow>=10.0.0"])
|
|
|
|
def get_system_fonts():
|
|
"""Get detailed list of available system fonts with paths"""
|
|
try:
|
|
from PIL import ImageFont
|
|
import glob
|
|
|
|
fonts = {}
|
|
system = platform.system()
|
|
|
|
if system == "Windows":
|
|
font_dirs = [
|
|
"C:/Windows/Fonts/",
|
|
os.path.expanduser("~/AppData/Local/Microsoft/Windows/Fonts/")
|
|
]
|
|
extensions = ["*.ttf", "*.ttc", "*.otf"]
|
|
elif system == "Darwin": # macOS
|
|
font_dirs = [
|
|
"/System/Library/Fonts/",
|
|
"/Library/Fonts/",
|
|
os.path.expanduser("~/Library/Fonts/")
|
|
]
|
|
extensions = ["*.ttf", "*.ttc", "*.otf", "*.dfont"]
|
|
else: # Linux
|
|
font_dirs = [
|
|
"/usr/share/fonts/",
|
|
"/usr/local/share/fonts/",
|
|
os.path.expanduser("~/.local/share/fonts/"),
|
|
os.path.expanduser("~/.fonts/")
|
|
]
|
|
extensions = ["*.ttf", "*.ttc", "*.otf"]
|
|
|
|
for font_dir in font_dirs:
|
|
if os.path.exists(font_dir):
|
|
for ext in extensions:
|
|
for font_path in glob.glob(os.path.join(font_dir, "**", ext), recursive=True):
|
|
try:
|
|
font_name = os.path.splitext(os.path.basename(font_path))[0]
|
|
font_name = font_name.replace("-", " ").replace("_", " ")
|
|
fonts[font_name] = font_path
|
|
except Exception:
|
|
continue
|
|
|
|
return fonts
|
|
except ImportError:
|
|
return {}
|
|
|
|
def get_font_enum_items(self, context):
|
|
"""Dynamic enum items for font selection"""
|
|
items = [("default", "Default Font", "Use system default font", 0)]
|
|
|
|
if not hasattr(get_font_enum_items, 'cached_fonts'):
|
|
get_font_enum_items.cached_fonts = get_system_fonts()
|
|
|
|
fonts = get_font_enum_items.cached_fonts
|
|
|
|
for i, (font_name, font_path) in enumerate(sorted(fonts.items())[:50]):
|
|
items.append((font_path, font_name, f"Font: {font_name}", i + 1))
|
|
|
|
return items
|
|
|
|
def get_preset_path():
|
|
"""Get path for storing presets"""
|
|
addon_dir = os.path.dirname(os.path.realpath(__file__))
|
|
presets_dir = os.path.join(addon_dir, "presets")
|
|
if not os.path.exists(presets_dir):
|
|
try:
|
|
os.makedirs(presets_dir)
|
|
except OSError as e:
|
|
print(f"Error creating presets directory: {e}")
|
|
return presets_dir
|
|
|
|
def initialize_presets():
|
|
"""Initialize presets during addon registration - prevents race conditions"""
|
|
try:
|
|
import bpy
|
|
if not hasattr(bpy.context, 'scene') or not bpy.context.scene:
|
|
return
|
|
|
|
props = bpy.context.scene.text_texture_props
|
|
if not props:
|
|
return
|
|
|
|
# Clear existing presets
|
|
props.presets.clear()
|
|
|
|
preset_dir = get_preset_path()
|
|
if os.path.exists(preset_dir):
|
|
try:
|
|
for filename in os.listdir(preset_dir):
|
|
if filename.endswith('.json'):
|
|
preset_name = os.path.splitext(filename)[0]
|
|
preset = props.presets.add()
|
|
preset.name = preset_name
|
|
except OSError as e:
|
|
print(f"Error loading presets during initialization: {e}")
|
|
except Exception as e:
|
|
print(f"Error in initialize_presets: {e}")
|
|
|
|
# ============================================================================
|
|
# PREVIEW GENERATION FUNCTIONS
|
|
# ============================================================================
|
|
|
|
def test_font_mixed_case_support(font, test_size=20):
|
|
"""Test if a font supports mixed-case rendering properly"""
|
|
try:
|
|
from PIL import Image, ImageDraw
|
|
|
|
# Test with mixed-case sample text
|
|
test_text = "AaBbCc123"
|
|
test_img = Image.new('RGB', (100, 50), (255, 255, 255))
|
|
test_draw = ImageDraw.Draw(test_img)
|
|
|
|
# DIAGNOSTIC: Add detailed logging for custom font debugging
|
|
font_info = "UNKNOWN"
|
|
if hasattr(font, 'path'):
|
|
font_info = font.path
|
|
elif hasattr(font, 'getname'):
|
|
try:
|
|
font_info = font.getname()
|
|
except:
|
|
pass
|
|
print(f"CUSTOM FONT DEBUG: Testing mixed-case support for font: {font_info}")
|
|
|
|
# Try to get bounding box - this will fail if font doesn't support the characters
|
|
try:
|
|
bbox = test_draw.textbbox((0, 0), test_text, font=font)
|
|
print(f"CUSTOM FONT DEBUG: Mixed-case text bbox successful: {bbox}")
|
|
except Exception as bbox_error:
|
|
print(f"CUSTOM FONT DEBUG: Mixed-case text bbox FAILED: {bbox_error}")
|
|
import traceback
|
|
print(f"CUSTOM FONT DEBUG: Full exception trace: {traceback.format_exc()}")
|
|
return False
|
|
|
|
# Test actual rendering - draw the text and check if it renders properly
|
|
try:
|
|
test_draw.text((5, 5), test_text, fill=(0, 0, 0), font=font)
|
|
print(f"CUSTOM FONT DEBUG: Mixed-case text rendering successful")
|
|
except Exception as render_error:
|
|
print(f"CUSTOM FONT DEBUG: Mixed-case text rendering FAILED: {render_error}")
|
|
import traceback
|
|
print(f"CUSTOM FONT DEBUG: Full exception trace: {traceback.format_exc()}")
|
|
return False
|
|
|
|
# Additional validation: check if lowercase letters render differently from uppercase
|
|
try:
|
|
upper_bbox = test_draw.textbbox((0, 0), "ABC", font=font)
|
|
lower_bbox = test_draw.textbbox((0, 0), "abc", font=font)
|
|
print(f"CUSTOM FONT DEBUG: Upper bbox: {upper_bbox}, Lower bbox: {lower_bbox}")
|
|
|
|
# RELAXED VALIDATION: Allow fonts with identical bounding boxes
|
|
# Many valid custom fonts may have identical bounding boxes but still render mixed case properly
|
|
if upper_bbox == lower_bbox:
|
|
print(f"CUSTOM FONT DEBUG: RELAXED VALIDATION - Identical bounding boxes detected")
|
|
print(f"CUSTOM FONT DEBUG: Instead of rejecting, testing actual mixed-case rendering...")
|
|
|
|
# Additional test: try rendering actual mixed case text to verify it works
|
|
try:
|
|
mixed_test = "AaBbCcDd"
|
|
mixed_bbox = test_draw.textbbox((0, 0), mixed_test, font=font)
|
|
test_draw.text((10, 10), mixed_test, fill=(0, 0, 0), font=font)
|
|
print(f"CUSTOM FONT DEBUG: Mixed-case render test '{mixed_test}' PASSED: {mixed_bbox}")
|
|
print(f"CUSTOM FONT DEBUG: Font ACCEPTED despite identical bounding boxes (relaxed validation)")
|
|
return True
|
|
except Exception as mixed_render_error:
|
|
print(f"CUSTOM FONT DEBUG: Mixed-case render test FAILED: {mixed_render_error}")
|
|
print(f"CUSTOM FONT DEBUG: Font REJECTED due to actual rendering failure")
|
|
import traceback
|
|
print(f"CUSTOM FONT DEBUG: Full exception trace: {traceback.format_exc()}")
|
|
return False
|
|
|
|
print(f"CUSTOM FONT DEBUG: Font mixed-case test PASSED - supports both cases properly")
|
|
return True
|
|
except Exception as bbox_compare_error:
|
|
print(f"CUSTOM FONT DEBUG: Bounding box comparison FAILED: {bbox_compare_error}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
print(f"CUSTOM FONT DEBUG: Font mixed-case test FAILED with exception: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
def get_validated_font(font_path, font_size):
|
|
"""Load and validate a font for mixed-case support"""
|
|
try:
|
|
from PIL import ImageFont
|
|
|
|
# DIAGNOSTIC: Enhanced logging for custom font debugging
|
|
print(f"CUSTOM FONT DEBUG: get_validated_font() called with:")
|
|
print(f"CUSTOM FONT DEBUG: font_path: '{font_path}'")
|
|
print(f"CUSTOM FONT DEBUG: font_size: {font_size}")
|
|
print(f"CUSTOM FONT DEBUG: path exists: {os.path.exists(font_path)}")
|
|
|
|
if os.path.exists(font_path):
|
|
# Get detailed file information for diagnostics
|
|
try:
|
|
import stat
|
|
file_stats = os.stat(font_path)
|
|
print(f"CUSTOM FONT DEBUG: file size: {file_stats.st_size} bytes")
|
|
print(f"CUSTOM FONT DEBUG: file permissions: {stat.filemode(file_stats.st_mode)}")
|
|
print(f"CUSTOM FONT DEBUG: file extension: {os.path.splitext(font_path)[1]}")
|
|
except Exception as stats_error:
|
|
print(f"CUSTOM FONT DEBUG: could not get detailed file stats: {stats_error}")
|
|
|
|
if not os.path.exists(font_path):
|
|
error_msg = f"Font file not found: {font_path}"
|
|
print(f"CUSTOM FONT DEBUG: {error_msg}")
|
|
return None, error_msg
|
|
|
|
# Get file info for debugging
|
|
try:
|
|
file_stats = os.stat(font_path)
|
|
print(f"CUSTOM FONT DEBUG: file size: {file_stats.st_size} bytes")
|
|
print(f"CUSTOM FONT DEBUG: file extension: {os.path.splitext(font_path)[1]}")
|
|
except Exception as stat_error:
|
|
print(f"CUSTOM FONT DEBUG: could not get file stats: {stat_error}")
|
|
|
|
# Load the font
|
|
print(f"CUSTOM FONT DEBUG: Attempting to load font with ImageFont.truetype()...")
|
|
try:
|
|
font = ImageFont.truetype(font_path, font_size)
|
|
print(f"CUSTOM FONT DEBUG: Font loaded successfully: {font}")
|
|
except Exception as load_error:
|
|
error_msg = f"Font loading error - ImageFont.truetype() failed: {load_error}"
|
|
print(f"CUSTOM FONT DEBUG: {error_msg}")
|
|
import traceback
|
|
print(f"CUSTOM FONT DEBUG: Full exception trace: {traceback.format_exc()}")
|
|
return None, error_msg
|
|
|
|
# Test mixed-case support
|
|
print(f"CUSTOM FONT DEBUG: Testing mixed-case support...")
|
|
if test_font_mixed_case_support(font):
|
|
print(f"CUSTOM FONT DEBUG: Mixed-case validation PASSED")
|
|
return font, "OK"
|
|
else:
|
|
error_msg = f"Font does not support mixed-case properly: {font_path}"
|
|
print(f"CUSTOM FONT DEBUG: Mixed-case validation FAILED: {error_msg}")
|
|
return None, error_msg
|
|
|
|
except Exception as e:
|
|
error_msg = f"Font loading error (outer exception): {e}"
|
|
print(f"CUSTOM FONT DEBUG: {error_msg}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return None, error_msg
|
|
|
|
def get_system_fallback_fonts():
|
|
"""Get platform-specific fallback fonts known to support mixed-case"""
|
|
system = platform.system()
|
|
|
|
if system == "Windows":
|
|
# Windows fonts with guaranteed mixed-case support
|
|
return [
|
|
"C:/Windows/Fonts/arial.ttf", # Arial - excellent mixed-case
|
|
"C:/Windows/Fonts/calibri.ttf", # Calibri - modern, mixed-case
|
|
"C:/Windows/Fonts/segoeui.ttf", # Segoe UI - system font, mixed-case
|
|
"C:/Windows/Fonts/verdana.ttf", # Verdana - web-safe, mixed-case
|
|
"C:/Windows/Fonts/tahoma.ttf", # Tahoma - fallback mixed-case
|
|
"C:/Windows/Fonts/times.ttf", # Times - serif mixed-case
|
|
"C:/Windows/Fonts/trebuc.ttf" # Trebuchet MS - mixed-case
|
|
]
|
|
elif system == "Darwin": # macOS
|
|
# macOS fonts with guaranteed mixed-case support
|
|
return [
|
|
"/System/Library/Fonts/Helvetica.ttc", # Helvetica - classic mixed-case
|
|
"/Library/Fonts/Arial.ttf", # Arial - if available
|
|
"/System/Library/Fonts/Times.ttc", # Times - serif mixed-case
|
|
"/System/Library/Fonts/Avenir.ttc", # Avenir - modern mixed-case
|
|
"/System/Library/Fonts/Geneva.ttf", # Geneva - system mixed-case
|
|
"/System/Library/Fonts/Palatino.ttc" # Palatino - elegant mixed-case
|
|
]
|
|
else: # Linux
|
|
# Linux fonts with guaranteed mixed-case support
|
|
return [
|
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", # LibreOffice mixed-case
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", # DejaVu - comprehensive
|
|
"/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf", # Ubuntu - modern mixed-case
|
|
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", # Google Noto - universal
|
|
"/usr/share/fonts/truetype/lato/Lato-Regular.ttf", # Lato - web font mixed-case
|
|
"/usr/share/fonts/truetype/opensans/OpenSans-Regular.ttf" # Open Sans - mixed-case
|
|
]
|
|
|
|
# ============================================================================
|
|
# ENHANCED POSITIONING FUNCTIONS
|
|
# ============================================================================
|
|
|
|
def calculate_text_position(props, canvas_width, canvas_height, text_width, text_height):
|
|
"""Calculate text position based on enhanced positioning properties"""
|
|
|
|
if props.position_mode == 'MANUAL':
|
|
# Manual positioning mode
|
|
if props.manual_position_unit == 'PERCENTAGE':
|
|
x = int((props.manual_position_x / 100.0) * canvas_width)
|
|
y = int((props.manual_position_y / 100.0) * canvas_height)
|
|
else: # PIXELS
|
|
x = int(props.manual_position_x)
|
|
y = int(props.manual_position_y)
|
|
else:
|
|
# Preset anchor-based positioning
|
|
margin_x_px = int((props.margin_x / 100.0) * canvas_width)
|
|
margin_y_px = int((props.margin_y / 100.0) * canvas_height)
|
|
|
|
# Calculate base position based on anchor point
|
|
if 'LEFT' in props.anchor_point:
|
|
base_x = margin_x_px
|
|
elif 'RIGHT' in props.anchor_point:
|
|
base_x = canvas_width - text_width - margin_x_px
|
|
else: # CENTER
|
|
base_x = (canvas_width - text_width) // 2
|
|
|
|
if 'TOP' in props.anchor_point:
|
|
base_y = margin_y_px
|
|
elif 'BOTTOM' in props.anchor_point:
|
|
base_y = canvas_height - text_height - margin_y_px
|
|
else: # MIDDLE
|
|
base_y = (canvas_height - text_height) // 2
|
|
|
|
# Apply fine-tuning offsets
|
|
x = base_x + props.offset_x
|
|
y = base_y + props.offset_y
|
|
|
|
# Apply canvas constraints if enabled
|
|
if props.constrain_to_canvas:
|
|
x = max(0, min(x, canvas_width - text_width))
|
|
y = max(0, min(y, canvas_height - text_height))
|
|
|
|
return x, y
|
|
|
|
def sync_margin_values(props, changed_margin):
|
|
"""Synchronize margin values when linked"""
|
|
if props.margins_linked:
|
|
if changed_margin == 'x':
|
|
props.margin_y = props.margin_x
|
|
elif changed_margin == 'y':
|
|
props.margin_x = props.margin_y
|
|
|
|
def get_anchor_point_matrix():
|
|
"""Return 3x3 matrix of anchor point identifiers for UI layout"""
|
|
return [
|
|
['TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT'],
|
|
['MIDDLE_LEFT', 'MIDDLE_CENTER', 'MIDDLE_RIGHT'],
|
|
['BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT']
|
|
]
|
|
|
|
def generate_texture_image(props, width, height):
|
|
"""Generate the actual texture image with overlays - width and height are REQUIRED"""
|
|
try:
|
|
install_pillow()
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import math
|
|
|
|
# Use the exact dimensions provided
|
|
print(f"Generating PIL image at {width}x{height}")
|
|
|
|
# Ensure minimum dimensions
|
|
width = max(16, width)
|
|
height = max(16, height)
|
|
|
|
print(f"DEBUG: Generating texture - requested: {width}x{height}, texture settings: {props.texture_width}x{props.texture_height}")
|
|
|
|
# Scale font size proportionally for preview
|
|
scale_factor = width / props.texture_width
|
|
font_size_pixels = max(6, int(props.font_size * scale_factor))
|
|
padding = max(0, int(props.padding * scale_factor))
|
|
|
|
print(f"DEBUG: Scale factor: {scale_factor}, scaled font size: {font_size_pixels}, scaled padding: {padding}")
|
|
|
|
# Create base image with background
|
|
if props.background_transparent:
|
|
base_img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
|
else:
|
|
bg_color = tuple(int(c * 255) for c in props.background_color[:3])
|
|
if len(props.background_color) > 3:
|
|
bg_color += (int(props.background_color[3] * 255),)
|
|
base_img = Image.new('RGBA', (width, height), bg_color)
|
|
|
|
print(f"Base image created: {base_img.size}")
|
|
|
|
# Collect all elements with z-index (text is z-index 0)
|
|
elements = []
|
|
|
|
# Add image overlays
|
|
for overlay in props.image_overlays:
|
|
if overlay.enabled and overlay.image_path and os.path.exists(overlay.image_path):
|
|
try:
|
|
overlay_img = Image.open(overlay.image_path).convert('RGBA')
|
|
|
|
# Scale overlay
|
|
if overlay.scale != 1.0:
|
|
new_size = (int(overlay_img.width * overlay.scale * scale_factor),
|
|
int(overlay_img.height * overlay.scale * scale_factor))
|
|
overlay_img = overlay_img.resize(new_size, Image.LANCZOS)
|
|
|
|
# Rotate overlay if needed
|
|
if overlay.rotation != 0:
|
|
overlay_img = overlay_img.rotate(overlay.rotation, expand=True)
|
|
|
|
# Calculate position
|
|
x_pos = int(overlay.x_position * width - overlay_img.width / 2)
|
|
y_pos = int((1.0 - overlay.y_position) * height - overlay_img.height / 2) # Flip Y for intuitive top-to-bottom
|
|
|
|
elements.append((overlay.z_index, 'image', overlay_img, x_pos, y_pos))
|
|
print(f"Added overlay {overlay.name} at z-index {overlay.z_index}, pos ({x_pos}, {y_pos})")
|
|
|
|
except Exception as e:
|
|
print(f"Error loading overlay {overlay.name}: {e}")
|
|
|
|
# Create text element (z-index 0)
|
|
text_img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(text_img)
|
|
|
|
# Load font with enhanced mixed-case validation
|
|
font = None
|
|
font_loaded = False
|
|
|
|
print("DEBUG UPPERCASE FIX: Starting enhanced font loading with mixed-case validation")
|
|
|
|
# Try custom font first with validation
|
|
if props.use_custom_font and props.custom_font_path:
|
|
print(f"DEBUG UPPERCASE FIX: Testing custom font: {props.custom_font_path}")
|
|
font, error_msg = get_validated_font(props.custom_font_path, font_size_pixels)
|
|
if font:
|
|
font_loaded = True
|
|
print(f"SUCCESS: Loaded and validated custom font: {props.custom_font_path}")
|
|
else:
|
|
print(f"FAILED: Custom font validation failed: {error_msg}")
|
|
|
|
# Try selected system font with validation
|
|
if not font_loaded and props.font_path != "default":
|
|
print(f"DEBUG UPPERCASE FIX: Testing system font: {props.font_path}")
|
|
font, error_msg = get_validated_font(props.font_path, font_size_pixels)
|
|
if font:
|
|
font_loaded = True
|
|
print(f"SUCCESS: Loaded and validated system font: {props.font_path}")
|
|
else:
|
|
print(f"FAILED: System font validation failed: {error_msg}")
|
|
|
|
# Enhanced fallback with mixed-case validation
|
|
if not font_loaded:
|
|
print("DEBUG UPPERCASE FIX: Starting validated fallback font search...")
|
|
fallback_fonts = get_system_fallback_fonts()
|
|
|
|
for fallback_path in fallback_fonts:
|
|
print(f"DEBUG UPPERCASE FIX: Testing fallback font: {fallback_path}")
|
|
if os.path.exists(fallback_path):
|
|
font, error_msg = get_validated_font(fallback_path, font_size_pixels)
|
|
if font:
|
|
font_loaded = True
|
|
print(f"SUCCESS: Using validated fallback font: {fallback_path}")
|
|
break
|
|
else:
|
|
print(f"FAILED: Fallback font validation failed: {error_msg}")
|
|
else:
|
|
print(f"SKIP: Fallback font not found: {fallback_path}")
|
|
|
|
# Last resort - use default font but warn about mixed-case limitations
|
|
if not font_loaded:
|
|
font = ImageFont.load_default()
|
|
font_loaded = True
|
|
print("WARNING: Using default font - this may not support mixed-case properly!")
|
|
print("WARNING: Consider installing additional fonts for proper mixed-case support")
|
|
|
|
# Calculate text position
|
|
text = props.text if props.text and props.text.strip() else "Hello World"
|
|
|
|
# ENHANCED DIAGNOSTIC LOGGING for uppercase text issue
|
|
print(f"DEBUG UPPERCASE ISSUE - Original text input: '{props.text}'")
|
|
print(f"DEBUG UPPERCASE ISSUE - Processed text: '{text}'")
|
|
print(f"DEBUG UPPERCASE ISSUE - Text type: {type(text)}")
|
|
print(f"DEBUG UPPERCASE ISSUE - Has upper chars: {any(c.isupper() for c in text)}")
|
|
print(f"DEBUG UPPERCASE ISSUE - Has lower chars: {any(c.islower() for c in text)}")
|
|
print(f"DEBUG UPPERCASE ISSUE - Font being used: {font}")
|
|
if hasattr(font, 'path'):
|
|
print(f"DEBUG UPPERCASE ISSUE - Font path: {font.path}")
|
|
|
|
# Test the current font's mixed-case rendering capability
|
|
print("DEBUG UPPERCASE ISSUE - Testing current font mixed-case capability...")
|
|
if test_font_mixed_case_support(font):
|
|
print("SUCCESS: Current font supports mixed-case rendering properly!")
|
|
else:
|
|
print("WARNING: Current font may not support mixed-case rendering properly!")
|
|
print("This could be the cause of uppercase-only text rendering")
|
|
|
|
try:
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
text_width = max(1, bbox[2] - bbox[0])
|
|
text_height = max(1, bbox[3] - bbox[1])
|
|
except (OSError, ZeroDivisionError, ValueError) as e:
|
|
print(f"Error getting text bbox: {e}")
|
|
text_width = int(len(text) * font_size_pixels * 0.6)
|
|
text_height = font_size_pixels
|
|
|
|
# ============================================================================
|
|
# ENHANCED POSITIONING SYSTEM - Replaces hardcoded vertical centering
|
|
# ============================================================================
|
|
|
|
# Use enhanced positioning system
|
|
x, y = calculate_text_position(props, width, height, text_width, text_height)
|
|
|
|
# Apply legacy text_align for backward compatibility if using MIDDLE_CENTER anchor
|
|
if props.anchor_point == 'MIDDLE_CENTER' and props.position_mode == 'PRESET':
|
|
if props.text_align == 'center':
|
|
x = (width - text_width) // 2
|
|
elif props.text_align == 'right':
|
|
x = width - text_width - padding
|
|
elif props.text_align == 'left':
|
|
x = padding
|
|
|
|
# Apply offsets even in legacy mode
|
|
x += props.offset_x
|
|
y += props.offset_y
|
|
|
|
# Draw text
|
|
text_color = tuple(int(c * 255) for c in props.text_color[:3])
|
|
if len(props.text_color) > 3:
|
|
text_color += (int(props.text_color[3] * 255),)
|
|
|
|
print(f"DEBUG UPPERCASE ISSUE - About to draw text: '{text}' with font: {font}")
|
|
draw.text((x, y), text, fill=text_color, font=font)
|
|
print(f"Text drawn at ({x}, {y})")
|
|
print(f"DEBUG UPPERCASE ISSUE - Text drawing completed")
|
|
|
|
# Add text to elements list
|
|
elements.append((0, 'text', text_img, 0, 0))
|
|
|
|
# Sort elements by z-index
|
|
elements.sort(key=lambda x: x[0])
|
|
|
|
# Composite all elements
|
|
final_img = base_img.copy()
|
|
for z_index, element_type, element_img, pos_x, pos_y in elements:
|
|
if element_type == 'text':
|
|
final_img = Image.alpha_composite(final_img, element_img)
|
|
elif element_type == 'image':
|
|
# Create a temporary image to paste the overlay
|
|
temp_img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
|
temp_img.paste(element_img, (pos_x, pos_y), element_img)
|
|
final_img = Image.alpha_composite(final_img, temp_img)
|
|
|
|
print(f"Final composite image created with {len(elements)} elements")
|
|
return final_img
|
|
|
|
except Exception as e:
|
|
print(f"Error in generate_texture_image: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return None
|
|
|
|
def generate_preview(context):
|
|
"""Generate preview texture"""
|
|
if not context or not hasattr(context, 'scene'):
|
|
print("No context or scene")
|
|
return None
|
|
|
|
props = context.scene.text_texture_props
|
|
|
|
# Always generate preview - removed live_preview_enabled check
|
|
|
|
try:
|
|
# Generate at full texture size for 100% quality preview
|
|
preview_w = props.texture_width
|
|
preview_h = props.texture_height
|
|
|
|
print(f"[TTG] Generating 100% quality preview at full resolution: {preview_w}x{preview_h}")
|
|
|
|
print(f"Starting preview generation at {preview_w}x{preview_h} (full resolution)")
|
|
print(f"DEBUG: Texture size is {props.texture_width}x{props.texture_height}, preview at {preview_w}x{preview_h}")
|
|
|
|
# Generate the image
|
|
img = generate_texture_image(props, preview_w, preview_h)
|
|
if not img:
|
|
print("Failed to generate PIL image")
|
|
return None
|
|
|
|
# Apply preview background if needed
|
|
if props.preview_bg_type != 'transparent':
|
|
from PIL import Image, ImageDraw
|
|
|
|
# Create background image
|
|
bg_img = Image.new('RGBA', (preview_w, preview_h))
|
|
draw = ImageDraw.Draw(bg_img)
|
|
|
|
if props.preview_bg_type == 'color':
|
|
# Solid color background
|
|
bg_color = tuple(int(c * 255) for c in props.preview_bg_color[:3])
|
|
bg_color += (int(props.preview_bg_color[3] * 255),)
|
|
bg_img.paste(bg_color, (0, 0, preview_w, preview_h))
|
|
elif props.preview_bg_type == 'gradient':
|
|
# Gradient background
|
|
color1 = tuple(int(c * 255) for c in props.preview_bg_color[:3])
|
|
color2 = tuple(int(c * 255) for c in props.preview_bg_color2[:3])
|
|
|
|
for y in range(preview_h):
|
|
ratio = y / preview_h
|
|
r = int(color1[0] * (1 - ratio) + color2[0] * ratio)
|
|
g = int(color1[1] * (1 - ratio) + color2[1] * ratio)
|
|
b = int(color1[2] * (1 - ratio) + color2[2] * ratio)
|
|
draw.rectangle([(0, y), (preview_w, y+1)], fill=(r, g, b, 255))
|
|
else: # transparent - create checkerboard
|
|
# Checkerboard pattern for transparent
|
|
checker_size = 16
|
|
color1 = (128, 128, 128, 255)
|
|
color2 = (192, 192, 192, 255)
|
|
|
|
for y in range(0, preview_h, checker_size):
|
|
for x in range(0, preview_w, checker_size):
|
|
color = color1 if ((x // checker_size) + (y // checker_size)) % 2 == 0 else color2
|
|
draw.rectangle([(x, y), (min(x + checker_size, preview_w), min(y + checker_size, preview_h))],
|
|
fill=color)
|
|
|
|
# Composite the text image over the background
|
|
bg_img.alpha_composite(img)
|
|
img = bg_img
|
|
|
|
# Name for preview image
|
|
preview_img_name = "TextTexturePreview"
|
|
|
|
# Remove old preview if it exists
|
|
if preview_img_name in bpy.data.images:
|
|
print("Removing old preview")
|
|
old_img = bpy.data.images[preview_img_name]
|
|
bpy.data.images.remove(old_img)
|
|
|
|
# Create new Blender image for preview
|
|
print("Creating new Blender image")
|
|
blender_img = bpy.data.images.new(preview_img_name, preview_w, preview_h, alpha=True)
|
|
|
|
# Convert PIL to Blender
|
|
print("Converting pixels")
|
|
pixels = []
|
|
for y in range(preview_h):
|
|
for x in range(preview_w):
|
|
pixel = img.getpixel((x, preview_h - 1 - y))
|
|
if len(pixel) == 4:
|
|
r, g, b, a = pixel
|
|
else:
|
|
r, g, b = pixel
|
|
a = 255
|
|
pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0])
|
|
|
|
print("Setting pixels")
|
|
blender_img.pixels[:] = pixels
|
|
|
|
# Store reference to the preview image only if not in draw context
|
|
try:
|
|
props.preview_image = blender_img
|
|
print(f"Preview stored: {props.preview_image}")
|
|
except AttributeError as attr_err:
|
|
if "Writing to ID classes in this context is not allowed" in str(attr_err):
|
|
print("Cannot set preview_image during draw - will be set later")
|
|
else:
|
|
raise
|
|
|
|
# Force UI redraw
|
|
for area in context.screen.areas:
|
|
area.tag_redraw()
|
|
|
|
print(f"Preview generated successfully: {blender_img.name}")
|
|
return blender_img
|
|
|
|
except Exception as e:
|
|
print(f"Preview generation error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
try:
|
|
props.preview_image = None
|
|
except AttributeError as attr_err:
|
|
if "Writing to ID classes in this context is not allowed" not in str(attr_err):
|
|
raise
|
|
return None
|
|
|
|
# Removed update_preview_enabled function - preview is always enabled
|
|
|
|
def update_live_preview(self, context):
|
|
"""Update preview when properties change"""
|
|
if not hasattr(context, 'scene'):
|
|
return
|
|
|
|
props = context.scene.text_texture_props
|
|
|
|
print(f"DEBUG: update_live_preview called, is_updating: {props.is_updating}")
|
|
|
|
# Always update preview unless already updating
|
|
if not props.is_updating:
|
|
print("DEBUG: Triggering preview generation from property update")
|
|
# Use a timer to debounce rapid property changes for better performance
|
|
if hasattr(update_live_preview, '_timer'):
|
|
bpy.app.timers.unregister(update_live_preview._timer)
|
|
|
|
def delayed_preview():
|
|
generate_preview(context)
|
|
return None # Stop timer
|
|
|
|
update_live_preview._timer = delayed_preview
|
|
bpy.app.timers.register(delayed_preview, first_interval=0.1)
|
|
|
|
# ============================================================================
|
|
# PROPERTY GROUPS
|
|
# ============================================================================
|
|
|
|
class TEXT_TEXTURE_ImageOverlay(PropertyGroup):
|
|
"""Single image overlay item"""
|
|
name: StringProperty(
|
|
name="Image Name",
|
|
description="Name identifier for this image overlay",
|
|
default="Overlay",
|
|
update=update_live_preview
|
|
)
|
|
|
|
image_path: StringProperty(
|
|
name="Image File",
|
|
description="Path to image file",
|
|
subtype='FILE_PATH',
|
|
default="",
|
|
update=update_live_preview
|
|
)
|
|
|
|
x_position: FloatProperty(
|
|
name="X Position",
|
|
description="X position (0.0 = left, 1.0 = right)",
|
|
default=0.5,
|
|
min=0.0,
|
|
max=1.0,
|
|
update=update_live_preview
|
|
)
|
|
|
|
y_position: FloatProperty(
|
|
name="Y Position",
|
|
description="Y position (0.0 = bottom, 1.0 = top)",
|
|
default=0.5,
|
|
min=0.0,
|
|
max=1.0,
|
|
update=update_live_preview
|
|
)
|
|
|
|
scale: FloatProperty(
|
|
name="Scale",
|
|
description="Scale of the image",
|
|
default=1.0,
|
|
min=0.1,
|
|
max=5.0,
|
|
update=update_live_preview
|
|
)
|
|
|
|
rotation: FloatProperty(
|
|
name="Rotation",
|
|
description="Rotation in degrees",
|
|
default=0.0,
|
|
min=-360.0,
|
|
max=360.0,
|
|
update=update_live_preview
|
|
)
|
|
|
|
z_index: IntProperty(
|
|
name="Z-Index",
|
|
description="Layer order (0=text level, -1=below text, 1+=above text)",
|
|
default=1,
|
|
min=-10,
|
|
max=10,
|
|
update=update_live_preview
|
|
)
|
|
|
|
enabled: BoolProperty(
|
|
name="Enabled",
|
|
description="Enable this image overlay",
|
|
default=True,
|
|
update=update_live_preview
|
|
)
|
|
|
|
class TEXT_TEXTURE_Preset(PropertyGroup):
|
|
"""Single preset item"""
|
|
name: StringProperty(
|
|
name="Preset Name",
|
|
description="Name of the preset",
|
|
default="New Preset"
|
|
)
|
|
|
|
class TEXT_TEXTURE_Properties(PropertyGroup):
|
|
"""Properties for text texture generation"""
|
|
|
|
is_updating: BoolProperty(default=False)
|
|
|
|
preview_image: PointerProperty(
|
|
type=bpy.types.Image,
|
|
name="Preview Image"
|
|
)
|
|
|
|
# Collapsible section expand/collapse states
|
|
expand_image_overlays: BoolProperty(
|
|
name="Image Overlays",
|
|
description="Show/hide Image Overlays section",
|
|
default=False
|
|
)
|
|
|
|
expand_positioning: BoolProperty(
|
|
name="Positioning & Layout",
|
|
description="Show/hide Positioning & Layout section",
|
|
default=False
|
|
)
|
|
|
|
expand_font_settings: BoolProperty(
|
|
name="Font Settings",
|
|
description="Show/hide Font Settings section",
|
|
default=False
|
|
)
|
|
|
|
expand_preview_options: BoolProperty(
|
|
name="Preview Options",
|
|
description="Show/hide Preview Options section",
|
|
default=False
|
|
)
|
|
|
|
expand_presets: BoolProperty(
|
|
name="Presets",
|
|
description="Show/hide Presets section",
|
|
default=False
|
|
)
|
|
|
|
text: StringProperty(
|
|
name="Text",
|
|
description="Text to render as texture",
|
|
default="Hello World",
|
|
maxlen=1024,
|
|
update=update_live_preview
|
|
)
|
|
|
|
texture_width: IntProperty(
|
|
name="Width",
|
|
description="Texture width in pixels",
|
|
default=1024,
|
|
min=64,
|
|
max=4096,
|
|
update=update_live_preview
|
|
)
|
|
|
|
texture_height: IntProperty(
|
|
name="Height",
|
|
description="Texture height in pixels",
|
|
default=1024,
|
|
min=64,
|
|
max=4096,
|
|
update=update_live_preview
|
|
)
|
|
|
|
font_size: IntProperty(
|
|
name="Font Size",
|
|
description="Font size in pixels",
|
|
default=96,
|
|
min=8,
|
|
max=500,
|
|
update=update_live_preview
|
|
)
|
|
|
|
padding: IntProperty(
|
|
name="Padding",
|
|
description="Padding around text in pixels",
|
|
default=20,
|
|
min=0,
|
|
max=200,
|
|
update=update_live_preview
|
|
)
|
|
|
|
text_color: FloatVectorProperty(
|
|
name="Text Color",
|
|
description="Color of the text",
|
|
subtype='COLOR',
|
|
default=(1.0, 1.0, 1.0, 1.0),
|
|
size=4,
|
|
min=0.0,
|
|
max=1.0,
|
|
update=update_live_preview
|
|
)
|
|
|
|
background_transparent: BoolProperty(
|
|
name="Transparent Background",
|
|
description="Use transparent background",
|
|
default=True,
|
|
update=update_live_preview
|
|
)
|
|
|
|
background_color: FloatVectorProperty(
|
|
name="Background Color",
|
|
description="Background color (when not transparent)",
|
|
subtype='COLOR',
|
|
default=(0.0, 0.0, 0.0, 1.0),
|
|
size=4,
|
|
min=0.0,
|
|
max=1.0,
|
|
update=update_live_preview
|
|
)
|
|
|
|
font_path: EnumProperty(
|
|
name="Font",
|
|
description="Font to use for text rendering",
|
|
items=get_font_enum_items,
|
|
update=update_live_preview
|
|
)
|
|
|
|
custom_font_path: StringProperty(
|
|
name="Custom Font File",
|
|
description="Path to custom font file (.ttf, .otf)",
|
|
subtype='FILE_PATH',
|
|
default="",
|
|
update=update_live_preview
|
|
)
|
|
|
|
use_custom_font: BoolProperty(
|
|
name="Use Custom Font",
|
|
description="Use custom font file instead of system fonts",
|
|
default=False,
|
|
update=update_live_preview
|
|
)
|
|
|
|
text_align: EnumProperty(
|
|
name="Alignment",
|
|
description="Text alignment",
|
|
items=[
|
|
('left', 'Left', 'Align text to the left'),
|
|
('center', 'Center', 'Center text'),
|
|
('right', 'Right', 'Align text to the right'),
|
|
],
|
|
default='center',
|
|
update=update_live_preview
|
|
)
|
|
|
|
# ============================================================================
|
|
# ENHANCED POSITIONING & ALIGNMENT PROPERTIES
|
|
# ============================================================================
|
|
|
|
# 9-point anchor system
|
|
anchor_point: EnumProperty(
|
|
name="Anchor Point",
|
|
description="Text anchor position on canvas",
|
|
items=[
|
|
('TOP_LEFT', 'Top Left', 'Position text from top-left corner'),
|
|
('TOP_CENTER', 'Top Center', 'Position text from top center'),
|
|
('TOP_RIGHT', 'Top Right', 'Position text from top-right corner'),
|
|
('MIDDLE_LEFT', 'Middle Left', 'Position text from middle-left'),
|
|
('MIDDLE_CENTER', 'Middle Center', 'Position text from center (default)'),
|
|
('MIDDLE_RIGHT', 'Middle Right', 'Position text from middle-right'),
|
|
('BOTTOM_LEFT', 'Bottom Left', 'Position text from bottom-left corner'),
|
|
('BOTTOM_CENTER', 'Bottom Center', 'Position text from bottom center'),
|
|
('BOTTOM_RIGHT', 'Bottom Right', 'Position text from bottom-right corner'),
|
|
],
|
|
default='MIDDLE_CENTER',
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Position mode toggle
|
|
position_mode: EnumProperty(
|
|
name="Position Mode",
|
|
description="Choose between preset anchor positions or manual positioning",
|
|
items=[
|
|
('PRESET', 'Preset', 'Use anchor points with margins and offsets'),
|
|
('MANUAL', 'Manual', 'Manually specify X,Y coordinates'),
|
|
],
|
|
default='PRESET',
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Percentage-based margins (0-50%)
|
|
margin_x: FloatProperty(
|
|
name="X Margin",
|
|
description="Horizontal margin from edge as percentage (0-50%)",
|
|
subtype='PERCENTAGE',
|
|
default=0.0,
|
|
min=0.0,
|
|
max=50.0,
|
|
precision=1,
|
|
update=update_live_preview
|
|
)
|
|
|
|
margin_y: FloatProperty(
|
|
name="Y Margin",
|
|
description="Vertical margin from edge as percentage (0-50%)",
|
|
subtype='PERCENTAGE',
|
|
default=0.0,
|
|
min=0.0,
|
|
max=50.0,
|
|
precision=1,
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Link margins option
|
|
margins_linked: BoolProperty(
|
|
name="Link Margins",
|
|
description="Keep X and Y margins synchronized",
|
|
default=False,
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Pixel offset fine-tuning (-200 to +200px)
|
|
offset_x: IntProperty(
|
|
name="X Offset",
|
|
description="Fine-tune X position in pixels (-200 to +200)",
|
|
default=0,
|
|
min=-200,
|
|
max=200,
|
|
update=update_live_preview
|
|
)
|
|
|
|
offset_y: IntProperty(
|
|
name="Y Offset",
|
|
description="Fine-tune Y position in pixels (-200 to +200)",
|
|
default=0,
|
|
min=-200,
|
|
max=200,
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Manual positioning properties
|
|
manual_position_x: FloatProperty(
|
|
name="Manual X",
|
|
description="Manual X position as percentage (0-100%)",
|
|
subtype='PERCENTAGE',
|
|
default=50.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
update=update_live_preview
|
|
)
|
|
|
|
manual_position_y: FloatProperty(
|
|
name="Manual Y",
|
|
description="Manual Y position as percentage (0-100%)",
|
|
subtype='PERCENTAGE',
|
|
default=50.0,
|
|
min=0.0,
|
|
max=100.0,
|
|
precision=1,
|
|
update=update_live_preview
|
|
)
|
|
|
|
manual_position_unit: EnumProperty(
|
|
name="Position Unit",
|
|
description="Units for manual positioning",
|
|
items=[
|
|
('PERCENTAGE', 'Percentage', 'Position as percentage of canvas'),
|
|
('PIXELS', 'Pixels', 'Position in absolute pixels'),
|
|
],
|
|
default='PERCENTAGE',
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Canvas constraint option
|
|
constrain_to_canvas: BoolProperty(
|
|
name="Constrain to Canvas",
|
|
description="Keep text within canvas bounds",
|
|
default=True,
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Visual feedback options
|
|
show_position_guides: BoolProperty(
|
|
name="Show Position Guides",
|
|
description="Display positioning guide lines in preview",
|
|
default=False,
|
|
update=update_live_preview
|
|
)
|
|
|
|
show_text_bounds: BoolProperty(
|
|
name="Show Text Bounds",
|
|
description="Display text bounding box in preview",
|
|
default=False,
|
|
update=update_live_preview
|
|
)
|
|
|
|
show_canvas_grid: BoolProperty(
|
|
name="Show Canvas Grid",
|
|
description="Display alignment grid overlay in preview",
|
|
default=False,
|
|
update=update_live_preview
|
|
)
|
|
|
|
grid_density: IntProperty(
|
|
name="Grid Density",
|
|
description="Number of grid divisions (4-16)",
|
|
default=8,
|
|
min=4,
|
|
max=16,
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Removed live_preview_enabled - preview is always on
|
|
|
|
preview_size: IntProperty(
|
|
name="Preview Size",
|
|
description="Size of the preview image (does not affect final output)",
|
|
default=512,
|
|
min=128,
|
|
max=2048,
|
|
update=update_live_preview
|
|
)
|
|
|
|
preview_zoom_level: FloatProperty(
|
|
name="Zoom",
|
|
description="Zoom level for preview",
|
|
default=1.0,
|
|
min=0.5,
|
|
max=4.0,
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Preview background options
|
|
preview_bg_type: EnumProperty(
|
|
name="Preview Background",
|
|
description="Background type for preview display",
|
|
items=[
|
|
('transparent', 'Transparent', 'Transparent checkerboard pattern'),
|
|
('color', 'Solid Color', 'Use a solid color background'),
|
|
('gradient', 'Gradient', 'Use a gradient background'),
|
|
],
|
|
default='transparent',
|
|
update=update_live_preview
|
|
)
|
|
|
|
preview_bg_color: FloatVectorProperty(
|
|
name="Preview BG Color",
|
|
description="Background color for preview",
|
|
subtype='COLOR',
|
|
default=(0.2, 0.2, 0.2, 1.0),
|
|
size=4,
|
|
min=0.0,
|
|
max=1.0,
|
|
update=update_live_preview
|
|
)
|
|
|
|
preview_bg_color2: FloatVectorProperty(
|
|
name="Preview BG Color 2",
|
|
description="Second color for gradient background",
|
|
subtype='COLOR',
|
|
default=(0.4, 0.4, 0.4, 1.0),
|
|
size=4,
|
|
min=0.0,
|
|
max=1.0,
|
|
update=update_live_preview
|
|
)
|
|
|
|
# Image overlays
|
|
image_overlays: CollectionProperty(type=TEXT_TEXTURE_ImageOverlay)
|
|
active_overlay_index: IntProperty(default=0)
|
|
|
|
# Presets
|
|
presets: CollectionProperty(type=TEXT_TEXTURE_Preset)
|
|
active_preset_index: IntProperty(default=0)
|
|
|
|
new_preset_name: StringProperty(
|
|
name="Preset Name",
|
|
description="Name for new preset (Press Enter to save)",
|
|
default="New Preset"
|
|
)
|
|
|
|
# Property to track when user wants to save with Enter
|
|
preset_save_requested: BoolProperty(
|
|
name="Save Requested",
|
|
description="Internal flag for Enter key save requests",
|
|
default=False
|
|
)
|
|
|
|
# ============================================================================
|
|
# OPERATORS
|
|
# ============================================================================
|
|
|
|
class TEXT_TEXTURE_OT_generate(Operator):
|
|
"""Generate final texture and pack it"""
|
|
bl_idname = "text_texture.generate"
|
|
bl_label = "Generate Text Texture"
|
|
bl_description = "Generate final high-quality texture and pack it into the blend file"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
try:
|
|
props = context.scene.text_texture_props
|
|
|
|
# IMPORTANT: Generate at EXACT user-specified dimensions
|
|
final_width = props.texture_width
|
|
final_height = props.texture_height
|
|
|
|
print(f"Generating texture at {final_width}x{final_height}px")
|
|
|
|
# Generate at FULL resolution specified by user
|
|
img = generate_texture_image(props, final_width, final_height)
|
|
|
|
# Create final name
|
|
img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}"
|
|
|
|
# Remove existing
|
|
if img_name in bpy.data.images:
|
|
bpy.data.images.remove(bpy.data.images[img_name])
|
|
|
|
# Create new image with USER SPECIFIED dimensions
|
|
blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True)
|
|
|
|
# Convert PIL to Blender
|
|
pixels = []
|
|
for y in range(final_height):
|
|
for x in range(final_width):
|
|
r, g, b, a = img.getpixel((x, final_height - 1 - y))
|
|
pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0])
|
|
|
|
blender_img.pixels = pixels
|
|
blender_img.pack() # PACK FINAL IMAGE
|
|
|
|
# Add to shader
|
|
if context.space_data and context.space_data.type == 'NODE_EDITOR':
|
|
if context.space_data.tree_type == 'ShaderNodeTree':
|
|
material = context.space_data.id
|
|
if material and material.use_nodes:
|
|
nodes = material.node_tree.nodes
|
|
img_node = nodes.new(type='ShaderNodeTexImage')
|
|
img_node.image = blender_img
|
|
img_node.location = (0, 0)
|
|
img_node.select = True
|
|
nodes.active = img_node
|
|
|
|
self.report({'INFO'}, f"Generated and packed: {img_name} ({final_width}x{final_height}px)")
|
|
return {'FINISHED'}
|
|
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Failed: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
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_save_preset(Operator):
|
|
"""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
|
|
)
|
|
|
|
def execute(self, context):
|
|
props = context.scene.text_texture_props
|
|
|
|
# Validate preset name
|
|
preset_name = props.new_preset_name.strip()
|
|
if not preset_name:
|
|
self.report({'ERROR'}, "Please enter a preset name")
|
|
return {'CANCELLED'}
|
|
|
|
# Validate preset name characters (avoid filesystem issues)
|
|
import re
|
|
if not re.match(r'^[a-zA-Z0-9_\-\s]+$', preset_name):
|
|
self.report({'ERROR'}, "Preset name can only contain letters, numbers, spaces, hyphens, and underscores")
|
|
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:
|
|
# 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")):
|
|
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'}
|
|
|
|
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,
|
|
'image_overlays': []
|
|
}
|
|
|
|
# Save overlay data
|
|
for overlay in props.image_overlays:
|
|
overlay_data = {
|
|
'name': overlay.name,
|
|
'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:
|
|
with open(preset_file, 'w') as f:
|
|
json.dump(preset_data, f, indent=2)
|
|
|
|
# Check if preset already exists in the collection and update it
|
|
existing_preset = None
|
|
for preset in props.presets:
|
|
if preset.name == preset_name:
|
|
existing_preset = preset
|
|
break
|
|
|
|
if not existing_preset:
|
|
preset = props.presets.add()
|
|
preset.name = preset_name
|
|
|
|
# Clear the input field
|
|
props.new_preset_name = "New Preset"
|
|
|
|
# Provide clear feedback
|
|
if self.overwrite:
|
|
self.report({'INFO'}, f"✅ Preset '{preset_name}' updated successfully")
|
|
else:
|
|
self.report({'INFO'}, f"✅ Preset '{preset_name}' saved successfully")
|
|
|
|
except (OSError, IOError, json.JSONEncodeError) as e:
|
|
self.report({'ERROR'}, f"Failed to save preset: {str(e)}")
|
|
return {'CANCELLED'}
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Unexpected error saving preset: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
# 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":
|
|
preset_file = os.path.join(get_preset_path(), f"{preset_name}.json")
|
|
if os.path.exists(preset_file):
|
|
return context.window_manager.invoke_confirm(self, event)
|
|
|
|
return self.execute(context)
|
|
|
|
def draw(self, context):
|
|
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):
|
|
"""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):
|
|
# Delegate to the main save preset operator
|
|
return bpy.ops.text_texture.save_preset('INVOKE_DEFAULT')
|
|
|
|
class TEXT_TEXTURE_OT_load_preset(Operator):
|
|
"""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):
|
|
props = context.scene.text_texture_props
|
|
|
|
try:
|
|
preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json")
|
|
if not os.path.exists(preset_file):
|
|
self.report({'ERROR'}, f"Preset file not found: {self.preset_name}")
|
|
return {'CANCELLED'}
|
|
|
|
with open(preset_file, 'r') as f:
|
|
preset_data = json.load(f)
|
|
|
|
props.is_updating = True
|
|
|
|
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)
|
|
|
|
# Load overlay data
|
|
props.image_overlays.clear()
|
|
overlay_data_list = preset_data.get('image_overlays', [])
|
|
for overlay_data in overlay_data_list:
|
|
overlay = props.image_overlays.add()
|
|
overlay.name = overlay_data.get('name', 'Overlay')
|
|
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
|
|
# Always generate preview after loading preset
|
|
generate_preview(context)
|
|
|
|
self.report({'INFO'}, f"Loaded: {self.preset_name}")
|
|
|
|
except (OSError, IOError) as e:
|
|
props.is_updating = False
|
|
self.report({'ERROR'}, f"Failed to read preset file: {str(e)}")
|
|
return {'CANCELLED'}
|
|
except json.JSONDecodeError as e:
|
|
props.is_updating = False
|
|
self.report({'ERROR'}, f"Invalid preset file format: {str(e)}")
|
|
return {'CANCELLED'}
|
|
except Exception as e:
|
|
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):
|
|
"""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):
|
|
props = context.scene.text_texture_props
|
|
|
|
try:
|
|
preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json")
|
|
if os.path.exists(preset_file):
|
|
os.remove(preset_file)
|
|
else:
|
|
self.report({'WARNING'}, f"Preset file not found: {self.preset_name}")
|
|
|
|
for i, preset in enumerate(props.presets):
|
|
if preset.name == self.preset_name:
|
|
props.presets.remove(i)
|
|
break
|
|
|
|
self.report({'INFO'}, f"🗑️ Deleted preset: {self.preset_name}")
|
|
|
|
except (OSError, IOError) as e:
|
|
self.report({'ERROR'}, f"Failed to delete preset file: {str(e)}")
|
|
return {'CANCELLED'}
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Unexpected error deleting preset: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
# Add confirmation dialog for better UX
|
|
return context.window_manager.invoke_confirm(self, event)
|
|
|
|
def draw(self, context):
|
|
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_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_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'}
|
|
|
|
|
|
# ============================================================================
|
|
# SHARED UI COMPONENTS
|
|
# ============================================================================
|
|
|
|
def draw_collapsible_header(layout, prop_name, text, icon='TRIA_DOWN'):
|
|
"""Draw a collapsible section header with expand/collapse arrow"""
|
|
props = bpy.context.scene.text_texture_props
|
|
expanded = getattr(props, prop_name)
|
|
|
|
box = layout.box()
|
|
header_row = box.row()
|
|
|
|
# Draw arrow icon and toggle expanded state
|
|
arrow_icon = 'TRIA_DOWN' if expanded else 'TRIA_RIGHT'
|
|
header_row.prop(props, prop_name, text="", icon=arrow_icon, emboss=False)
|
|
header_row.label(text=text, icon=icon)
|
|
|
|
return box if expanded else None
|
|
|
|
def draw_text_input_section(layout, props):
|
|
"""Draw the main text input field"""
|
|
layout.prop(props, "text", text="")
|
|
|
|
def draw_font_dropdown_section(layout, props):
|
|
"""Draw basic font dropdown for top level"""
|
|
row = layout.row(align=True)
|
|
row.prop(props, "font_path", text="")
|
|
row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH')
|
|
|
|
def draw_generate_button(layout):
|
|
"""Draw the main generate button"""
|
|
row = layout.row()
|
|
row.scale_y = 1.5
|
|
row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA')
|
|
|
|
def draw_image_overlays_section(layout, props):
|
|
"""Draw image overlays controls"""
|
|
# Add/Remove buttons
|
|
row = layout.row(align=True)
|
|
row.operator("text_texture.add_overlay", text="Add Image Overlay", icon='ADD')
|
|
if props.image_overlays:
|
|
row.operator("text_texture.remove_overlay", text="Remove", icon='REMOVE')
|
|
|
|
# Overlay list
|
|
if props.image_overlays:
|
|
layout.separator()
|
|
for i, overlay in enumerate(props.image_overlays):
|
|
overlay_row = layout.row()
|
|
overlay_row.prop(overlay, "enabled", text="")
|
|
overlay_row.prop(overlay, "name", text="")
|
|
|
|
if overlay.enabled:
|
|
layout.prop(overlay, "image_path", text="Image File")
|
|
|
|
pos_row = layout.row(align=True)
|
|
pos_row.prop(overlay, "x_position", text="X")
|
|
pos_row.prop(overlay, "y_position", text="Y")
|
|
|
|
trans_row = layout.row(align=True)
|
|
trans_row.prop(overlay, "scale", text="Scale")
|
|
trans_row.prop(overlay, "rotation", text="Rotation")
|
|
trans_row.prop(overlay, "z_index", text="Z-Level")
|
|
|
|
def draw_positioning_section(layout, props):
|
|
"""Draw positioning and alignment controls"""
|
|
# Dimensions moved here from top level
|
|
layout.label(text="Dimensions:", icon='SETTINGS')
|
|
row = layout.row(align=True)
|
|
row.prop(props, "texture_width", text="Width")
|
|
row.prop(props, "texture_height", text="Height")
|
|
|
|
layout.separator()
|
|
layout.prop(props, "position_mode", text="Mode")
|
|
|
|
if props.position_mode == 'PRESET':
|
|
layout.separator()
|
|
layout.label(text="Anchor Point:", icon='ANCHOR_TOP')
|
|
|
|
anchor_matrix = get_anchor_point_matrix()
|
|
for row_idx, row in enumerate(anchor_matrix):
|
|
grid_row = layout.row(align=True)
|
|
grid_row.scale_y = 1.2
|
|
for col_idx, anchor in enumerate(row):
|
|
if anchor == props.anchor_point:
|
|
op = grid_row.operator("text_texture.set_anchor_point", text="●", depress=True)
|
|
else:
|
|
op = grid_row.operator("text_texture.set_anchor_point", text="○")
|
|
op.anchor_point = anchor
|
|
|
|
# Margins
|
|
layout.separator()
|
|
margin_row = layout.row(align=True)
|
|
margin_row.label(text="Margins:")
|
|
margin_row.prop(props, "margins_linked", text="", icon='LINKED' if props.margins_linked else 'UNLINKED', toggle=True)
|
|
|
|
margins_row = layout.row(align=True)
|
|
margins_row.prop(props, "margin_x", text="X")
|
|
if not props.margins_linked:
|
|
margins_row.prop(props, "margin_y", text="Y")
|
|
|
|
# Offsets
|
|
layout.separator()
|
|
layout.label(text="Fine-tune (pixels):")
|
|
offset_row = layout.row(align=True)
|
|
offset_row.prop(props, "offset_x", text="X")
|
|
offset_row.prop(props, "offset_y", text="Y")
|
|
|
|
else: # MANUAL mode
|
|
layout.separator()
|
|
layout.prop(props, "manual_position_unit", text="Unit")
|
|
manual_row = layout.row(align=True)
|
|
manual_row.prop(props, "manual_position_x", text="X")
|
|
manual_row.prop(props, "manual_position_y", text="Y")
|
|
|
|
layout.separator()
|
|
layout.prop(props, "constrain_to_canvas")
|
|
|
|
def draw_font_settings_section(layout, props):
|
|
"""Draw extended font settings"""
|
|
# Font size moved here from top level
|
|
layout.prop(props, "font_size")
|
|
|
|
layout.separator()
|
|
layout.label(text="Font Selection:")
|
|
# Default font dropdown moved here from top level
|
|
row = layout.row(align=True)
|
|
row.prop(props, "font_path", text="")
|
|
row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH')
|
|
|
|
layout.separator()
|
|
# Custom font controls moved here from top level
|
|
layout.prop(props, "use_custom_font")
|
|
if props.use_custom_font:
|
|
layout.prop(props, "custom_font_path", text="")
|
|
|
|
layout.separator()
|
|
row = layout.row(align=True)
|
|
row.prop(props, "padding")
|
|
row.prop(props, "text_align")
|
|
|
|
layout.separator()
|
|
layout.label(text="Colors:")
|
|
layout.prop(props, "text_color")
|
|
layout.prop(props, "background_transparent")
|
|
if not props.background_transparent:
|
|
layout.prop(props, "background_color")
|
|
|
|
def draw_preview_options_section(layout, props):
|
|
"""Draw preview options"""
|
|
layout.prop(props, "preview_size", text="Preview Size")
|
|
layout.prop(props, "preview_bg_type", text="Background")
|
|
if props.preview_bg_type == 'color':
|
|
layout.prop(props, "preview_bg_color", text="Background Color")
|
|
elif props.preview_bg_type == 'gradient':
|
|
layout.prop(props, "preview_bg_color", text="Color 1")
|
|
layout.prop(props, "preview_bg_color2", text="Color 2")
|
|
|
|
def draw_presets_section(layout, props):
|
|
"""Draw 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')
|
|
|
|
# Status info
|
|
if props.new_preset_name and props.new_preset_name.strip() != "New Preset":
|
|
preset_file = os.path.join(get_preset_path(), f"{props.new_preset_name.strip()}.json")
|
|
if os.path.exists(preset_file):
|
|
status_row = layout.row()
|
|
status_row.label(text="⚠️ Preset exists - will prompt to overwrite", icon='INFO')
|
|
|
|
layout.separator()
|
|
|
|
# Load presets section
|
|
if props.presets:
|
|
layout.label(text=f"📁 Saved Presets ({len(props.presets)}):", icon='PRESET')
|
|
|
|
for i, preset in enumerate(props.presets):
|
|
row = layout.row()
|
|
load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}")
|
|
load_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')
|
|
|
|
# ============================================================================
|
|
# MENUS
|
|
# ============================================================================
|
|
|
|
def menu_func_node_editor(self, context):
|
|
"""Add menu item to Node Editor"""
|
|
self.layout.separator()
|
|
self.layout.label(text="Text Texture Generator", icon='TEXT')
|
|
self.layout.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA')
|
|
|
|
def menu_func_view3d(self, context):
|
|
"""Add menu item to 3D View"""
|
|
self.layout.separator()
|
|
self.layout.operator("text_texture.generate", text="Generate Text Texture", icon='TEXT')
|
|
|
|
def menu_func_image(self, context):
|
|
"""Add to Image menu"""
|
|
self.layout.separator()
|
|
self.layout.operator("text_texture.generate", text="Generate Text Texture", icon='TEXT')
|
|
|
|
# ============================================================================
|
|
# UI PANELS
|
|
# ============================================================================
|
|
|
|
class TEXT_TEXTURE_PT_panel(Panel):
|
|
"""Main Panel in Shader Editor"""
|
|
bl_label = "Text Texture Generator"
|
|
bl_idname = "TEXT_TEXTURE_PT_panel"
|
|
bl_space_type = 'NODE_EDITOR'
|
|
bl_region_type = 'UI'
|
|
bl_category = "Tool"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
# DEBUG: Log poll method calls
|
|
print(f"[TTG DEBUG] Panel poll called - space_data: {context.space_data}")
|
|
print(f"[TTG DEBUG] space_data.type: {context.space_data.type if context.space_data else 'None'}")
|
|
if hasattr(context.space_data, 'tree_type'):
|
|
print(f"[TTG DEBUG] tree_type: {context.space_data.tree_type}")
|
|
else:
|
|
print("[TTG DEBUG] No tree_type attribute")
|
|
|
|
# More permissive poll condition to ensure panel shows up
|
|
if context.space_data.type == 'NODE_EDITOR':
|
|
if hasattr(context.space_data, 'tree_type'):
|
|
result = context.space_data.tree_type == 'ShaderNodeTree'
|
|
print(f"[TTG DEBUG] Poll result: {result}")
|
|
return result
|
|
else:
|
|
# Allow panel even without tree_type (sometimes happens)
|
|
print("[TTG DEBUG] Poll result: True (no tree_type but NODE_EDITOR)")
|
|
return True
|
|
|
|
print("[TTG DEBUG] Poll result: False (not NODE_EDITOR)")
|
|
return False
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
props = context.scene.text_texture_props
|
|
|
|
# TOP LEVEL - Always Visible Controls
|
|
|
|
# Text Input
|
|
box = layout.box()
|
|
box.label(text="Text:", icon='TEXT')
|
|
draw_text_input_section(box, props)
|
|
|
|
|
|
# Generate Button
|
|
layout.separator()
|
|
draw_generate_button(layout)
|
|
|
|
layout.separator()
|
|
|
|
# COLLAPSIBLE SECTIONS
|
|
|
|
# Image Overlays Section (collapsed by default)
|
|
overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA')
|
|
if overlay_section:
|
|
draw_image_overlays_section(overlay_section, props)
|
|
|
|
# Positioning & Layout Section (collapsed by default)
|
|
position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER')
|
|
if position_section:
|
|
draw_positioning_section(position_section, props)
|
|
|
|
# Font Settings Section (collapsed by default)
|
|
font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
|
|
if font_section:
|
|
draw_font_settings_section(font_section, props)
|
|
|
|
# Preview Options Section (collapsed by default)
|
|
preview_section = draw_collapsible_header(layout, "expand_preview_options", "Preview Options", 'IMAGE_DATA')
|
|
if preview_section:
|
|
draw_preview_options_section(preview_section, props)
|
|
|
|
# Preview Button in expanded section
|
|
layout.separator()
|
|
preview_section.separator()
|
|
button_row = preview_section.row()
|
|
button_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
|
|
|
|
# Presets Section (collapsed by default)
|
|
presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
|
|
if presets_section:
|
|
draw_presets_section(presets_section, props)
|
|
|
|
class TEXT_TEXTURE_PT_panel_3d(Panel):
|
|
"""3D View Panel - Access from sidebar (N key)"""
|
|
bl_label = "Text Texture Generator"
|
|
bl_idname = "TEXT_TEXTURE_PT_panel_3d"
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = "Tool"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
props = context.scene.text_texture_props
|
|
|
|
# TOP LEVEL - Always Visible Controls
|
|
|
|
# Text Input
|
|
box = layout.box()
|
|
box.label(text="Text:", icon='TEXT')
|
|
draw_text_input_section(box, props)
|
|
|
|
|
|
# Generate Button
|
|
layout.separator()
|
|
draw_generate_button(layout)
|
|
|
|
layout.separator()
|
|
|
|
# COLLAPSIBLE SECTIONS
|
|
|
|
# Image Overlays Section (collapsed by default)
|
|
overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA')
|
|
if overlay_section:
|
|
draw_image_overlays_section(overlay_section, props)
|
|
|
|
# Positioning & Layout Section (collapsed by default)
|
|
position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER')
|
|
if position_section:
|
|
draw_positioning_section(position_section, props)
|
|
|
|
# Font Settings Section (collapsed by default)
|
|
font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
|
|
if font_section:
|
|
draw_font_settings_section(font_section, props)
|
|
|
|
# Preview Options Section (collapsed by default)
|
|
preview_section = draw_collapsible_header(layout, "expand_preview_options", "Preview Options", 'IMAGE_DATA')
|
|
if preview_section:
|
|
draw_preview_options_section(preview_section, props)
|
|
|
|
# Preview Button in expanded section
|
|
preview_section.separator()
|
|
button_row = preview_section.row()
|
|
button_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
|
|
|
|
# Presets Section (collapsed by default)
|
|
presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
|
|
if presets_section:
|
|
draw_presets_section(presets_section, props)
|
|
|
|
class TEXT_TEXTURE_PT_panel_properties(Panel):
|
|
"""Properties Panel"""
|
|
bl_label = "Text Texture Generator"
|
|
bl_idname = "TEXT_TEXTURE_PT_panel_properties"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "material"
|
|
bl_category = "Tool"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
props = context.scene.text_texture_props
|
|
|
|
# TOP LEVEL - Always Visible Controls
|
|
|
|
# Text Input
|
|
box = layout.box()
|
|
box.label(text="Text:", icon='TEXT')
|
|
draw_text_input_section(box, props)
|
|
|
|
|
|
# Generate Button
|
|
layout.separator()
|
|
draw_generate_button(layout)
|
|
|
|
layout.separator()
|
|
|
|
# COLLAPSIBLE SECTIONS
|
|
|
|
# Image Overlays Section (collapsed by default)
|
|
overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA')
|
|
if overlay_section:
|
|
draw_image_overlays_section(overlay_section, props)
|
|
|
|
# Positioning & Layout Section (collapsed by default)
|
|
position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER')
|
|
if position_section:
|
|
draw_positioning_section(position_section, props)
|
|
|
|
# Font Settings Section (collapsed by default)
|
|
font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
|
|
if font_section:
|
|
draw_font_settings_section(font_section, props)
|
|
|
|
# Preview Options Section (collapsed by default)
|
|
preview_section = draw_collapsible_header(layout, "expand_preview_options", "Preview Options", 'IMAGE_DATA')
|
|
if preview_section:
|
|
draw_preview_options_section(preview_section, props)
|
|
|
|
# Preview Button in expanded section
|
|
preview_section.separator()
|
|
button_row = preview_section.row()
|
|
button_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
|
|
|
|
# Presets Section (collapsed by default)
|
|
presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
|
|
if presets_section:
|
|
draw_presets_section(presets_section, props)
|
|
|
|
# ============================================================================
|
|
# REGISTRATION
|
|
# ============================================================================
|
|
|
|
classes = (
|
|
TEXT_TEXTURE_ImageOverlay,
|
|
TEXT_TEXTURE_Preset,
|
|
TEXT_TEXTURE_Properties,
|
|
TEXT_TEXTURE_OT_generate,
|
|
TEXT_TEXTURE_OT_refresh_preview,
|
|
TEXT_TEXTURE_OT_view_preview_zoom,
|
|
TEXT_TEXTURE_OT_add_overlay,
|
|
TEXT_TEXTURE_OT_remove_overlay,
|
|
TEXT_TEXTURE_OT_open_panel,
|
|
TEXT_TEXTURE_OT_save_preset,
|
|
TEXT_TEXTURE_OT_save_preset_enter,
|
|
TEXT_TEXTURE_OT_set_anchor_point,
|
|
TEXT_TEXTURE_OT_sync_margins,
|
|
TEXT_TEXTURE_OT_load_preset,
|
|
TEXT_TEXTURE_OT_delete_preset,
|
|
TEXT_TEXTURE_OT_refresh_fonts,
|
|
TEXT_TEXTURE_PT_panel,
|
|
TEXT_TEXTURE_PT_panel_3d,
|
|
TEXT_TEXTURE_PT_panel_properties,
|
|
)
|
|
|
|
def register():
|
|
print("[TTG DEBUG] ========== ADDON REGISTRATION START ==========")
|
|
for cls in classes:
|
|
print(f"[TTG DEBUG] Registering class: {cls.__name__}")
|
|
try:
|
|
bpy.utils.register_class(cls)
|
|
print(f"[TTG DEBUG] Successfully registered: {cls.__name__}")
|
|
except Exception as e:
|
|
print(f"[TTG DEBUG] FAILED to register {cls.__name__}: {e}")
|
|
|
|
print("[TTG DEBUG] Registering scene properties...")
|
|
bpy.types.Scene.text_texture_props = PointerProperty(type=TEXT_TEXTURE_Properties)
|
|
print("[TTG DEBUG] Scene properties registered successfully")
|
|
|
|
# CRITICAL FIX: Force UI refresh after registration
|
|
print("[TTG DEBUG] Forcing UI refresh...")
|
|
try:
|
|
# Update all areas to ensure panels are properly displayed
|
|
for window in bpy.context.window_manager.windows:
|
|
for area in window.screen.areas:
|
|
area.tag_redraw()
|
|
print("[TTG DEBUG] UI refresh completed successfully")
|
|
except Exception as e:
|
|
print(f"[TTG DEBUG] UI refresh failed: {e}")
|
|
|
|
# Add to menus
|
|
bpy.types.NODE_MT_add.append(menu_func_node_editor)
|
|
bpy.types.VIEW3D_MT_add.append(menu_func_view3d)
|
|
bpy.types.VIEW3D_MT_mesh_add.append(menu_func_view3d)
|
|
bpy.types.NODE_MT_node.append(menu_func_node_editor)
|
|
|
|
# Add to Image menu if available
|
|
if hasattr(bpy.types, 'IMAGE_MT_image'):
|
|
bpy.types.IMAGE_MT_image.append(menu_func_image)
|
|
|
|
# Initialize presets after registration to prevent race conditions
|
|
# Use a timer to ensure the scene is ready
|
|
def delayed_preset_init():
|
|
initialize_presets()
|
|
return None # Stop timer
|
|
|
|
print("[TTG DEBUG] Registration completed - scheduling preset initialization")
|
|
bpy.app.timers.register(delayed_preset_init, first_interval=0.1)
|
|
print("[TTG DEBUG] ========== ADDON REGISTRATION END ==========")
|
|
|
|
def unregister():
|
|
# Clean up preview
|
|
try:
|
|
if bpy.context.scene:
|
|
props = bpy.context.scene.text_texture_props
|
|
if props and props.preview_image:
|
|
bpy.data.images.remove(props.preview_image)
|
|
except Exception:
|
|
pass
|
|
|
|
if "TextTexturePreview" in bpy.data.images:
|
|
bpy.data.images.remove(bpy.data.images["TextTexturePreview"])
|
|
|
|
# Remove from menus
|
|
bpy.types.NODE_MT_add.remove(menu_func_node_editor)
|
|
bpy.types.VIEW3D_MT_add.remove(menu_func_view3d)
|
|
bpy.types.VIEW3D_MT_mesh_add.remove(menu_func_view3d)
|
|
bpy.types.NODE_MT_node.remove(menu_func_node_editor)
|
|
|
|
if hasattr(bpy.types, 'IMAGE_MT_image'):
|
|
bpy.types.IMAGE_MT_image.remove(menu_func_image)
|
|
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
|
|
del bpy.types.Scene.text_texture_props
|
|
|
|
if __name__ == "__main__":
|
|
register() |