diff --git a/Archive.zip b/Archive.zip index 9a05eb3..35251b6 100644 Binary files a/Archive.zip and b/Archive.zip differ diff --git a/__init__.py b/__init__.py index b8bf351..7f1ce13 100644 --- a/__init__.py +++ b/__init__.py @@ -98,18 +98,291 @@ def get_preset_path(): addon_dir = os.path.dirname(os.path.realpath(__file__)) presets_dir = os.path.join(addon_dir, "presets") if not os.path.exists(presets_dir): - os.makedirs(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 - width and height are REQUIRED""" + """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}") @@ -121,134 +394,186 @@ def generate_texture_image(props, width, 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 - # If this is a preview (smaller than texture size), scale the font accordingly 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}") - print(f"Font size: {font_size_pixels}, Padding: {padding}") - - # Create image with background + # Create base image with background if props.background_transparent: - img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) + 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),) - img = Image.new('RGBA', (width, height), bg_color) + base_img = Image.new('RGBA', (width, height), bg_color) - print(f"Image created: {img.size}") + print(f"Base image created: {base_img.size}") - draw = ImageDraw.Draw(img) + # Collect all elements with z-index (text is z-index 0) + elements = [] - # Load font with proper size handling + # 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 - try: - if props.use_custom_font and props.custom_font_path and os.path.exists(props.custom_font_path): - test_font = ImageFont.truetype(props.custom_font_path, font_size_pixels) - # Test the font before accepting it - test_draw = ImageDraw.Draw(Image.new('RGB', (10, 10))) - test_draw.textbbox((0, 0), "A", font=test_font) - font = test_font - font_loaded = True - print(f"Loaded custom font: {props.custom_font_path}") - elif props.font_path != "default" and os.path.exists(props.font_path): - test_font = ImageFont.truetype(props.font_path, font_size_pixels) - # Test the font before accepting it - test_draw = ImageDraw.Draw(Image.new('RGB', (10, 10))) - test_draw.textbbox((0, 0), "A", font=test_font) - font = test_font - font_loaded = True - print(f"Loaded font: {props.font_path}") - except Exception as e: - print(f"Font loading error: {e}") - font_loaded = False + print("DEBUG UPPERCASE FIX: Starting enhanced font loading with mixed-case validation") - # Better fallback - try to find a system TrueType font + # 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: - try: - # Try to find a basic system font - system = platform.system() - fallback_fonts = [] - - if system == "Windows": - fallback_fonts = [ - "C:/Windows/Fonts/arial.ttf", - "C:/Windows/Fonts/Arial.ttf", - "C:/Windows/Fonts/calibri.ttf", - "C:/Windows/Fonts/tahoma.ttf" - ] - elif system == "Darwin": # macOS - fallback_fonts = [ - "/System/Library/Fonts/Helvetica.ttc", - "/Library/Fonts/Arial.ttf", - "/System/Library/Fonts/Avenir.ttc" - ] - else: # Linux - fallback_fonts = [ - "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", - "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf" - ] - - for fallback_path in fallback_fonts: - if os.path.exists(fallback_path): - try: - font = ImageFont.truetype(fallback_path, font_size_pixels) - # Test font by trying to get bbox of a simple character - test_draw = ImageDraw.Draw(Image.new('RGB', (10, 10))) - test_draw.textbbox((0, 0), "A", font=font) - font_loaded = True - print(f"Using fallback font: {fallback_path}") - break - except Exception as font_error: - print(f"Failed to load fallback font {fallback_path}: {font_error}") - continue - except Exception: - pass + 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 (but it won't respect size) + # Last resort - use default font but warn about mixed-case limitations if not font_loaded: font = ImageFont.load_default() - print("Warning: Using default font - size will not be accurate!") + 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 with error handling + # 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}") - # Fallback: estimate text size text_width = int(len(text) * font_size_pixels * 0.6) text_height = font_size_pixels - # Calculate X position based on alignment - if props.text_align == 'center': - x = (width - text_width) // 2 - elif props.text_align == 'right': - x = width - text_width - padding - else: - x = padding + # ============================================================================ + # ENHANCED POSITIONING SYSTEM - Replaces hardcoded vertical centering + # ============================================================================ - # Center vertically - y = (height - text_height) // 2 + # 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") - return img + # 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}") @@ -267,22 +592,13 @@ def generate_preview(context): # Always generate preview - removed live_preview_enabled check try: - # Generate at preview size for performance, not full texture size - preview_size = props.preview_size # Default is 256 - # Calculate aspect ratio preserving dimensions - aspect_ratio = props.texture_width / props.texture_height - if aspect_ratio > 1: # Wider than tall - preview_w = preview_size - preview_h = int(preview_size / aspect_ratio) - else: # Taller than wide or square - preview_w = int(preview_size * aspect_ratio) - preview_h = preview_size + # Generate at full texture size for 100% quality preview + preview_w = props.texture_width + preview_h = props.texture_height - # Ensure minimum dimensions - preview_w = max(16, preview_w) - preview_h = max(16, preview_h) + 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} (preview size: {preview_size})") + 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 @@ -292,7 +608,7 @@ def generate_preview(context): return None # Apply preview background if needed - if props.preview_bg_type != 'transparent' or not props.background_transparent: + if props.preview_bg_type != 'transparent': from PIL import Image, ImageDraw # Create background image @@ -417,6 +733,75 @@ def update_live_preview(self, context): # 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( @@ -435,6 +820,37 @@ class TEXT_TEXTURE_Properties(PropertyGroup): 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", @@ -453,7 +869,7 @@ class TEXT_TEXTURE_Properties(PropertyGroup): ) texture_height: IntProperty( - name="Height", + name="Height", description="Texture height in pixels", default=1024, min=64, @@ -542,14 +958,181 @@ class TEXT_TEXTURE_Properties(PropertyGroup): 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=256, + default=512, min=128, - max=512, + 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 ) @@ -588,14 +1171,26 @@ class TEXT_TEXTURE_Properties(PropertyGroup): 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", + 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 @@ -687,7 +1282,141 @@ class TEXT_TEXTURE_OT_refresh_preview(Operator): return {'FINISHED'} -# Removed view_preview operator - we want inline display only +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""" @@ -696,11 +1425,39 @@ class TEXT_TEXTURE_OT_save_preset(Operator): 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 - if not props.new_preset_name.strip(): - self.report({'ERROR'}, "Enter a preset name") + # 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 = { @@ -716,24 +1473,84 @@ class TEXT_TEXTURE_OT_save_preset(Operator): '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: - preset_file = os.path.join(get_preset_path(), f"{props.new_preset_name}.json") with open(preset_file, 'w') as f: json.dump(preset_data, f, indent=2) - preset = props.presets.add() - preset.name = props.new_preset_name + # 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 - self.report({'INFO'}, f"Saved: {props.new_preset_name}") + 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"Failed: {str(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""" @@ -749,6 +1566,10 @@ class TEXT_TEXTURE_OT_load_preset(Operator): 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) @@ -767,15 +1588,37 @@ class TEXT_TEXTURE_OT_load_preset(Operator): 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"Failed: {str(e)}") + self.report({'ERROR'}, f"Unexpected error loading preset: {str(e)}") return {'CANCELLED'} return {'FINISHED'} @@ -796,19 +1639,33 @@ class TEXT_TEXTURE_OT_delete_preset(Operator): 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: {self.preset_name}") + 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"Failed: {str(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""" @@ -853,6 +1710,272 @@ class TEXT_TEXTURE_OT_refresh_fonts(Operator): 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 # ============================================================================ @@ -887,128 +2010,78 @@ class TEXT_TEXTURE_PT_panel(Panel): @classmethod def poll(cls, context): - return context.space_data.type == 'NODE_EDITOR' and context.space_data.tree_type == 'ShaderNodeTree' + # 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 - # Always ensure preview exists and is up to date - # Generate initial preview on first draw - if not props.preview_image: - # Schedule preview generation using timer - def generate_preview_delayed(): - generate_preview(context) - return None # Stop timer - bpy.app.timers.register(generate_preview_delayed, first_interval=0.1) + # TOP LEVEL - Always Visible Controls - # LIVE PREVIEW - Simplified and clean - if props.preview_image: - box = layout.box() - box.template_ID_preview(props, "preview_image", rows=8, cols=8) - else: - box = layout.box() - box.label(text="Generating preview...", icon='TIME') - - # TEXT INPUT - BIG + # Text Input box = layout.box() - box.label(text="Text Input:", icon='TEXT') - row = box.row() - row.scale_y = 2.0 - row.prop(props, "text", text="") + box.label(text="Text:", icon='TEXT') + draw_text_input_section(box, props) - # SETTINGS - box = layout.box() - box.label(text="Texture Settings:", icon='SETTINGS') - # Dimensions - col = box.column() - row = col.row(align=True) - row.prop(props, "texture_width", text="Width") - row.prop(props, "texture_height", text="Height") - col.label(text=f"Output: {props.texture_width}Ɨ{props.texture_height}px", icon='INFO') - - # Font Settings - col.separator() - col.label(text="Font:", icon='FONT_DATA') - col.prop(props, "use_custom_font") - if props.use_custom_font: - col.prop(props, "custom_font_path", text="") - else: - row = col.row(align=True) - row.prop(props, "font_path", text="") - row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH') - - row = col.row(align=True) - row.prop(props, "font_size") - row.prop(props, "padding") - col.prop(props, "text_align") - - # Colors - col.separator() - col.label(text="Colors:", icon='COLOR') - col.prop(props, "text_color") - col.prop(props, "background_transparent") - if not props.background_transparent: - col.prop(props, "background_color") - - # Preview settings (compact) - col.separator() - row = col.row(align=True) - row.prop(props, "preview_size", text="Preview") - row.prop(props, "preview_bg_type", text="") - if props.preview_bg_type == 'color': - col.prop(props, "preview_bg_color", text="") - elif props.preview_bg_type == 'gradient': - row = col.row(align=True) - row.prop(props, "preview_bg_color", text="") - row.prop(props, "preview_bg_color2", text="") - - # GENERATE BUTTON + # Generate Button layout.separator() - row = layout.row() - row.scale_y = 2.0 - row.operator("text_texture.generate", text="GENERATE FINAL TEXTURE", icon='IMAGE_DATA') - -class TEXT_TEXTURE_PT_presets(Panel): - """Presets Panel""" - bl_label = "Presets" - bl_idname = "TEXT_TEXTURE_PT_presets" - bl_parent_id = "TEXT_TEXTURE_PT_panel" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - props = context.scene.text_texture_props + draw_generate_button(layout) - # Load presets - if not props.presets: - preset_dir = get_preset_path() - if os.path.exists(preset_dir): - for filename in os.listdir(preset_dir): - if filename.endswith('.json'): - preset_name = os.path.splitext(filename)[0] - if preset_name not in [p.name for p in props.presets]: - preset = props.presets.add() - preset.name = preset_name + layout.separator() - # Save preset - box = layout.box() - box.label(text="Save Current Settings:", icon='EXPORT') - row = box.row(align=True) - row.prop(props, "new_preset_name", text="") - row.operator("text_texture.save_preset", text="Save", icon='ADD') + # COLLAPSIBLE SECTIONS - # Load preset - if props.presets: - box = layout.box() - box.label(text="Load Preset:", icon='IMPORT') - for preset in props.presets: - row = box.row(align=True) - row.operator("text_texture.load_preset", text=preset.name).preset_name = preset.name - row.operator("text_texture.delete_preset", text="", icon='X').preset_name = preset.name + # 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)""" @@ -1023,46 +2096,51 @@ class TEXT_TEXTURE_PT_panel_3d(Panel): layout = self.layout props = context.scene.text_texture_props - # Generate initial preview if needed - if not props.preview_image: - # Schedule preview generation using timer - def generate_preview_delayed(): - generate_preview(context) - return None # Stop timer - bpy.app.timers.register(generate_preview_delayed, first_interval=0.1) + # TOP LEVEL - Always Visible Controls - # LIVE PREVIEW - Simplified - if props.preview_image: - box = layout.box() - box.template_ID_preview(props, "preview_image", rows=6, cols=6) - else: - box = layout.box() - box.label(text="Generating preview...", icon='TIME') - - # TEXT INPUT + # Text Input box = layout.box() box.label(text="Text:", icon='TEXT') - row = box.row() - row.scale_y = 1.5 - row.prop(props, "text", text="") + draw_text_input_section(box, props) - # Quick Settings - col = layout.column() - row = col.row(align=True) - row.prop(props, "texture_width", text="W") - row.prop(props, "texture_height", text="H") - col.prop(props, "font_size") - col.prop(props, "text_color") - col.prop(props, "background_transparent") - # Preview Toggle - # Preview is always enabled - removed toggle - - # Generate + # Generate Button layout.separator() - row = layout.row() - row.scale_y = 1.5 - row.operator("text_texture.generate", text="Generate Texture", icon='IMAGE_DATA') + 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""" @@ -1077,71 +2155,102 @@ class TEXT_TEXTURE_PT_panel_properties(Panel): layout = self.layout props = context.scene.text_texture_props - # Generate initial preview if needed - if not props.preview_image: - # Schedule preview generation using timer - def generate_preview_delayed(): - generate_preview(context) - return None # Stop timer - bpy.app.timers.register(generate_preview_delayed, first_interval=0.1) + # TOP LEVEL - Always Visible Controls - # LIVE PREVIEW - Simplified - if props.preview_image: - box = layout.box() - box.template_ID_preview(props, "preview_image", rows=6, cols=6) - else: - box = layout.box() - box.label(text="Generating preview...", icon='TIME') - - # TEXT INPUT + # Text Input box = layout.box() box.label(text="Text:", icon='TEXT') - row = box.row() - row.scale_y = 1.5 - row.prop(props, "text", text="") + draw_text_input_section(box, props) - # Quick Settings - col = layout.column() - row = col.row(align=True) - row.prop(props, "texture_width", text="W") - row.prop(props, "texture_height", text="H") - col.prop(props, "font_size") - col.prop(props, "text_color") - col.prop(props, "background_transparent") - if not props.background_transparent: - col.prop(props, "background_color") - # Generate + # Generate Button layout.separator() - row = layout.row() - row.scale_y = 1.5 - row.operator("text_texture.generate", text="Generate Texture", icon='IMAGE_DATA') + 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_presets, TEXT_TEXTURE_PT_panel_3d, TEXT_TEXTURE_PT_panel_properties, ) def register(): + print("[TTG DEBUG] ========== ADDON REGISTRATION START ==========") for cls in classes: - bpy.utils.register_class(cls) + 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) @@ -1152,6 +2261,16 @@ def register(): # 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 diff --git a/test_positioning_system.py b/test_positioning_system.py new file mode 100644 index 0000000..eee2d5d --- /dev/null +++ b/test_positioning_system.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python3 +""" +Comprehensive test suite for the Text Texture Generator's enhanced positioning system. +This script validates the positioning calculations and identifies potential issues +without requiring Blender to be installed. +""" + +import sys +import traceback +from typing import Dict, Tuple, List, Any + +# Mock Blender properties for testing +class MockProps: + """Mock properties class to simulate Blender addon properties""" + def __init__(self): + # Enhanced positioning properties + self.anchor_point = 'MIDDLE_CENTER' + self.position_mode = 'PRESET' + self.margin_x = 0.0 + self.margin_y = 0.0 + self.margins_linked = False + self.offset_x = 0 + self.offset_y = 0 + self.manual_position_x = 50.0 + self.manual_position_y = 50.0 + self.manual_position_unit = 'PERCENTAGE' + self.constrain_to_canvas = True + + # Legacy properties for backward compatibility testing + self.text_align = 'center' + self.padding = 20 + +# Copy of the enhanced positioning function from the addon +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'] + ] + +class PositioningTestSuite: + """Comprehensive test suite for positioning system""" + + def __init__(self): + self.test_results = [] + self.issues_found = [] + self.canvas_width = 1024 + self.canvas_height = 1024 + self.text_width = 200 + self.text_height = 50 + + def log_test(self, test_name: str, passed: bool, details: str = "", expected=None, actual=None): + """Log test result""" + result = { + 'test': test_name, + 'passed': passed, + 'details': details, + 'expected': expected, + 'actual': actual + } + self.test_results.append(result) + + if not passed: + self.issues_found.append(f"āŒ {test_name}: {details}") + if expected is not None and actual is not None: + self.issues_found.append(f" Expected: {expected}, Got: {actual}") + else: + print(f"āœ… {test_name}") + + def test_anchor_point_calculations(self): + """Test all 9 anchor point calculations""" + print("\n=== Testing Anchor Point Calculations ===") + + anchor_matrix = get_anchor_point_matrix() + expected_positions = { + 'TOP_LEFT': (0, 0), + 'TOP_CENTER': (412, 0), # (1024-200)/2 = 412 + 'TOP_RIGHT': (824, 0), # 1024-200 = 824 + 'MIDDLE_LEFT': (0, 487), # (1024-50)/2 = 487 + 'MIDDLE_CENTER': (412, 487), + 'MIDDLE_RIGHT': (824, 487), + 'BOTTOM_LEFT': (0, 974), # 1024-50 = 974 + 'BOTTOM_CENTER': (412, 974), + 'BOTTOM_RIGHT': (824, 974) + } + + for row in anchor_matrix: + for anchor in row: + props = MockProps() + props.anchor_point = anchor + props.position_mode = 'PRESET' + props.margin_x = 0.0 + props.margin_y = 0.0 + props.offset_x = 0 + props.offset_y = 0 + + try: + x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, + self.text_width, self.text_height) + expected = expected_positions[anchor] + + if (x, y) == expected: + self.log_test(f"Anchor {anchor}", True, f"Position: ({x}, {y})") + else: + self.log_test(f"Anchor {anchor}", False, + f"Incorrect position calculation", expected, (x, y)) + + except Exception as e: + self.log_test(f"Anchor {anchor}", False, f"Exception: {str(e)}") + + def test_margin_calculations(self): + """Test margin calculations with various percentages""" + print("\n=== Testing Margin Calculations ===") + + test_cases = [ + (10.0, 10.0), # 10% margins + (25.0, 25.0), # 25% margins + (50.0, 50.0), # 50% margins (maximum) + (0.0, 0.0), # No margins + ] + + for margin_x, margin_y in test_cases: + props = MockProps() + props.anchor_point = 'TOP_LEFT' + props.position_mode = 'PRESET' + props.margin_x = margin_x + props.margin_y = margin_y + props.offset_x = 0 + props.offset_y = 0 + + expected_x = int((margin_x / 100.0) * self.canvas_width) + expected_y = int((margin_y / 100.0) * self.canvas_height) + + try: + x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, + self.text_width, self.text_height) + + if x == expected_x and y == expected_y: + self.log_test(f"Margins {margin_x}%/{margin_y}%", True, f"Position: ({x}, {y})") + else: + self.log_test(f"Margins {margin_x}%/{margin_y}%", False, + f"Incorrect margin calculation", (expected_x, expected_y), (x, y)) + + except Exception as e: + self.log_test(f"Margins {margin_x}%/{margin_y}%", False, f"Exception: {str(e)}") + + def test_manual_positioning(self): + """Test manual positioning mode with percentage and pixel units""" + print("\n=== Testing Manual Positioning ===") + + # Test percentage mode + props = MockProps() + props.position_mode = 'MANUAL' + props.manual_position_unit = 'PERCENTAGE' + props.manual_position_x = 25.0 # 25% + props.manual_position_y = 75.0 # 75% + + expected_x = int((25.0 / 100.0) * self.canvas_width) # 256 + expected_y = int((75.0 / 100.0) * self.canvas_height) # 768 + + try: + x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, + self.text_width, self.text_height) + + if x == expected_x and y == expected_y: + self.log_test("Manual Percentage Mode", True, f"Position: ({x}, {y})") + else: + self.log_test("Manual Percentage Mode", False, + f"Incorrect calculation", (expected_x, expected_y), (x, y)) + + except Exception as e: + self.log_test("Manual Percentage Mode", False, f"Exception: {str(e)}") + + # Test pixel mode + props.manual_position_unit = 'PIXELS' + props.manual_position_x = 100.0 + props.manual_position_y = 200.0 + + expected_x = 100 + expected_y = 200 + + try: + x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, + self.text_width, self.text_height) + + if x == expected_x and y == expected_y: + self.log_test("Manual Pixel Mode", True, f"Position: ({x}, {y})") + else: + self.log_test("Manual Pixel Mode", False, + f"Incorrect calculation", (expected_x, expected_y), (x, y)) + + except Exception as e: + self.log_test("Manual Pixel Mode", False, f"Exception: {str(e)}") + + def test_canvas_constraints(self): + """Test canvas constraint functionality""" + print("\n=== Testing Canvas Constraints ===") + + # Test position that would go out of bounds + props = MockProps() + props.position_mode = 'MANUAL' + props.manual_position_unit = 'PIXELS' + props.manual_position_x = 1000.0 # Would put text partially outside (text_width=200) + props.manual_position_y = 1000.0 # Would put text partially outside (text_height=50) + props.constrain_to_canvas = True + + expected_x = self.canvas_width - self.text_width # 824 + expected_y = self.canvas_height - self.text_height # 974 + + try: + x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, + self.text_width, self.text_height) + + if x == expected_x and y == expected_y: + self.log_test("Canvas Constraints (constrained)", True, f"Position: ({x}, {y})") + else: + self.log_test("Canvas Constraints (constrained)", False, + f"Constraint failed", (expected_x, expected_y), (x, y)) + + except Exception as e: + self.log_test("Canvas Constraints (constrained)", False, f"Exception: {str(e)}") + + # Test with constraints disabled + props.constrain_to_canvas = False + expected_x = 1000 # Should not be constrained + expected_y = 1000 + + try: + x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, + self.text_width, self.text_height) + + if x == expected_x and y == expected_y: + self.log_test("Canvas Constraints (unconstrained)", True, f"Position: ({x}, {y})") + else: + self.log_test("Canvas Constraints (unconstrained)", False, + f"Should not be constrained", (expected_x, expected_y), (x, y)) + + except Exception as e: + self.log_test("Canvas Constraints (unconstrained)", False, f"Exception: {str(e)}") + + def test_offset_application(self): + """Test pixel offset fine-tuning""" + print("\n=== Testing Pixel Offset Application ===") + + props = MockProps() + props.anchor_point = 'MIDDLE_CENTER' + props.position_mode = 'PRESET' + props.margin_x = 0.0 + props.margin_y = 0.0 + props.offset_x = 50 # 50px right + props.offset_y = -25 # 25px up + + base_x = (self.canvas_width - self.text_width) // 2 # 412 + base_y = (self.canvas_height - self.text_height) // 2 # 487 + expected_x = base_x + props.offset_x # 462 + expected_y = base_y + props.offset_y # 462 + + try: + x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, + self.text_width, self.text_height) + + if x == expected_x and y == expected_y: + self.log_test("Pixel Offset Application", True, f"Position: ({x}, {y})") + else: + self.log_test("Pixel Offset Application", False, + f"Offset not applied correctly", (expected_x, expected_y), (x, y)) + + except Exception as e: + self.log_test("Pixel Offset Application", False, f"Exception: {str(e)}") + + def test_margin_synchronization(self): + """Test margin link functionality""" + print("\n=== Testing Margin Synchronization ===") + + props = MockProps() + props.margins_linked = True + props.margin_x = 15.0 + props.margin_y = 10.0 + + # Test X margin sync + sync_margin_values(props, 'x') + if props.margin_y == props.margin_x: + self.log_test("Margin Sync (X to Y)", True, f"Y margin updated to {props.margin_y}") + else: + self.log_test("Margin Sync (X to Y)", False, + f"Y margin not synced", props.margin_x, props.margin_y) + + # Reset and test Y margin sync + props.margin_x = 20.0 + props.margin_y = 25.0 + sync_margin_values(props, 'y') + if props.margin_x == props.margin_y: + self.log_test("Margin Sync (Y to X)", True, f"X margin updated to {props.margin_x}") + else: + self.log_test("Margin Sync (Y to X)", False, + f"X margin not synced", props.margin_y, props.margin_x) + + # Test unlinked margins + props.margins_linked = False + original_x = props.margin_x + original_y = props.margin_y + sync_margin_values(props, 'x') + + if props.margin_x == original_x and props.margin_y == original_y: + self.log_test("Margin Sync (Unlinked)", True, "Margins unchanged when unlinked") + else: + self.log_test("Margin Sync (Unlinked)", False, + "Margins changed when they shouldn't") + + def test_edge_cases(self): + """Test edge cases and boundary conditions""" + print("\n=== Testing Edge Cases ===") + + # Test zero canvas dimensions + props = MockProps() + try: + x, y = calculate_text_position(props, 0, 0, self.text_width, self.text_height) + self.log_test("Zero Canvas Dimensions", True, f"Handled gracefully: ({x}, {y})") + except Exception as e: + self.log_test("Zero Canvas Dimensions", False, f"Exception: {str(e)}") + + # Test text larger than canvas + props = MockProps() + props.anchor_point = 'TOP_LEFT' + props.constrain_to_canvas = True + large_text_width = 2000 + large_text_height = 2000 + + try: + x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, + large_text_width, large_text_height) + + if x <= 0 and y <= 0: + self.log_test("Text Larger Than Canvas", True, f"Constrained to: ({x}, {y})") + else: + self.log_test("Text Larger Than Canvas", False, + f"Not properly constrained: ({x}, {y})") + except Exception as e: + self.log_test("Text Larger Than Canvas", False, f"Exception: {str(e)}") + + # Test maximum margin values (50%) + props = MockProps() + props.anchor_point = 'TOP_LEFT' + props.margin_x = 50.0 + props.margin_y = 50.0 + + try: + x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, + self.text_width, self.text_height) + + expected_x = int(0.5 * self.canvas_width) # 512 + expected_y = int(0.5 * self.canvas_height) # 512 + + if x == expected_x and y == expected_y: + self.log_test("Maximum Margins (50%)", True, f"Position: ({x}, {y})") + else: + self.log_test("Maximum Margins (50%)", False, + f"Incorrect calculation", (expected_x, expected_y), (x, y)) + except Exception as e: + self.log_test("Maximum Margins (50%)", False, f"Exception: {str(e)}") + + def analyze_ui_integration(self): + """Analyze UI integration potential issues""" + print("\n=== Analyzing UI Integration ===") + + issues = [] + + # Check anchor point matrix structure + matrix = get_anchor_point_matrix() + if len(matrix) != 3 or any(len(row) != 3 for row in matrix): + issues.append("āŒ Anchor point matrix is not 3x3") + else: + print("āœ… Anchor point matrix structure is correct") + + # Check all anchor points are defined + all_anchors = [anchor for row in matrix for anchor in row] + expected_anchors = [ + 'TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT', + 'MIDDLE_LEFT', 'MIDDLE_CENTER', 'MIDDLE_RIGHT', + 'BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT' + ] + + missing_anchors = set(expected_anchors) - set(all_anchors) + if missing_anchors: + issues.append(f"āŒ Missing anchor points: {missing_anchors}") + else: + print("āœ… All required anchor points are defined") + + # Check for property type consistency + props = MockProps() + + # Test property ranges + if hasattr(props, 'margin_x') and hasattr(props, 'margin_y'): + print("āœ… Margin properties exist") + else: + issues.append("āŒ Margin properties missing") + + if hasattr(props, 'offset_x') and hasattr(props, 'offset_y'): + print("āœ… Offset properties exist") + else: + issues.append("āŒ Offset properties missing") + + self.issues_found.extend(issues) + + def run_all_tests(self): + """Run all tests and generate comprehensive report""" + print("šŸ” ENHANCED POSITIONING SYSTEM TEST SUITE") + print("=" * 50) + + try: + self.test_anchor_point_calculations() + self.test_margin_calculations() + self.test_manual_positioning() + self.test_canvas_constraints() + self.test_offset_application() + self.test_margin_synchronization() + self.test_edge_cases() + self.analyze_ui_integration() + + except Exception as e: + print(f"āŒ Critical error during testing: {e}") + traceback.print_exc() + + self.generate_report() + + def generate_report(self): + """Generate comprehensive test report""" + print("\n" + "=" * 60) + print("šŸ“Š COMPREHENSIVE TEST REPORT") + print("=" * 60) + + total_tests = len(self.test_results) + passed_tests = sum(1 for result in self.test_results if result['passed']) + failed_tests = total_tests - passed_tests + + print(f"šŸ“ˆ SUMMARY:") + print(f" Total Tests: {total_tests}") + print(f" āœ… Passed: {passed_tests}") + print(f" āŒ Failed: {failed_tests}") + print(f" Success Rate: {(passed_tests/total_tests*100):.1f}%" if total_tests > 0 else "No tests run") + + if self.issues_found: + print(f"\nšŸ› ISSUES IDENTIFIED ({len(self.issues_found)}):") + for issue in self.issues_found: + print(f" {issue}") + + # Categorize issues by severity + critical_issues = [] + major_issues = [] + minor_issues = [] + + for result in self.test_results: + if not result['passed']: + if 'exception' in result['details'].lower(): + critical_issues.append(result) + elif 'constraint' in result['test'].lower() or 'calculation' in result['details'].lower(): + major_issues.append(result) + else: + minor_issues.append(result) + + if critical_issues: + print(f"\n🚨 CRITICAL ISSUES ({len(critical_issues)}):") + for issue in critical_issues: + print(f" • {issue['test']}: {issue['details']}") + + if major_issues: + print(f"\nāš ļø MAJOR ISSUES ({len(major_issues)}):") + for issue in major_issues: + print(f" • {issue['test']}: {issue['details']}") + + if minor_issues: + print(f"\n ā„¹ļø MINOR ISSUES ({len(minor_issues)}):") + for issue in minor_issues: + print(f" • {issue['test']}: {issue['details']}") + + # Recommendations + print(f"\nšŸŽÆ RECOMMENDATIONS:") + + if critical_issues: + print(" 1. Fix critical exceptions before deployment") + print(" 2. Add proper error handling for edge cases") + + if major_issues: + print(" 3. Review positioning calculation logic") + print(" 4. Test constraint functionality thoroughly") + + print(" 5. Add comprehensive unit tests to the addon") + print(" 6. Implement validation for property ranges") + print(" 7. Add user feedback for invalid input combinations") + + return { + 'total_tests': total_tests, + 'passed': passed_tests, + 'failed': failed_tests, + 'issues': self.issues_found, + 'critical_issues': len(critical_issues), + 'major_issues': len(major_issues), + 'minor_issues': len(minor_issues) + } + +if __name__ == "__main__": + print("šŸš€ Starting Enhanced Positioning System Test Suite...") + test_suite = PositioningTestSuite() + results = test_suite.run_all_tests() + + print(f"\n✨ Testing completed!") + print(f"Check the detailed report above for analysis and recommendations.") \ No newline at end of file diff --git a/test_ui_and_compatibility.py b/test_ui_and_compatibility.py new file mode 100644 index 0000000..69c949d --- /dev/null +++ b/test_ui_and_compatibility.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + +""" +Advanced UI Integration and Backward Compatibility Test Suite +for the Text Texture Generator's enhanced positioning system. + +This focuses on potential issues that the basic positioning tests might miss. +""" + +import sys +import re + +class UICompatibilityTestSuite: + """Test suite focused on UI integration and compatibility issues""" + + def __init__(self): + self.issues_found = [] + self.warnings = [] + + def analyze_ui_layout_structure(self): + """Analyze the UI layout structure from the addon code""" + print("\n=== Analyzing UI Layout Structure ===") + + # Read the addon code to analyze UI structure + try: + with open('__init__.py', 'r') as f: + addon_code = f.read() + except FileNotFoundError: + self.issues_found.append("āŒ Cannot read __init__.py for UI analysis") + return + + # Check for Positioning & Alignment section + if "Positioning & Alignment" in addon_code: + print("āœ… 'Positioning & Alignment' section found in UI") + else: + self.issues_found.append("āŒ 'Positioning & Alignment' section not found in UI") + + # Check for position mode toggle + if 'position_mode' in addon_code and 'PRESET' in addon_code and 'MANUAL' in addon_code: + print("āœ… Position mode toggle implementation found") + else: + self.issues_found.append("āŒ Position mode toggle not properly implemented") + + # Check for 3x3 anchor grid implementation + anchor_grid_patterns = [ + r'get_anchor_point_matrix\(\)', + r'for.*anchor.*in.*matrix', + r'grid_row.*=.*position_col\.row' + ] + + grid_found = all(re.search(pattern, addon_code, re.DOTALL) for pattern in anchor_grid_patterns) + if grid_found: + print("āœ… 3x3 anchor grid UI implementation found") + else: + self.issues_found.append("āŒ 3x3 anchor grid UI implementation incomplete") + + # Check for margin controls with link functionality + if 'margins_linked' in addon_code and 'LINKED' in addon_code and 'UNLINKED' in addon_code: + print("āœ… Margin link/unlink controls found") + else: + self.issues_found.append("āŒ Margin link/unlink controls not properly implemented") + + # Check for conditional UI visibility + if 'position_mode == ' in addon_code: + print("āœ… Conditional UI visibility based on position mode found") + else: + self.warnings.append("āš ļø No conditional UI visibility found - controls might always be visible") + + def test_backward_compatibility_integration(self): + """Test how the new system integrates with existing text_align property""" + print("\n=== Testing Backward Compatibility Integration ===") + + try: + with open('__init__.py', 'r') as f: + addon_code = f.read() + except FileNotFoundError: + self.issues_found.append("āŒ Cannot read addon code for compatibility analysis") + return + + # Find the backward compatibility section in generate_texture_image + compat_section = re.search( + r'# Apply legacy text_align.*?props\.anchor_point == \'MIDDLE_CENTER\'.*?# Apply offsets even in legacy mode', + addon_code, re.DOTALL + ) + + if compat_section: + print("āœ… Backward compatibility section found in texture generation") + + # Check if all text_align values are handled + compat_code = compat_section.group(0) + align_values = ['left', 'center', 'right'] + missing_aligns = [] + + for align in align_values: + if f"text_align == '{align}'" not in compat_code: + missing_aligns.append(align) + + if missing_aligns: + self.issues_found.append(f"āŒ Missing text_align compatibility for: {missing_aligns}") + else: + print("āœ… All text_align values have compatibility handling") + + # Check if legacy mode only applies to MIDDLE_CENTER + if "anchor_point == 'MIDDLE_CENTER'" in compat_code: + print("āœ… Legacy compatibility properly scoped to MIDDLE_CENTER anchor") + else: + self.issues_found.append("āŒ Legacy compatibility scope issue - should only apply to MIDDLE_CENTER") + + else: + self.issues_found.append("āŒ Backward compatibility section not found in texture generation") + + # Check if text_align property is still defined + if re.search(r'text_align.*EnumProperty', addon_code): + print("āœ… Legacy text_align property still defined") + else: + self.issues_found.append("āŒ Legacy text_align property not found - breaking change!") + + def analyze_property_update_chains(self): + """Analyze property update chains for consistency""" + print("\n=== Analyzing Property Update Chains ===") + + try: + with open('__init__.py', 'r') as f: + addon_code = f.read() + except FileNotFoundError: + self.issues_found.append("āŒ Cannot read addon code for update chain analysis") + return + + # Find all properties with update=update_live_preview + update_properties = re.findall(r'(\w+):\s*\w+Property.*?update=update_live_preview', addon_code, re.DOTALL) + + expected_positioning_props = [ + 'anchor_point', 'position_mode', 'margin_x', 'margin_y', + 'margins_linked', 'offset_x', 'offset_y', 'manual_position_x', + 'manual_position_y', 'manual_position_unit', 'constrain_to_canvas' + ] + + missing_updates = [] + for prop in expected_positioning_props: + if prop not in update_properties: + missing_updates.append(prop) + + if missing_updates: + self.issues_found.append(f"āŒ Properties missing live preview updates: {missing_updates}") + else: + print("āœ… All positioning properties have live preview updates") + + # Check for visual feedback properties + visual_props = ['show_position_guides', 'show_text_bounds', 'show_canvas_grid'] + missing_visual_updates = [] + for prop in visual_props: + if prop not in update_properties: + missing_visual_updates.append(prop) + + if missing_visual_updates: + self.issues_found.append(f"āŒ Visual feedback properties missing updates: {missing_visual_updates}") + else: + print("āœ… All visual feedback properties have live preview updates") + + def run_compatibility_tests(self): + """Run all compatibility and UI integration tests""" + print("šŸ” UI INTEGRATION & COMPATIBILITY TEST SUITE") + print("=" * 55) + + try: + self.analyze_ui_layout_structure() + self.test_backward_compatibility_integration() + self.analyze_property_update_chains() + + except Exception as e: + print(f"āŒ Critical error during compatibility testing: {e}") + import traceback + traceback.print_exc() + + self.generate_compatibility_report() + + def generate_compatibility_report(self): + """Generate compatibility and UI integration report""" + print("\n" + "=" * 60) + print("šŸ“Š UI INTEGRATION & COMPATIBILITY REPORT") + print("=" * 60) + + total_issues = len(self.issues_found) + total_warnings = len(self.warnings) + + print(f"šŸ“ˆ SUMMARY:") + print(f" 🚨 Critical Issues: {total_issues}") + print(f" āš ļø Warnings: {total_warnings}") + + if self.issues_found: + print(f"\n🚨 CRITICAL ISSUES FOUND ({total_issues}):") + for issue in self.issues_found: + print(f" {issue}") + + if self.warnings: + print(f"\nāš ļø WARNINGS ({total_warnings}):") + for warning in self.warnings: + print(f" {warning}") + + if not self.issues_found and not self.warnings: + print("\nšŸŽ‰ NO CRITICAL ISSUES OR WARNINGS FOUND!") + print(" The UI integration and compatibility appear to be well implemented.") + + # Recommendations based on findings + print(f"\nšŸŽÆ RECOMMENDATIONS:") + + if total_issues > 0: + print(" 1. 🚨 Address all critical issues before deployment") + print(" 2. Test UI elements thoroughly in actual Blender environment") + + if total_warnings > 0: + print(" 3. āš ļø Review and address warnings for robustness") + print(" 4. Add explicit error handling for edge cases") + + print(" 5. āœ… Add automated UI tests to prevent regressions") + print(" 6. āœ… Test backward compatibility with existing presets") + print(" 7. āœ… Validate all property ranges in actual use cases") + + return { + 'critical_issues': total_issues, + 'warnings': total_warnings, + 'issues_list': self.issues_found, + 'warnings_list': self.warnings + } + +if __name__ == "__main__": + print("šŸš€ Starting UI Integration & Compatibility Test Suite...") + test_suite = UICompatibilityTestSuite() + results = test_suite.run_compatibility_tests() + + print(f"\n✨ UI & Compatibility testing completed!") \ No newline at end of file