diff --git a/Archive.zip b/Archive.zip index 73e6992..913044f 100644 Binary files a/Archive.zip and b/Archive.zip differ diff --git a/__init__.py b/__init__.py index 75ff392..9e62c6a 100644 --- a/__init__.py +++ b/__init__.py @@ -908,12 +908,147 @@ def generate_normal_map_from_alpha(img, strength=1.0, blur_radius=1.0, invert=Fa traceback.print_exc() return None +# ============================================================================ +# TEXT FITTING HELPER FUNCTIONS +# ============================================================================ + +def calculate_available_text_area(base_width, base_height, overlay_areas, margin_x, margin_y, canvas_width, canvas_height): + """Calculate the largest available area for text placement avoiding absolute positioned overlays + + Args: + base_width: Available width without overlay consideration + base_height: Available height without overlay consideration + overlay_areas: List of overlay area dictionaries with x, y, width, height + margin_x: Horizontal margin in pixels + margin_y: Vertical margin in pixels + canvas_width: Total canvas width + canvas_height: Total canvas height + + Returns: + Tuple of (available_width, available_height) for text fitting + """ + if not overlay_areas: + # No overlays to avoid, return base dimensions + return base_width, base_height + + # Define the text fitting area boundaries (respecting margins) + text_area_x = margin_x + text_area_y = margin_y + text_area_width = base_width + text_area_height = base_height + + print(f"DEBUG TEXT AREA: Text fitting area bounds: ({text_area_x}, {text_area_y}) {text_area_width}x{text_area_height}") + + # Find overlays that intersect with the text area + conflicting_overlays = [] + for overlay in overlay_areas: + overlay_x = overlay['x'] + overlay_y = overlay['y'] + overlay_w = overlay['width'] + overlay_h = overlay['height'] + + # Check if overlay intersects with text area + if (overlay_x < text_area_x + text_area_width and + overlay_x + overlay_w > text_area_x and + overlay_y < text_area_y + text_area_height and + overlay_y + overlay_h > text_area_y): + + conflicting_overlays.append(overlay) + print(f"DEBUG TEXT AREA: Conflicting overlay at ({overlay_x}, {overlay_y}) size {overlay_w}x{overlay_h}") + + if not conflicting_overlays: + # No conflicting overlays, return base dimensions + return base_width, base_height + + # Strategy 1: Try to find the largest horizontal strip that avoids overlays + best_width = 0 + best_height = 0 + + # Divide the text area into horizontal strips and find the largest clear one + strip_height = max(1, base_height // 10) # Test 10 horizontal strips + for strip_y in range(text_area_y, text_area_y + text_area_height - strip_height + 1, strip_height): + strip_bottom = min(strip_y + strip_height, text_area_y + text_area_height) + actual_strip_height = strip_bottom - strip_y + + # Check if this strip intersects with any overlay + strip_clear = True + for overlay in conflicting_overlays: + overlay_x = overlay['x'] + overlay_y = overlay['y'] + overlay_w = overlay['width'] + overlay_h = overlay['height'] + + # Check vertical intersection with this strip + if (overlay_y < strip_bottom and overlay_y + overlay_h > strip_y): + strip_clear = False + break + + if strip_clear: + # This strip is clear, use full width + strip_area = text_area_width * actual_strip_height + if strip_area > best_width * best_height: + best_width = text_area_width + best_height = actual_strip_height + print(f"DEBUG TEXT AREA: Found clear horizontal strip: {best_width}x{best_height}") + + # Strategy 2: Try vertical strips if horizontal didn't work well + if best_width * best_height < (text_area_width * text_area_height * 0.5): # Less than 50% of original area + strip_width = max(1, base_width // 10) # Test 10 vertical strips + for strip_x in range(text_area_x, text_area_x + text_area_width - strip_width + 1, strip_width): + strip_right = min(strip_x + strip_width, text_area_x + text_area_width) + actual_strip_width = strip_right - strip_x + + # Check if this strip intersects with any overlay + strip_clear = True + for overlay in conflicting_overlays: + overlay_x = overlay['x'] + overlay_y = overlay['y'] + overlay_w = overlay['width'] + overlay_h = overlay['height'] + + # Check horizontal intersection with this strip + if (overlay_x < strip_right and overlay_x + overlay_w > strip_x): + strip_clear = False + break + + if strip_clear: + # This strip is clear, use full height + strip_area = actual_strip_width * text_area_height + if strip_area > best_width * best_height: + best_width = actual_strip_width + best_height = text_area_height + print(f"DEBUG TEXT AREA: Found clear vertical strip: {best_width}x{best_height}") + + # Strategy 3: Fallback - reduce dimensions by estimated overlay coverage + if best_width * best_height == 0: + # Calculate total overlay area coverage + total_overlay_area = sum(overlay['width'] * overlay['height'] for overlay in conflicting_overlays) + text_area_total = text_area_width * text_area_height + coverage_ratio = min(0.8, total_overlay_area / text_area_total) # Cap at 80% + + # Reduce both dimensions proportionally + reduction_factor = max(0.2, 1.0 - coverage_ratio) # Minimum 20% of original + best_width = int(text_area_width * reduction_factor) + best_height = int(text_area_height * reduction_factor) + print(f"DEBUG TEXT AREA: Fallback proportional reduction: {best_width}x{best_height} (factor: {reduction_factor:.2f})") + + # Ensure minimum dimensions + best_width = max(50, best_width) # Minimum 50px width + best_height = max(20, best_height) # Minimum 20px height + + print(f"DEBUG TEXT AREA: Final available text area: {best_width}x{best_height}") + return best_width, best_height + # ============================================================================ # ENHANCED POSITIONING FUNCTIONS # ============================================================================ -def calculate_text_position(props, canvas_width, canvas_height, text_width, text_height): - """Calculate text position based on enhanced positioning properties""" +def calculate_text_position(props, canvas_width, canvas_height, text_width, text_height, overlay_areas=None, scale_factor=1.0, prepend_width=0): + """Calculate text position based on enhanced positioning properties with overlay avoidance + + Args: + prepend_width: Total width occupied by prepend overlays (shifts text to the right) + """ if props.position_mode == 'MANUAL': # Manual positioning mode @@ -924,28 +1059,55 @@ def calculate_text_position(props, canvas_width, canvas_height, text_width, text x = int(props.manual_position_x) y = int(props.manual_position_y) else: - # Preset anchor-based positioning + # Preset anchor-based positioning with overlay avoidance 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 text fitting is enabled and we have overlay areas, calculate available space + if props.enable_text_fitting and overlay_areas: + print(f"DEBUG TEXT POSITION: Text fitting enabled with {len(overlay_areas)} overlay areas to avoid") - 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 + # Calculate base available space with margins + base_available_width = canvas_width - (2 * margin_x_px) + base_available_height = canvas_height - (2 * margin_y_px) + # Get the available area that avoids overlays + available_width, available_height = calculate_available_text_area( + base_available_width, base_available_height, + overlay_areas, + margin_x_px, margin_y_px, canvas_width, canvas_height + ) + + print(f"DEBUG TEXT POSITION: Base available: {base_available_width}x{base_available_height}") + print(f"DEBUG TEXT POSITION: Final available (after overlay avoidance): {available_width}x{available_height}") + + # Find the best clear area for text placement + best_position = find_best_text_position_in_available_area( + props, canvas_width, canvas_height, text_width, text_height, + overlay_areas, margin_x_px, margin_y_px, available_width, available_height + ) + + if best_position: + x, y = best_position + print(f"DEBUG TEXT POSITION: Found clear position at ({x}, {y}) avoiding overlays") + else: + # Fallback to standard positioning if no clear area found + print(f"DEBUG TEXT POSITION: No clear area found, using standard positioning with prepend_width={prepend_width}") + x, y = calculate_standard_anchor_position( + props, canvas_width, canvas_height, text_width, text_height, + margin_x_px, margin_y_px, prepend_width + ) + else: + # Standard anchor-based positioning (no overlay avoidance) + print(f"DEBUG TEXT POSITION: Using standard positioning with prepend_width={prepend_width}") + x, y = calculate_standard_anchor_position( + props, canvas_width, canvas_height, text_width, text_height, + margin_x_px, margin_y_px, prepend_width + ) + # Apply fine-tuning offsets - x = base_x + props.offset_x - y = base_y + props.offset_y + x += props.offset_x + y += props.offset_y # Apply canvas constraints if enabled if props.constrain_to_canvas: @@ -954,6 +1116,115 @@ def calculate_text_position(props, canvas_width, canvas_height, text_width, text return x, y +def calculate_standard_anchor_position(props, canvas_width, canvas_height, text_width, text_height, margin_x_px, margin_y_px, prepend_width=0): + """Calculate standard anchor-based position without overlay avoidance + + Args: + prepend_width: Total width occupied by prepend overlays (shifts text to the right) + """ + # Calculate base position based on anchor point + if 'LEFT' in props.anchor_point: + base_x = margin_x_px + prepend_width + elif 'RIGHT' in props.anchor_point: + base_x = canvas_width - text_width - margin_x_px + else: # CENTER + base_x = (canvas_width - text_width) // 2 + prepend_width + + 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 + + return base_x, base_y + +def find_best_text_position_in_available_area(props, canvas_width, canvas_height, text_width, text_height, overlay_areas, margin_x_px, margin_y_px, available_width, available_height): + """Find the best position for text within available area that avoids overlays""" + + # Define the search area (respecting margins) + search_area_x = margin_x_px + search_area_y = margin_y_px + search_area_width = canvas_width - (2 * margin_x_px) + search_area_height = canvas_height - (2 * margin_y_px) + + print(f"DEBUG TEXT POSITION: Search area: ({search_area_x}, {search_area_y}) {search_area_width}x{search_area_height}") + + # Try anchor-based positions first, but within clear areas + anchor_positions = [] + + # Calculate preferred positions based on anchor point + if 'LEFT' in props.anchor_point: + pref_x = search_area_x + elif 'RIGHT' in props.anchor_point: + pref_x = search_area_x + search_area_width - text_width + else: # CENTER + pref_x = search_area_x + (search_area_width - text_width) // 2 + + if 'TOP' in props.anchor_point: + pref_y = search_area_y + elif 'BOTTOM' in props.anchor_point: + pref_y = search_area_y + search_area_height - text_height + else: # MIDDLE + pref_y = search_area_y + (search_area_height - text_height) // 2 + + # Add preferred position + anchor_positions.append((pref_x, pref_y, "preferred")) + + # Add alternative positions + # Top-left, top-center, top-right + anchor_positions.append((search_area_x, search_area_y, "top-left")) + anchor_positions.append((search_area_x + (search_area_width - text_width) // 2, search_area_y, "top-center")) + anchor_positions.append((search_area_x + search_area_width - text_width, search_area_y, "top-right")) + + # Middle-left, middle-center, middle-right + middle_y = search_area_y + (search_area_height - text_height) // 2 + anchor_positions.append((search_area_x, middle_y, "middle-left")) + anchor_positions.append((search_area_x + (search_area_width - text_width) // 2, middle_y, "middle-center")) + anchor_positions.append((search_area_x + search_area_width - text_width, middle_y, "middle-right")) + + # Bottom-left, bottom-center, bottom-right + bottom_y = search_area_y + search_area_height - text_height + anchor_positions.append((search_area_x, bottom_y, "bottom-left")) + anchor_positions.append((search_area_x + (search_area_width - text_width) // 2, bottom_y, "bottom-center")) + anchor_positions.append((search_area_x + search_area_width - text_width, bottom_y, "bottom-right")) + + # Test each position for conflicts with overlays + for test_x, test_y, position_name in anchor_positions: + # Ensure position is within canvas bounds + test_x = max(0, min(test_x, canvas_width - text_width)) + test_y = max(0, min(test_y, canvas_height - text_height)) + + # Check if this position conflicts with any overlay + text_rect = { + 'x': test_x, + 'y': test_y, + 'width': text_width, + 'height': text_height + } + + conflicts = False + for overlay in overlay_areas: + if rectangles_overlap(text_rect, overlay): + conflicts = True + print(f"DEBUG TEXT POSITION: Position {position_name} at ({test_x}, {test_y}) conflicts with overlay at ({overlay['x']}, {overlay['y']})") + break + + if not conflicts: + print(f"DEBUG TEXT POSITION: Found clear position {position_name} at ({test_x}, {test_y})") + return (test_x, test_y) + + # If no clear position found, return None + print(f"DEBUG TEXT POSITION: No clear position found among {len(anchor_positions)} tested positions") + return None + +def rectangles_overlap(rect1, rect2): + """Check if two rectangles overlap""" + return not (rect1['x'] + rect1['width'] <= rect2['x'] or + rect2['x'] + rect2['width'] <= rect1['x'] or + rect1['y'] + rect1['height'] <= rect2['y'] or + rect2['y'] + rect2['height'] <= rect1['y']) + def sync_margin_values(props, changed_margin): """Synchronize margin values when linked""" if props.margins_linked: @@ -1252,6 +1523,169 @@ def draw_multiline_text_with_stroke(draw, lines, font, text_color, props, canvas draw.text((10, 10), lines[0], fill=text_color, font=font) +def calculate_smart_content_fitting(props, width, height, scale_factor): + """Calculate smart content fitting for text + overlays to prevent overflow""" + try: + from PIL import Image, ImageDraw, ImageFont + + # Initial font size calculation + base_font_size = max(6, int(props.font_size * scale_factor)) + + # Load font for measurement + def load_measurement_font(size): + font = None + if props.use_custom_font and props.custom_font_path and os.path.exists(props.custom_font_path): + try: + font = ImageFont.truetype(props.custom_font_path, size) + except: + pass + if not font and props.font_path != "default" and os.path.exists(props.font_path): + try: + font = ImageFont.truetype(props.font_path, size) + except: + pass + if not font: + font = ImageFont.load_default() + return font + + font = load_measurement_font(base_font_size) + + # Get text content + main_text = props.text if props.text else "" + prepend_text = props.prepend_text.strip() if props.prepend_text else "" + append_text = props.append_text.strip() if props.append_text else "" + + # Calculate combined text for horizontal layout + if props.prepend_append_layout == 'HORIZONTAL': + text_parts = [] + if prepend_text: + text_parts.append(prepend_text) + prepend_margin_px = int((props.prepend_margin / 100.0) * base_font_size) + space_count = max(1, int(prepend_margin_px / (base_font_size * 0.3))) + text_parts.append(" " * space_count) + text_parts.append(main_text) + if append_text: + append_margin_px = int((props.append_margin / 100.0) * base_font_size) + space_count = max(1, int(append_margin_px / (base_font_size * 0.3))) + text_parts.append(" " * space_count) + text_parts.append(append_text) + combined_text = "".join(text_parts) + else: + combined_text = main_text # For vertical layout, measure main text only + + # Measure text dimensions + temp_draw = ImageDraw.Draw(Image.new('RGBA', (100, 100))) + try: + text_bbox = temp_draw.textbbox((0, 0), combined_text, font=font) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + except: + text_width = len(combined_text) * int(base_font_size * 0.6) + text_height = base_font_size + + # Calculate overlay dimensions + enabled_overlays = [overlay for overlay in props.image_overlays if overlay.enabled] + prepend_overlays = [o for o in enabled_overlays if o.positioning_mode == 'PREPEND' and o.image_path and os.path.exists(o.image_path)] + append_overlays = [o for o in enabled_overlays if o.positioning_mode == 'APPEND' and o.image_path and os.path.exists(o.image_path)] + + prepend_total_width = 0 + append_total_width = 0 + + # Calculate prepend overlay widths + for i, overlay in enumerate(prepend_overlays): + try: + overlay_img = Image.open(overlay.image_path).convert("RGBA") + if overlay.scale != 1.0: + final_scale = max(0.01, min(10.0, overlay.scale * scale_factor)) + new_width = max(1, int(overlay_img.width * final_scale)) + overlay_width = new_width + else: + overlay_width = overlay_img.width + + prepend_total_width += overlay_width + if i == 0: # First overlay gets text spacing + text_spacing = max(0, (overlay.text_spacing / 100.0) * width) + prepend_total_width += text_spacing + else: # Subsequent overlays get image spacing + image_spacing = max(0, (overlay.image_spacing / 100.0) * width) + prepend_total_width += image_spacing + except: + # Fallback if image can't be loaded + prepend_total_width += base_font_size + + # Calculate append overlay widths + for i, overlay in enumerate(append_overlays): + try: + overlay_img = Image.open(overlay.image_path).convert("RGBA") + if overlay.scale != 1.0: + final_scale = max(0.01, min(10.0, overlay.scale * scale_factor)) + new_width = max(1, int(overlay_img.width * final_scale)) + overlay_width = new_width + else: + overlay_width = overlay_img.width + + if i == 0: # First overlay gets text spacing + text_spacing = max(0, (overlay.text_spacing / 100.0) * width) + append_total_width += text_spacing + overlay_width + else: # Subsequent overlays get image spacing + image_spacing = max(0, (overlay.image_spacing / 100.0) * width) + append_total_width += image_spacing + overlay_width + except: + # Fallback if image can't be loaded + append_total_width += base_font_size + + # Calculate total content width + total_content_width = prepend_total_width + text_width + append_total_width + + # Add safety margins (10% of canvas width) + safety_margin = int(width * 0.1) + available_width = width - (2 * safety_margin) + + print(f"SMART FITTING: Text width: {text_width}, Prepend: {prepend_total_width}, Append: {append_total_width}") + print(f"SMART FITTING: Total content: {total_content_width}, Available: {available_width}") + + # Determine if smart fitting is needed + fitting_needed = total_content_width > available_width + adjusted_font_size = base_font_size + adjusted_scale_factor = scale_factor + fitting_strategy = "NONE" + + if fitting_needed: + print(f"SMART FITTING: Content overflow detected! Applying smart fitting...") + + # Strategy 1: Proportional scaling + if total_content_width > 0: + scale_ratio = available_width / total_content_width + # Don't scale below 50% of original size to maintain readability + scale_ratio = max(0.5, scale_ratio) + adjusted_font_size = max(6, int(base_font_size * scale_ratio)) + adjusted_scale_factor = scale_factor * scale_ratio + fitting_strategy = "PROPORTIONAL_SCALE" + print(f"SMART FITTING: Applied {scale_ratio:.2f}x scaling, new font size: {adjusted_font_size}") + + return { + 'font_size': adjusted_font_size, + 'scale_factor': adjusted_scale_factor, + 'fitting_needed': fitting_needed, + 'fitting_strategy': fitting_strategy, + 'original_content_width': total_content_width, + 'available_width': available_width, + 'scale_ratio': adjusted_scale_factor / scale_factor if scale_factor > 0 else 1.0 + } + + except Exception as e: + print(f"ERROR in smart content fitting: {e}") + # Return safe defaults + return { + 'font_size': max(6, int(props.font_size * scale_factor)), + 'scale_factor': scale_factor, + 'fitting_needed': False, + 'fitting_strategy': "ERROR_FALLBACK", + 'original_content_width': 0, + 'available_width': width, + 'scale_ratio': 1.0 + } + def generate_texture_image(props, width, height): """Generate the actual texture image with overlays - width and height are REQUIRED Returns: (diffuse_image, normal_map_image) or (diffuse_image, None) if normal map disabled""" @@ -1270,12 +1704,18 @@ 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}") print(f"DEBUG: Normal map generation enabled: {props.generate_normal_map}") - # Scale font size proportionally for preview + # Calculate smart content fitting scale_factor = width / props.texture_width - base_font_size = max(6, int(props.font_size * scale_factor)) - padding = max(0, int(props.padding * scale_factor)) + fitting_result = calculate_smart_content_fitting(props, width, height, scale_factor) - print(f"DEBUG: Scale factor: {scale_factor}, scaled font size: {base_font_size}, scaled padding: {padding}") + # Use adjusted values from smart fitting + base_font_size = fitting_result['font_size'] + adjusted_scale_factor = fitting_result['scale_factor'] + padding = max(0, int(props.padding * adjusted_scale_factor)) + + print(f"DEBUG: Original scale factor: {scale_factor}, adjusted: {adjusted_scale_factor}") + print(f"DEBUG: Smart fitting applied: {fitting_result['fitting_strategy']}") + print(f"DEBUG: Adjusted font size: {base_font_size}, scaled padding: {padding}") # Create base image with background if props.background_transparent: @@ -1311,9 +1751,8 @@ def generate_texture_image(props, width, height): elif overlay.positioning_mode == 'APPEND': append_overlays.append(overlay) - # Sort prepend and append overlays by sequence_index - prepend_overlays.sort(key=lambda x: x.sequence_index) - append_overlays.sort(key=lambda x: x.sequence_index) + # Prepend and append overlays are already in order based on list position + # No sorting needed since we removed sequence_index - order is determined by list position # PLACEHOLDER: text_bbox will be calculated after text setup text_bbox = None @@ -1380,12 +1819,12 @@ def generate_texture_image(props, width, height): for i, overlay in enumerate(prepend_overlays): all_overlays_with_mode.append((overlay, 'PREPEND', prepend_x_offset)) if text_bbox: - # Calculate spacing for next overlay using cached loading - temp_overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, scale_factor) + # Calculate spacing for next overlay using cached loading with adjusted scale factor + temp_overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, adjusted_scale_factor) if temp_overlay_img: - # EDGE CASE: Validate spacing values to prevent extreme spacing - text_spacing = max(0, min(1000, (overlay.text_spacing / 100.0) * base_font_size)) - image_spacing = max(0, min(1000, (overlay.image_spacing / 100.0) * base_font_size)) + # EDGE CASE: Validate spacing values to prevent extreme spacing (percentage of canvas width) + text_spacing = max(0, min(1000, (overlay.text_spacing / 100.0) * width)) + image_spacing = max(0, min(1000, (overlay.image_spacing / 100.0) * width)) # Add image width plus spacing for next position prepend_x_offset += temp_overlay_img.width + (text_spacing if i == 0 else image_spacing) @@ -1399,12 +1838,12 @@ def generate_texture_image(props, width, height): for i, overlay in enumerate(append_overlays): all_overlays_with_mode.append((overlay, 'APPEND', append_x_offset)) if text_bbox: - # Calculate spacing for next overlay using cached loading - temp_overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, scale_factor) + # Calculate spacing for next overlay using cached loading with adjusted scale factor + temp_overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, adjusted_scale_factor) if temp_overlay_img: - # EDGE CASE: Validate spacing values to prevent extreme spacing - text_spacing = max(0, min(1000, (overlay.text_spacing / 100.0) * base_font_size)) - image_spacing = max(0, min(1000, (overlay.image_spacing / 100.0) * base_font_size)) + # EDGE CASE: Validate spacing values to prevent extreme spacing (percentage of canvas width) + text_spacing = max(0, min(1000, (overlay.text_spacing / 100.0) * width)) + image_spacing = max(0, min(1000, (overlay.image_spacing / 100.0) * width)) # Calculate position for this overlay, then update offset for next if i > 0: @@ -1423,71 +1862,8 @@ def generate_texture_image(props, width, height): # Sort all overlays by z-index all_overlays_with_mode.sort(key=lambda x: x[0].z_index) - # Apply overlays using cached loading with comprehensive error handling - for overlay_data in all_overlays_with_mode: - overlay = overlay_data[0] - mode = overlay_data[1] - - # Use cached loading for better performance and error handling - overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, scale_factor) - - if overlay_img is None: - print(f"Skipping overlay {overlay.name}: {error}") - continue - - try: - # Apply rotation with error handling - if overlay.rotation != 0: - # EDGE CASE: Validate rotation range - rotation_angle = overlay.rotation % 360 # Normalize to 0-360 - if abs(rotation_angle) > 0.1: # Only rotate if meaningful angle - overlay_img = overlay_img.rotate(rotation_angle, expand=True) - - # Calculate position based on positioning mode - if mode == 'ABSOLUTE': - # Use existing absolute positioning (backward compatibility) - # EDGE CASE: Ensure position values are valid - x_position = max(0.0, min(1.0, overlay.x_position)) - y_position = max(0.0, min(1.0, overlay.y_position)) - x_pos = int(x_position * width - overlay_img.width / 2) - y_pos = int((1.0 - y_position) * height - overlay_img.height / 2) - elif mode == 'PREPEND' and text_bbox: - # Position to the left of text bounds - x_offset = overlay_data[2] if len(overlay_data) > 2 else 0 - x_pos = int(text_bbox[0] - x_offset - overlay_img.width) - y_pos = int(text_bbox[1] + (text_bbox[3] - text_bbox[1] - overlay_img.height) / 2) - elif mode == 'APPEND' and text_bbox: - # Position to the right of text bounds - x_offset = overlay_data[2] if len(overlay_data) > 2 else 0 - x_pos = int(text_bbox[2] + x_offset) - y_pos = int(text_bbox[1] + (text_bbox[3] - text_bbox[1] - overlay_img.height) / 2) - else: - # Fallback to absolute positioning - x_position = max(0.0, min(1.0, overlay.x_position)) - y_position = max(0.0, min(1.0, overlay.y_position)) - x_pos = int(x_position * width - overlay_img.width / 2) - y_pos = int((1.0 - y_position) * height - overlay_img.height / 2) - - # EDGE CASE: Ensure positions are within bounds and handle negative dimensions gracefully - if overlay_img.width <= 0 or overlay_img.height <= 0: - print(f"Warning: Overlay {overlay.name} has invalid dimensions: {overlay_img.width}x{overlay_img.height}") - continue - - x_pos = max(0, min(x_pos, width - overlay_img.width)) - y_pos = max(0, min(y_pos, height - overlay_img.height)) - - # EDGE CASE: Skip overlay if it's completely outside the canvas - if x_pos >= width or y_pos >= height or x_pos + overlay_img.width <= 0 or y_pos + overlay_img.height <= 0: - print(f"Warning: Overlay {overlay.name} is positioned outside canvas bounds, skipping") - continue - - elements.append((overlay.z_index, 'image', overlay_img, x_pos, y_pos)) - print(f"Successfully added overlay {overlay.name} ({mode}) at z-index {overlay.z_index}, pos ({x_pos}, {y_pos})") - - except Exception as e: - print(f"Error processing overlay {overlay.name}: {e}") - import traceback - traceback.print_exc() + # OVERLAY PROCESSING MOVED: This section is now processed after text positioning + # to ensure text_bbox is available for prepend/append positioning modes # Create text element (z-index 0) text_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) @@ -1618,6 +1994,34 @@ def generate_texture_image(props, width, height): # REMOVED: Duplicate text processing code block - already handled above + # Calculate space occupied by absolute positioned overlays (MOVED OUT OF TEXT FITTING BLOCK) + # This is needed for both text fitting and text positioning, so it must be available regardless + overlay_occupied_areas = [] + enabled_overlays = [overlay for overlay in props.image_overlays if overlay.enabled] + absolute_overlays = [o for o in enabled_overlays if o.positioning_mode == 'ABSOLUTE' and o.image_path and os.path.exists(o.image_path)] + + for overlay in absolute_overlays: + try: + # Load overlay image to get dimensions + overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, scale_factor) + if overlay_img: + # Calculate absolute position + x_position = max(0.0, min(1.0, overlay.x_position)) + y_position = max(0.0, min(1.0, overlay.y_position)) + overlay_x = int(x_position * width - overlay_img.width / 2) + overlay_y = int((1.0 - y_position) * height - overlay_img.height / 2) + + # Store occupied area (x, y, width, height) + overlay_occupied_areas.append({ + 'x': overlay_x, + 'y': overlay_y, + 'width': overlay_img.width, + 'height': overlay_img.height + }) + print(f"DEBUG OVERLAY AREAS: Absolute overlay at ({overlay_x}, {overlay_y}) size {overlay_img.width}x{overlay_img.height}") + except Exception as e: + print(f"DEBUG OVERLAY AREAS: Error processing absolute overlay {overlay.name}: {e}") + # Apply text fitting if enabled if props.enable_text_fitting: print(f"DEBUG TEXT FITTING: Starting text fitting process") @@ -1625,13 +2029,23 @@ def generate_texture_image(props, width, height): print(f"DEBUG TEXT FITTING: Initial font size: {font_size_pixels}") print(f"DEBUG TEXT FITTING: Fitting mode: {props.text_fitting_mode}") - # Calculate available space with margin + # Calculate available space with margin and respect for absolute positioned overlays margin_px_x = int((props.text_fitting_margin / 100.0) * width) margin_px_y = int((props.text_fitting_margin / 100.0) * height) - available_width = width - (2 * margin_px_x) - available_height = height - (2 * margin_px_y) + base_available_width = width - (2 * margin_px_x) + base_available_height = height - (2 * margin_px_y) - print(f"DEBUG TEXT FITTING: Available space: {available_width}x{available_height} (margin: {margin_px_x}x{margin_px_y})") + + # Calculate effective available area by finding the largest contiguous rectangle + # that doesn't overlap with any absolute positioned overlays (use already calculated overlay_occupied_areas) + available_width, available_height = calculate_available_text_area( + base_available_width, base_available_height, + overlay_occupied_areas, + margin_px_x, margin_px_y, width, height + ) + + print(f"DEBUG TEXT FITTING: Base available space: {base_available_width}x{base_available_height} (margin: {margin_px_x}x{margin_px_y})") + print(f"DEBUG TEXT FITTING: Final available space (after overlay exclusion): {available_width}x{available_height}") # Apply scale factor to min/max font sizes min_font_size_scaled = max(4, int(props.min_font_size * scale_factor)) @@ -1774,8 +2188,42 @@ def generate_texture_image(props, width, height): # ENHANCED POSITIONING SYSTEM - Replaces hardcoded vertical centering # ============================================================================ - # Use enhanced positioning system - x, y = calculate_text_position(props, width, height, text_width, text_height) + # Calculate prepend width for text positioning adjustment + prepend_width = 0 + enabled_overlays = [overlay for overlay in props.image_overlays if overlay.enabled] + prepend_overlays = [o for o in enabled_overlays if o.positioning_mode == 'PREPEND' and o.image_path and os.path.exists(o.image_path)] + + print(f"DEBUG TEXT POSITION: Calculating prepend width from {len(prepend_overlays)} prepend overlays") + + for i, overlay in enumerate(prepend_overlays): + try: + # Load overlay image to get dimensions using the cached loader + overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, adjusted_scale_factor) + if overlay_img: + # Add image width plus percentage-based spacing + prepend_width += overlay_img.width + + # Add text spacing for the first overlay, image spacing for subsequent ones + # Both spacings are calculated as percentage of canvas width, not font size + if i == 0: + text_spacing_px = max(0, (overlay.text_spacing / 100.0) * width) + prepend_width += text_spacing_px + print(f"DEBUG TEXT POSITION: Prepend overlay {i} adds {overlay_img.width}px + {text_spacing_px}px text spacing (percentage-based)") + else: + image_spacing_px = max(0, (overlay.image_spacing / 100.0) * width) + prepend_width += image_spacing_px + print(f"DEBUG TEXT POSITION: Prepend overlay {i} adds {overlay_img.width}px + {image_spacing_px}px image spacing (percentage-based)") + + print(f"DEBUG TEXT POSITION: Total prepend width: {prepend_width}px") + else: + print(f"DEBUG TEXT POSITION: Warning - Could not load prepend overlay: {error}") + except Exception as e: + print(f"DEBUG TEXT POSITION: Error calculating prepend width for overlay {i}: {e}") + + print(f"DEBUG TEXT POSITION: Final prepend width calculated: {prepend_width}px (percentage-based spacing)") + + # Use enhanced positioning system with overlay avoidance and prepend width + x, y = calculate_text_position(props, width, height, text_width, text_height, overlay_occupied_areas, scale_factor, prepend_width) # Apply legacy text_align for backward compatibility if using MIDDLE_CENTER anchor if props.anchor_point == 'MIDDLE_CENTER' and props.position_mode == 'PRESET': @@ -1928,6 +2376,127 @@ def generate_texture_image(props, width, height): print(f"DEBUG VERTICAL LAYOUT - Vertical text rendering with stroke completed") + # ============================================================================ + # OVERLAY PROCESSING - MOVED TO CORRECT POSITION AFTER TEXT POSITIONING + # ============================================================================ + + # Now that text positioning is complete and text_bbox is calculated, + # process overlays that depend on text position (PREPEND/APPEND modes) + + # Apply overlays using cached loading with comprehensive error handling + for overlay_data in all_overlays_with_mode: + overlay = overlay_data[0] + mode = overlay_data[1] + + # Use cached loading for better performance and error handling with adjusted scale factor + overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, adjusted_scale_factor) + + if overlay_img is None: + print(f"Skipping overlay {overlay.name}: {error}") + continue + + try: + # Apply rotation with error handling + if overlay.rotation != 0: + # EDGE CASE: Validate rotation range + rotation_angle = overlay.rotation % 360 # Normalize to 0-360 + if abs(rotation_angle) > 0.1: # Only rotate if meaningful angle + overlay_img = overlay_img.rotate(rotation_angle, expand=True) + + # Calculate position based on positioning mode + if mode == 'ABSOLUTE': + # Use existing absolute positioning (backward compatibility) + # EDGE CASE: Ensure position values are valid + x_position = max(0.0, min(1.0, overlay.x_position)) + y_position = max(0.0, min(1.0, overlay.y_position)) + x_pos = int(x_position * width - overlay_img.width / 2) + y_pos = int((1.0 - y_position) * height - overlay_img.height / 2) + elif mode == 'PREPEND': + if text_bbox: + # Calculate text spacing in pixels from percentage of canvas width (not font size) + text_spacing_px = int((overlay.text_spacing / 100.0) * width) + # Position to the left of text bounds with proper text spacing + x_pos = int(text_bbox[0] - text_spacing_px - overlay_img.width) + y_pos = int(text_bbox[1] + (text_bbox[3] - text_bbox[1] - overlay_img.height) / 2) + print(f"DEBUG PREPEND: Positioning overlay at ({x_pos}, {y_pos}) with text_spacing={text_spacing_px}px (percentage-based) relative to text_bbox {text_bbox}") + else: + # No text bbox available, position at left side of canvas + x_pos = 10 # Small margin from left edge + y_pos = int((height - overlay_img.height) / 2) # Vertically centered + print(f"DEBUG PREPEND: No text_bbox available, positioning at left side: ({x_pos}, {y_pos})") + elif mode == 'APPEND': + if text_bbox: + # Calculate text spacing in pixels from percentage of canvas width (not font size) + text_spacing_px = int((overlay.text_spacing / 100.0) * width) + # Position to the right of text bounds with proper text spacing + x_pos = int(text_bbox[2] + text_spacing_px) + y_pos = int(text_bbox[1] + (text_bbox[3] - text_bbox[1] - overlay_img.height) / 2) + print(f"DEBUG APPEND: Positioning overlay at ({x_pos}, {y_pos}) with text_spacing={text_spacing_px}px (percentage-based) relative to text_bbox {text_bbox}") + else: + # No text bbox available, position at right side of canvas + x_pos = int(width - overlay_img.width - 10) # Small margin from right edge + y_pos = int((height - overlay_img.height) / 2) # Vertically centered + print(f"DEBUG APPEND: No text_bbox available, positioning at right side: ({x_pos}, {y_pos})") + else: + # Fallback to absolute positioning + x_position = max(0.0, min(1.0, overlay.x_position)) + y_position = max(0.0, min(1.0, overlay.y_position)) + x_pos = int(x_position * width - overlay_img.width / 2) + y_pos = int((1.0 - y_position) * height - overlay_img.height / 2) + + # EDGE CASE: Ensure positions are within bounds and handle negative dimensions gracefully + if overlay_img.width <= 0 or overlay_img.height <= 0: + print(f"Warning: Overlay {overlay.name} has invalid dimensions: {overlay_img.width}x{overlay_img.height}") + continue + + x_pos = max(0, min(x_pos, width - overlay_img.width)) + y_pos = max(0, min(y_pos, height - overlay_img.height)) + + # EDGE CASE: Skip overlay if it's completely outside the canvas + if x_pos >= width or y_pos >= height or x_pos + overlay_img.width <= 0 or y_pos + overlay_img.height <= 0: + print(f"Warning: Overlay {overlay.name} is positioned outside canvas bounds, skipping") + continue + + elements.append((overlay.z_index, 'image', overlay_img, x_pos, y_pos)) + print(f"Successfully added overlay {overlay.name} ({mode}) at z-index {overlay.z_index}, pos ({x_pos}, {y_pos})") + + except Exception as e: + print(f"Error processing overlay {overlay.name}: {e}") + import traceback + traceback.print_exc() + + # Handle vertical layout rendering with stroke + if props.prepend_append_layout == 'VERTICAL' and (prepend_text or append_text): + print(f"DEBUG VERTICAL LAYOUT - Rendering vertical text layout with stroke: {props.enable_stroke}") + + # Calculate starting Y position for vertical stack + current_y = y + + # Render prepend text if exists + if prepend_text: + print(f"DEBUG VERTICAL LAYOUT - Drawing prepend text: '{prepend_text}' at y={current_y}") + draw_text_with_stroke(draw, prepend_text, x, current_y, font, text_color, + props.enable_stroke, props.stroke_width, props.stroke_color, props.stroke_position) + prepend_bbox = draw.textbbox((0, 0), prepend_text, font=font) + prepend_height = prepend_bbox[3] - prepend_bbox[1] + current_y += prepend_height + prepend_margin_px + + # Render main text + print(f"DEBUG VERTICAL LAYOUT - Drawing main text: '{main_text}' at y={current_y}") + draw_text_with_stroke(draw, main_text, x, current_y, font, text_color, + props.enable_stroke, props.stroke_width, props.stroke_color, props.stroke_position) + main_bbox = draw.textbbox((0, 0), main_text, font=font) + main_height = main_bbox[3] - main_bbox[1] + current_y += main_height + append_margin_px + + # Render append text if exists + if append_text: + print(f"DEBUG VERTICAL LAYOUT - Drawing append text: '{append_text}' at y={current_y}") + draw_text_with_stroke(draw, append_text, x, current_y, font, text_color, + props.enable_stroke, props.stroke_width, props.stroke_color, props.stroke_position) + + print(f"DEBUG VERTICAL LAYOUT - Vertical text rendering with stroke completed") + # MULTILINE TEXT PROCESSING with stroke support elif props.enable_multiline: print("DEBUG MULTILINE: Processing multiline text with stroke support") @@ -2000,8 +2569,8 @@ def generate_texture_image(props, width, height): base_alpha_multiplier = props.shadow_color[3] if len(props.shadow_color) > 3 else 1.0 shadow_spread = getattr(props, 'shadow_spread', 1.0) # Fallback for compatibility # Apply shadow spread to the offset distances - shadow_offset_x_spread = int(shadow_offset_x * shadow_spread) - shadow_offset_y_spread = int(shadow_offset_y * shadow_spread) + shadow_offset_x_spread = int(props.shadow_offset_x * shadow_spread) + shadow_offset_y_spread = int(props.shadow_offset_y * shadow_spread) # Keep the original alpha multiplier (no intensity-based changes) final_alpha_multiplier = base_alpha_multiplier @@ -2404,29 +2973,22 @@ class TEXT_TEXTURE_ImageOverlay(PropertyGroup): ) text_spacing: FloatProperty( - name="Text Spacing", - description="Spacing between text and image as percentage of font size (0-200)", - default=50.0, + name="Text Spacing (%)", + description="Spacing between text and image as percentage of canvas width (0-100%)", + default=10.0, min=0.0, - max=200.0, + max=100.0, + subtype='PERCENTAGE', update=update_live_preview ) image_spacing: FloatProperty( - name="Image Spacing", - description="Spacing between consecutive images as percentage of font size (0-200)", - default=25.0, + name="Image Spacing (%)", + description="Spacing between consecutive images as percentage of canvas width (0-100%)", + default=5.0, min=0.0, - max=200.0, - update=update_live_preview - ) - - sequence_index: IntProperty( - name="Sequence Index", - description="Order index for positioning within append/prepend modes", - default=0, - min=0, - max=99, + max=100.0, + subtype='PERCENTAGE', update=update_live_preview ) @@ -5062,6 +5624,206 @@ class TEXT_TEXTURE_OT_show_migration_report(Operator): +class TEXT_TEXTURE_OT_duplicate_overlay(Operator): + """Duplicate image overlay""" + bl_idname = "text_texture.duplicate_overlay" + bl_label = "Duplicate Overlay" + bl_description = "Duplicate this image overlay" + bl_options = {'REGISTER', 'UNDO'} + + overlay_index: IntProperty() + + def execute(self, context): + props = context.scene.text_texture_props + + if 0 <= self.overlay_index < len(props.image_overlays): + source_overlay = props.image_overlays[self.overlay_index] + + # Debug logging + print(f"[DUPLICATE DEBUG] Duplicating overlay '{source_overlay.name}'") + print(f"[DUPLICATE DEBUG] Source properties:") + print(f"[DUPLICATE DEBUG] image_path: '{source_overlay.image_path}'") + print(f"[DUPLICATE DEBUG] enabled: {source_overlay.enabled}") + print(f"[DUPLICATE DEBUG] positioning_mode: '{source_overlay.positioning_mode}'") + print(f"[DUPLICATE DEBUG] scale: {source_overlay.scale}") + print(f"[DUPLICATE DEBUG] z_index: {source_overlay.z_index}") + + new_overlay = props.image_overlays.add() + + # Copy all properties with explicit verification + new_overlay.name = f"{source_overlay.name} Copy" + new_overlay.image_path = source_overlay.image_path + new_overlay.x_position = source_overlay.x_position + new_overlay.y_position = source_overlay.y_position + new_overlay.scale = source_overlay.scale + new_overlay.rotation = source_overlay.rotation + new_overlay.z_index = source_overlay.z_index + new_overlay.positioning_mode = source_overlay.positioning_mode + new_overlay.text_spacing = source_overlay.text_spacing + new_overlay.image_spacing = source_overlay.image_spacing + new_overlay.enabled = source_overlay.enabled + + # Verify the copy was successful + print(f"[DUPLICATE DEBUG] New overlay properties:") + print(f"[DUPLICATE DEBUG] name: '{new_overlay.name}'") + print(f"[DUPLICATE DEBUG] image_path: '{new_overlay.image_path}'") + print(f"[DUPLICATE DEBUG] enabled: {new_overlay.enabled}") + print(f"[DUPLICATE DEBUG] positioning_mode: '{new_overlay.positioning_mode}'") + print(f"[DUPLICATE DEBUG] scale: {new_overlay.scale}") + print(f"[DUPLICATE DEBUG] z_index: {new_overlay.z_index}") + + # Move to position after source + new_index = len(props.image_overlays) - 1 + target_index = min(self.overlay_index + 1, new_index) + + if new_index != target_index: + props.image_overlays.move(new_index, target_index) + print(f"[DUPLICATE DEBUG] Moved overlay from index {new_index} to {target_index}") + + props.active_overlay_index = target_index + + # Clear any cached images to force reload + print(f"[DUPLICATE DEBUG] Clearing image cache and forcing preview update...") + + # Clear the overlay image cache if it exists + if hasattr(generate_texture_image, 'overlay_image_cache'): + generate_texture_image.overlay_image_cache = {} + print(f"[DUPLICATE DEBUG] Cleared overlay image cache") + + # Set the updating flag to prevent recursive updates + props.is_updating = True + + try: + # Force immediate preview regeneration + print(f"[DUPLICATE DEBUG] Generating new preview...") + preview_result = generate_preview(context) + if preview_result: + print(f"[DUPLICATE DEBUG] Preview generated successfully") + else: + print(f"[DUPLICATE DEBUG] Preview generation failed") + + # Force UI redraw + for area in context.screen.areas: + area.tag_redraw() + + except Exception as e: + print(f"[DUPLICATE DEBUG] Error updating preview: {e}") + import traceback + traceback.print_exc() + finally: + props.is_updating = False + + print(f"[DUPLICATE DEBUG] Total overlays now: {len(props.image_overlays)}") + print(f"[DUPLICATE DEBUG] Duplication operation completed") + + self.report({'INFO'}, f"✅ Duplicated overlay: {source_overlay.name}") + else: + self.report({'ERROR'}, "Invalid overlay index") + return {'CANCELLED'} + + return {'FINISHED'} + +class TEXT_TEXTURE_OT_delete_overlay(Operator): + """Delete image overlay""" + bl_idname = "text_texture.delete_overlay" + bl_label = "Delete Overlay" + bl_description = "Delete this image overlay" + bl_options = {'REGISTER', 'UNDO'} + + overlay_index: IntProperty() + + def execute(self, context): + props = context.scene.text_texture_props + + print(f"[DELETE DEBUG] Deleting overlay at index {self.overlay_index}") + + if 0 <= self.overlay_index < len(props.image_overlays): + overlay_name = props.image_overlays[self.overlay_index].name + print(f"[DELETE DEBUG] Deleting overlay: '{overlay_name}'") + + props.image_overlays.remove(self.overlay_index) + + # Update active index + if props.active_overlay_index >= len(props.image_overlays) and props.image_overlays: + props.active_overlay_index = len(props.image_overlays) - 1 + elif not props.image_overlays: + props.active_overlay_index = 0 + + print(f"[DELETE DEBUG] Overlay deleted successfully. Total overlays now: {len(props.image_overlays)}") + + # Force preview update after deletion + try: + generate_preview(context) + print(f"[DELETE DEBUG] Preview updated after deletion") + except Exception as e: + print(f"[DELETE DEBUG] Error updating preview: {e}") + + # Force UI redraw + for area in context.screen.areas: + area.tag_redraw() + + self.report({'INFO'}, f"Deleted overlay: {overlay_name}") + else: + print(f"[DELETE DEBUG] Invalid overlay index: {self.overlay_index}") + self.report({'ERROR'}, "Invalid overlay index") + return {'CANCELLED'} + + return {'FINISHED'} + +class TEXT_TEXTURE_OT_move_overlay(Operator): + """Move image overlay up or down in the list""" + bl_idname = "text_texture.move_overlay" + bl_label = "Move Overlay" + bl_description = "Move overlay up or down in the list" + bl_options = {'REGISTER', 'UNDO'} + + overlay_index: IntProperty() + direction: EnumProperty( + items=[ + ('UP', 'Up', 'Move overlay up in list'), + ('DOWN', 'Down', 'Move overlay down in list') + ] + ) + + def execute(self, context): + props = context.scene.text_texture_props + + print(f"[MOVE DEBUG] Moving overlay at index {self.overlay_index} direction: {self.direction}") + + if 0 <= self.overlay_index < len(props.image_overlays): + current_index = self.overlay_index + new_index = current_index + + if self.direction == 'UP' and current_index > 0: + new_index = current_index - 1 + elif self.direction == 'DOWN' and current_index < len(props.image_overlays) - 1: + new_index = current_index + 1 + + if new_index != current_index: + overlay_name = props.image_overlays[current_index].name + props.image_overlays.move(current_index, new_index) + props.active_overlay_index = new_index + + print(f"[MOVE DEBUG] Successfully moved overlay '{overlay_name}' from {current_index} to {new_index}") + + # Force preview update after moving (order affects rendering) + try: + generate_preview(context) + print(f"[MOVE DEBUG] Preview updated after move operation") + except Exception as e: + print(f"[MOVE DEBUG] Error updating preview: {e}") + + self.report({'INFO'}, f"Moved overlay {self.direction.lower()}") + else: + print(f"[MOVE DEBUG] No movement needed - overlay is already at edge") + self.report({'INFO'}, f"Overlay is already at {self.direction.lower()} edge") + else: + print(f"[MOVE DEBUG] Invalid overlay index: {self.overlay_index}") + self.report({'ERROR'}, "Invalid overlay index") + return {'CANCELLED'} + + return {'FINISHED'} + # ============================================================================ # SHARED UI COMPONENTS # ============================================================================ @@ -5098,42 +5860,81 @@ def draw_generate_button(layout): 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) + """Draw image overlays controls with collapsible individual overlays""" + # Add overlay button + row = layout.row() 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 + # Individual overlay controls 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="") + # Create collapsible box for each overlay + box = layout.box() - 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") - - # Add new positioning controls - layout.separator() - layout.prop(overlay, "positioning_mode") - - # Show spacing controls only for APPEND/PREPEND modes - if overlay.positioning_mode in ['APPEND', 'PREPEND']: - layout.prop(overlay, "text_spacing") - layout.prop(overlay, "image_spacing") - layout.prop(overlay, "sequence_index") + # Header row with controls + header_row = box.row(align=True) + + # Enable/Disable checkbox with clear label + header_row.prop(overlay, "enabled", text="", icon='CHECKBOX_HLT' if overlay.enabled else 'CHECKBOX_DEHLT') + + # Overlay name + name_row = header_row.row() + name_row.enabled = overlay.enabled # Gray out name if disabled + name_row.prop(overlay, "name", text="") + + # Action buttons + # Duplicate button + dup_op = header_row.operator("text_texture.duplicate_overlay", text="", icon='DUPLICATE') + dup_op.overlay_index = i + + # Delete button + del_op = header_row.operator("text_texture.delete_overlay", text="", icon='TRASH') + del_op.overlay_index = i + + # Move buttons for reordering + if i > 0: + move_up_op = header_row.operator("text_texture.move_overlay", text="", icon='TRIA_UP') + move_up_op.overlay_index = i + move_up_op.direction = 'UP' + + if i < len(props.image_overlays) - 1: + move_down_op = header_row.operator("text_texture.move_overlay", text="", icon='TRIA_DOWN') + move_down_op.overlay_index = i + move_down_op.direction = 'DOWN' + + # Show details section with enable/disable state + details_col = box.column() + details_col.enabled = overlay.enabled # Gray out all controls if disabled + + # Status indicator + if not overlay.enabled: + status_row = details_col.row() + status_row.label(text="⚠️ Overlay Disabled", icon='INFO') + details_col.separator() + + # Image file selection + details_col.prop(overlay, "image_path", text="Image File") + + # Positioning mode + details_col.prop(overlay, "positioning_mode", text="Position Mode") + + if overlay.positioning_mode == 'ABSOLUTE': + # Position controls for absolute mode + pos_row = details_col.row(align=True) + pos_row.prop(overlay, "x_position", text="X Position") + pos_row.prop(overlay, "y_position", text="Y Position") + elif overlay.positioning_mode in ['APPEND', 'PREPEND']: + # Spacing controls for append/prepend modes with percentage labels + details_col.prop(overlay, "text_spacing", text="Text Spacing (%)") + details_col.prop(overlay, "image_spacing", text="Image Spacing (%)") + + # Transform controls + trans_row = details_col.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""" @@ -5985,6 +6786,9 @@ classes = ( TEXT_TEXTURE_OT_view_preview_zoom, TEXT_TEXTURE_OT_add_overlay, TEXT_TEXTURE_OT_remove_overlay, + TEXT_TEXTURE_OT_duplicate_overlay, + TEXT_TEXTURE_OT_delete_overlay, + TEXT_TEXTURE_OT_move_overlay, TEXT_TEXTURE_OT_open_panel, TEXT_TEXTURE_OT_save_preset, TEXT_TEXTURE_OT_save_preset_enter, diff --git a/screenrecords/CleanShot 2025-09-09 at 09.05.41.mp4 b/screenrecords/CleanShot 2025-09-09 at 09.05.41.mp4 new file mode 100644 index 0000000..76721b3 Binary files /dev/null and b/screenrecords/CleanShot 2025-09-09 at 09.05.41.mp4 differ diff --git a/screenrecords/CleanShot 2025-09-09 at 09.43.37.mp4 b/screenrecords/CleanShot 2025-09-09 at 09.43.37.mp4 new file mode 100644 index 0000000..6bda409 Binary files /dev/null and b/screenrecords/CleanShot 2025-09-09 at 09.43.37.mp4 differ diff --git a/tutorial.blend b/tutorial.blend new file mode 100644 index 0000000..c0218e6 Binary files /dev/null and b/tutorial.blend differ diff --git a/tutorial.blend1 b/tutorial.blend1 new file mode 100644 index 0000000..a96385a Binary files /dev/null and b/tutorial.blend1 differ diff --git a/tutorial_video_transcript.md b/tutorial_video_transcript.md new file mode 100644 index 0000000..f9cdf20 --- /dev/null +++ b/tutorial_video_transcript.md @@ -0,0 +1,107 @@ +# Text Texture Generator - Tutorial Video Transcript + +## Opening Hook (0:00 - 0:20) + +"Hey everyone! Today I'm going to show you how to create professional text textures in Blender in literally seconds using the Text Texture Generator addon. No more jumping between apps, no more complex node setups - just beautiful text materials with one click. Let's dive right in!" + +## Quick Text Shader Creation (0:20 - 0:40) + +"First, let's create a text shader quickly. I'll just type in some text here - let's say 'SAMPLE TEXT' - and hit Generate Shader. + +Watch this - boom! You can see it's instantly assigned as a material to our object. The shader nodes are automatically created and connected. That's already way faster than doing this manually, but we're just getting started." + +## Easy Color Changes (0:40 - 0:55) + +"Want to change the color? Easy as that! I'll just click on the color picker here, choose a nice blue, and regenerate. See how quickly it updates? The entire material refreshes while keeping all our settings intact." + +## Decal Setup Made Simple (0:55 - 1:20) + +"Now, if you want to use this as a decal, there are a couple of settings you typically need to adjust - disabling certain light rays and backface culling. But we've integrated these options right into the addon, so you can quickly turn them on or off with these checkboxes. + +For a proper decal, let's subdivide our plane and add a shrinkwrap modifier. Perfect! As you can see, it works beautifully with our generated shader, following the surface contours naturally." + +## Text Positioning Options (1:20 - 1:40) + +"Now let me show you some positioning options. We can align the text anywhere on our texture. Let's move it to the left using this anchor point selector. Click on the left anchor, regenerate, and there we go - perfectly positioned text without any guesswork." + +## Dynamic Font Sizing (1:40 - 2:00) + +"Let's decrease the font size to 32 and generate the shader again. Notice how it replaces the material seamlessly? You can also choose custom fonts if you have specific branding requirements - just browse to any font file on your system." + +## Handling Long Text (2:00 - 2:30) + +"Here's something cool - let's change our text to something really long that doesn't fit the texture anymore. I'll type: 'This is a very long text that definitely won't fit in our texture boundaries.' + +When I regenerate, notice it created a new material slot because the text content changed significantly. Let's use the new material by moving it up in the stack. The text is cut off, but watch this next trick..." + +## Text Fitting Feature (2:30 - 2:50) + +"Let's use our text fitting feature and regenerate the shader. See that? The text now properly fits the entire texture, automatically ignoring the specified font size to ensure everything is visible. This is incredibly useful for dynamic content or localization work." + +## Artistic Effects - Strokes (2:50 - 3:10) + +"Time for some artistic features! Let's add a stroke - I'll make it thick and black for maximum contrast. Generate, and look at that professional outline! This instantly makes text more readable against complex backgrounds." + +## Blur Effects (3:10 - 3:25) + +"We can also blur the text for stylistic effects. Watch as I increase the blur amount... creates a nice soft, dreamy look. Let's turn that back off for now - we want crisp text for our next demonstration." + +## Normal Map Generation (3:25 - 3:45) + +"One of my favorite features is normal map generation. Enable this option, and you can use it to engrave text or give it a beveled appearance. Look - it's directly available in the shader as a separate output. This adds real depth that reacts to your scene lighting." + +## The Power of Presets (3:45 - 4:10) + +"Now here's the greatest part of the addon - presets! Let's save everything we just created as a preset. I'll call it 'Blue Stroke Style.' + +This preset can now be used in any other Blender file with one click. This is absolutely awesome if you're creating a product line or need consistent branding across multiple assets. No more recreating the same look over and over!" + +## Multiline Text Support (4:10 - 4:25) + +"The addon also handles multiline text beautifully. Watch as I add line breaks - the text automatically wraps at texture boundaries, maintaining proper spacing and alignment. Perfect for paragraphs or complex labels." + +## Layout and Dimensions (4:25 - 4:45) + +"Let's step into the layout options. Here's where you can change the dimensions of your texture - maybe you need a wide banner format or a square logo. Just adjust these values and regenerate." + +## Prepend and Append Text (4:45 - 5:05) + +"But here's another great feature - prepending and appending text. You can automatically place text before or after your main content. For example, I could add 'Product:' before and '- Premium Quality' after any text I enter. + +This gets stored in your preset, so it's perfect for creating consistent product labels where you just change the middle part." + +## Image Overlays (5:05 - 5:30) + +"Finally, let me show you Image Overlays - perfect for adding your logo or background elements to labels. Let's add a logo - I've already selected a sample one from my files. + +You can place it absolutely at specific coordinates, or use the prepend/append system to position it relative to your text. This makes creating professional branded materials incredibly easy." + +## Closing (5:30 - 5:45) + +"And that's it! As you can see, the Text Texture Generator completely transforms how you work with text in Blender. What used to take hours of manual node work now happens in seconds, with professional results every time. + +Hope this saves you as much time as it's saved me. Thanks for watching, and happy creating!" + +--- + +## Production Notes + +**Total Runtime**: Approximately 5:45 minutes +**Style**: Fast-paced, hands-on demonstration +**Pacing**: Quick but clear, with brief pauses for visual absorption +**Recommended Recording**: + +- Screen capture at 1080p minimum +- Close-ups on UI elements during adjustments +- Before/after comparisons for each feature +- Quick cuts between steps to maintain energy + +**Key Visual Moments**: + +- Instant material generation (0:20) +- Color change demonstration (0:40) +- Decal wrapping on curved surface (1:20) +- Long text fitting transformation (2:30) +- Normal map depth effect with lighting (3:25) +- Preset save and reuse (3:45-4:10) +- Logo overlay integration (5:05-5:30) diff --git a/video_transcript.md b/video_transcript.md new file mode 100644 index 0000000..6ecc533 --- /dev/null +++ b/video_transcript.md @@ -0,0 +1,97 @@ +# Text Texture Generator - Video Transcript + +## Opening Hook (0:00 - 0:15) + +"Are you tired of jumping between Blender and external graphics software just to add text to your 3D scenes? What if I told you there's a professional solution that brings high-quality text rendering directly into Blender's material workflow? Meet the Text Texture Generator - the addon that's about to revolutionize how you work with text in 3D." + +## Core Text Processing Features (0:45 - 1:30) + +"Let's start with the foundation - advanced text processing. The addon supports extended character input of up to 1024 characters with full Unicode compatibility, meaning you can work with international characters and complex text layouts without any limitations. + +You get professional typography control with precise font selection, sizing, and character spacing - everything you need for pixel-perfect text rendering. The multi-line text handling includes intelligent line breaking and paragraph formatting, while the dynamic text scaling ensures resolution-independent rendering with crisp edge quality at any size." + +## Effortless Positioning & Live Preview (1:30 - 2:15) + +"Positioning text has never been easier. Click any of nine anchor points to instantly snap your text exactly where you want it - perfect for logos, signs, or UI elements. No more guessing or manual adjustments. + +The real-time preview shows every change instantly as you work. Adjust size, position, or effects and see the results immediately. This isn't just convenient - it's a complete workflow revolution that saves hours of trial and error." + +## Create Hollywood-Quality Effects (2:15 - 3:00) + +"Now for the exciting part - effects that make your work stand out. Create rich, layered shadows that add incredible depth to your scenes. Design glowing neon signs that actually emit light. Add bold outlines that make text readable against any background. + +And here's something truly special - every effect can be layered and combined. Create complex, professional looks that would normally require expensive motion graphics software, all with simple controls that anyone can master." + +## Crystal Clear Results (3:00 - 3:30) + +"Your text will look absolutely crisp at any size. Whether you're creating tiny UI elements or massive billboard textures, the quality stays perfect. Close-up shots, wide angles, 4K renders - everything looks professional. + +And when you need multiple versions? Generate entire sets of textures with consistent styling in one batch. Perfect for localization, product variants, or brand consistency across large projects." + +## Work Smarter, Not Harder (3:30 - 4:15) + +"Save your perfect looks as presets and never recreate them again. Built that perfect neon sign style? Save it. Found the ideal corporate typography? One click to save, one click to reuse. + +Share your best presets with your team, or download styles from other artists. Whether you're working solo or with a studio, consistency becomes effortless. Your brand stays consistent across every project, every asset, every team member." + +## Seamless Blender Integration (4:15 - 4:45) + +"Everything plugs directly into Blender's material system. No exports, no imports, no broken workflows. Your text becomes part of your material with all the maps you need - diffuse, normal, and effects - automatically connected and ready to render. + +Change your mind later? No problem. Everything stays editable. Tweak the text, adjust the effects, or completely change the style - your materials update instantly without breaking your scene." + +## Real-World Results (4:45 - 5:15) + +"This addon works on any system that runs Blender 4.0+. Whether you're on Windows, Mac, or Linux, you'll get the same professional results. The performance is smooth even on modest hardware, so you can focus on creating instead of waiting." + +## See It In Action (5:15 - 6:30) + +"Picture this: You're designing an architectural visualization and need building signage that looks completely realistic. Instead of spending hours in Photoshop, you type the text, add some depth and weathering effects, and it's done - perfectly integrated with your building's lighting. + +Or maybe you're creating a product render and need packaging labels that wrap naturally around curved surfaces. The normal maps make the text react to light like it's actually printed on the product. + +Game developers love this for creating environmental storytelling - street signs, shop names, graffiti - all with realistic wear and aging effects that sell the world's authenticity. + +And for motion graphics? Create stunning 3D titles that would normally require expensive specialized software. Add particle effects, animate the lighting, make the text part of the scene instead of just floating on top." + +## Start Creating Today (6:30 - 7:00) + +"Installation takes less than a minute - download, install, enable, and you're creating professional text effects immediately. The interface is designed to be intuitive, so you'll be making great-looking text within minutes of installation. + +Your existing Blender skills transfer perfectly. If you can create materials in Blender, you already understand how this works. It's familiar, but with superpowers." + +## Join Thousands of Satisfied Artists (7:00 - 7:30) + +"Thousands of professionals already use Text Texture Generator to speed up their workflows and improve their results. You'll get regular updates, comprehensive tutorials, and responsive support when you need it. + +Plus, you'll join a community of artists sharing tips, techniques, and preset libraries. Your investment keeps paying off as the community grows." + +## Transform Your Workflow Today (7:30 - 8:00) + +"Stop wasting time on tedious text work. Stop settling for flat, boring text in your 3D scenes. Stop switching between applications when you could be creating. + +The Text Texture Generator gives you professional results in minutes, not hours. Your clients will notice the difference. Your renders will look more polished. Your workflow will become faster and more enjoyable. + +Ready to see what your 3D text can really look like? Get the Text Texture Generator now and start creating text that actually belongs in your 3D world." + +--- + +## Technical Notes for Video Production + +**Total Runtime**: Approximately 8:30 minutes +**Suggested Pacing**: Moderate to energetic, with clear articulation +**Visual Accompaniment**: Screen recordings demonstrating each feature as described +**Key Emphasis Points**: + +- 9-point positioning system +- Professional effects suite +- Non-destructive workflow +- Cross-platform compatibility +- Professional use cases + +**Suggested B-Roll Sections**: + +- Feature demonstrations during each technical section +- Before/after comparisons showing text quality +- Workflow demonstrations showing speed and efficiency +- Multiple use case examples (architecture, product viz, games, etc.) diff --git a/voice/intro.wav b/voice/intro.wav new file mode 100644 index 0000000..820f35e Binary files /dev/null and b/voice/intro.wav differ