From c1ac5297f22d6a1ca7bd57fb745605e616de5549 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 11 Sep 2025 13:13:21 +0300 Subject: [PATCH] 1.0.0 --- core/generation_engine.py | 225 ++++++++++++++++-- .../text-texture-generator_1.0.0.zip | Bin 114667 -> 117648 bytes operators/generation_ops.py | 115 +++++---- 3 files changed, 280 insertions(+), 60 deletions(-) rename Archive.zip => dist/text-texture-generator_1.0.0.zip (88%) diff --git a/core/generation_engine.py b/core/generation_engine.py index acab20d..7225f07 100644 --- a/core/generation_engine.py +++ b/core/generation_engine.py @@ -19,7 +19,7 @@ def generate_texture_image(props, width, height): Returns (None, None) on error """ try: - print(f"[TTG DEBUG] Generating texture image at {width}x{height}px") + print(f"[TTG DEBUG] Starting texture generation at {width}x{height}px") # TODO: Implement actual texture generation logic # This is a minimal stub to resolve the import error @@ -29,37 +29,228 @@ def generate_texture_image(props, width, height): # - Normal map generation if enabled # - Image composition and final output - # For now, create a basic placeholder image - import numpy as np - - # Create a basic colored image as placeholder - image_data = np.ones((height, width, 4), dtype=np.float32) - image_data[:, :, 0] = props.background_color[0] # R - image_data[:, :, 1] = props.background_color[1] # G - image_data[:, :, 2] = props.background_color[2] # B - image_data[:, :, 3] = 1.0 # A - - # Create Blender image + print(f"[TTG DEBUG] Step 1: Creating Blender image object") + # Create Blender image directly without large numpy arrays img_name = f"TextTexture_{width}x{height}" if img_name in bpy.data.images: + print(f"[TTG DEBUG] Step 1a: Removing existing image: {img_name}") bpy.data.images.remove(bpy.data.images[img_name]) + print(f"[TTG DEBUG] Step 1b: Creating new Blender image: {img_name}") blender_img = bpy.data.images.new(img_name, width, height, alpha=True) - blender_img.pixels[:] = image_data.flatten() + print(f"[TTG DEBUG] Step 1c: Blender image created successfully") + + print(f"[TTG DEBUG] Step 2: Creating text texture with PIL") + # Use PIL to create a proper text texture instead of solid color + try: + from PIL import Image, ImageDraw, ImageFont + print(f"[TTG DEBUG] Step 2a: PIL imported successfully") + + # Create PIL image with transparent background + if props.background_transparent: + pil_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) + print(f"[TTG DEBUG] Step 2b: Created transparent PIL image") + else: + bg_color = tuple(int(c * 255) for c in props.background_color[:3]) + (255,) + pil_img = Image.new('RGBA', (width, height), bg_color) + print(f"[TTG DEBUG] Step 2b: Created PIL image with background color: {bg_color}") + + # Draw text if provided (including prepend/append) + full_text_parts = [] + if props.prepend_text.strip(): + full_text_parts.append(props.prepend_text.strip()) + if props.text.strip(): + full_text_parts.append(props.text.strip()) + if props.append_text.strip(): + full_text_parts.append(props.append_text.strip()) + + if full_text_parts: + draw = ImageDraw.Draw(pil_img) + + # Combine text based on layout mode + if props.prepend_append_layout == 'HORIZONTAL': + # For horizontal layout, use simple space separation with margin control + if len(full_text_parts) == 1: + full_text = full_text_parts[0] + else: + # Calculate number of spaces based on margin (but reasonable amounts) + prepend_spaces = max(1, int(props.prepend_margin / 10)) if props.prepend_text.strip() else 0 + append_spaces = max(1, int(props.append_margin / 10)) if props.append_text.strip() else 0 + + # Build text with controlled spacing + text_parts_with_spacing = [] + for i, part in enumerate(full_text_parts): + if i == 0 and prepend_spaces > 0: + # Add spacing after prepend text + text_parts_with_spacing.append(part + " " * prepend_spaces) + elif i == len(full_text_parts) - 1 and append_spaces > 0: + # Add spacing before append text + text_parts_with_spacing.append(" " * append_spaces + part) + else: + text_parts_with_spacing.append(part) + + full_text = " ".join(text_parts_with_spacing) + layout_mode = "horizontal" + else: + # Vertical layout - keep parts separate for individual positioning + full_text = "\n".join(full_text_parts) + layout_mode = "vertical" + + print(f"[TTG DEBUG] Step 2c: Full text composed: '{full_text}' (layout: {layout_mode})") + + # Determine font size (with text fitting if enabled) + font_size = props.font_size + if props.enable_text_fitting: + print(f"[TTG DEBUG] Step 2d: Text fitting enabled, calculating optimal size") + + # Try to load font for sizing + try: + if props.use_custom_font and props.custom_font_path: + test_font_path = props.custom_font_path + elif props.font_path != "default": + test_font_path = props.font_path + else: + test_font_path = None + + # Binary search for optimal font size + min_size = props.min_font_size + max_size = props.max_font_size + target_width = width * (1.0 - props.text_fitting_margin / 100.0) + target_height = height * (1.0 - props.text_fitting_margin / 100.0) + + optimal_size = min_size + for test_size in range(min_size, max_size + 1, 2): # Step by 2 for performance + try: + if test_font_path: + test_font = ImageFont.truetype(test_font_path, test_size) + else: + test_font = ImageFont.load_default() + + bbox = draw.textbbox((0, 0), full_text, font=test_font) + test_width = bbox[2] - bbox[0] + test_height = bbox[3] - bbox[1] + + if test_width <= target_width and test_height <= target_height: + optimal_size = test_size + else: + break + except (OSError, IOError): + break + + font_size = optimal_size + print(f"[TTG DEBUG] Step 2d: Optimal font size calculated: {font_size}") + except Exception as e: + print(f"[TTG DEBUG] Step 2d: Font fitting failed, using default size: {e}") + font_size = props.font_size + + # Load final font + try: + if props.use_custom_font and props.custom_font_path: + font = ImageFont.truetype(props.custom_font_path, font_size) + print(f"[TTG DEBUG] Step 2e: Using custom font: {props.custom_font_path}, size: {font_size}") + elif props.font_path != "default": + font = ImageFont.truetype(props.font_path, font_size) + print(f"[TTG DEBUG] Step 2e: Using system font: {props.font_path}, size: {font_size}") + else: + font = ImageFont.load_default() + print(f"[TTG DEBUG] Step 2e: Using default font, size: {font_size}") + except (OSError, IOError) as e: + print(f"[TTG DEBUG] Step 2e: Font loading failed, using default: {e}") + font = ImageFont.load_default() + + # Calculate text position and draw + if layout_mode == "vertical" and props.prepend_append_layout == 'VERTICAL': + # Draw each part separately for vertical layout + y_offset = 0 + total_height = 0 + + # Calculate total height first + part_heights = [] + for part in full_text_parts: + bbox = draw.textbbox((0, 0), part, font=font) + part_height = bbox[3] - bbox[1] + part_heights.append(part_height) + total_height += part_height + + # Add spacing between parts + if len(full_text_parts) > 1: + spacing = font_size // 4 # 25% of font size as line spacing + total_height += spacing * (len(full_text_parts) - 1) + + # Center vertically + start_y = (height - total_height) // 2 + + # Draw each part + current_y = start_y + text_color = tuple(int(c * 255) for c in props.text_color[:4]) + + for i, part in enumerate(full_text_parts): + bbox = draw.textbbox((0, 0), part, font=font) + part_width = bbox[2] - bbox[0] + x = (width - part_width) // 2 # Center horizontally + + draw.text((x, current_y), part, font=font, fill=text_color) + print(f"[TTG DEBUG] Step 2f: Drew part '{part}' at ({x}, {current_y})") + + current_y += part_heights[i] + if i < len(full_text_parts) - 1: # Add spacing except after last part + current_y += font_size // 4 + + else: + # Horizontal layout or single text + text_bbox = draw.textbbox((0, 0), full_text, font=font) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + + # Center text + x = (width - text_width) // 2 + y = (height - text_height) // 2 + + # Draw text + text_color = tuple(int(c * 255) for c in props.text_color[:4]) + draw.text((x, y), full_text, font=font, fill=text_color) + print(f"[TTG DEBUG] Step 2f: Full text '{full_text}' drawn at ({x}, {y}) with size {font_size}") + else: + print(f"[TTG DEBUG] Step 2d: No text provided, using solid background") + + # Convert PIL image to Blender format (flip vertically to fix mirroring) + print(f"[TTG DEBUG] Step 2f: Converting PIL to Blender format with vertical flip") + pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM) # Fix mirroring issue + pil_pixels = list(pil_img.getdata()) + blender_pixels = [] + for r, g, b, a in pil_pixels: + blender_pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) + + blender_img.pixels[:] = blender_pixels + print(f"[TTG DEBUG] Step 2g: Pixel data conversion completed with flip correction") + + except ImportError: + print(f"[TTG DEBUG] Step 2: PIL not available, falling back to solid color") + # Fallback to solid color if PIL is not available + pixel_pattern = [props.background_color[0], props.background_color[1], props.background_color[2], 1.0] + total_pixels = width * height + blender_img.pixels[:] = pixel_pattern * total_pixels + + print(f"[TTG DEBUG] Step 3: Packing image...") blender_img.pack() + print(f"[TTG DEBUG] Step 3: Image packing completed") # Generate normal map if enabled normal_map_img = None - if props.enable_normal_map: + if props.generate_normal_map: + print(f"[TTG DEBUG] Step 4: Normal map generation enabled, starting...") from ..core.normal_maps import generate_normal_map_from_alpha normal_map_img = generate_normal_map_from_alpha( - blender_img, + blender_img, props.normal_map_strength, props.normal_map_blur_radius, - props.invert_normal_map + props.normal_map_invert ) + print(f"[TTG DEBUG] Step 4: Normal map generation completed") + else: + print(f"[TTG DEBUG] Step 4: Normal map generation disabled, skipping") - print(f"[TTG DEBUG] Texture generation completed successfully") + print(f"[TTG DEBUG] All steps completed successfully") return blender_img, normal_map_img except Exception as e: diff --git a/Archive.zip b/dist/text-texture-generator_1.0.0.zip similarity index 88% rename from Archive.zip rename to dist/text-texture-generator_1.0.0.zip index 212f963e808e4e5bbe10483cd6daba8cba73271a..74515392a0df51292c4370bb781ff0a2fa464059 100644 GIT binary patch delta 9832 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 delta 6803 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