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 212f963..7451539 100644 Binary files a/Archive.zip and b/dist/text-texture-generator_1.0.0.zip differ diff --git a/operators/generation_ops.py b/operators/generation_ops.py index e987f8e..1b34dec 100644 --- a/operators/generation_ops.py +++ b/operators/generation_ops.py @@ -47,8 +47,16 @@ class TEXT_TEXTURE_OT_generate(Operator): self.report({'ERROR'}, "Failed to generate diffuse image") return {'CANCELLED'} - # Create diffuse texture - img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}" + # Create diffuse texture with proper sanitization + import re + # Remove non-ASCII and non-alphanumeric characters, keep spaces temporarily + sanitized_text = re.sub(r'[^\w\s-]', '', props.text[:20]) + # Replace spaces and multiple underscores/dashes with single underscores + sanitized_text = re.sub(r'[\s_-]+', '_', sanitized_text).strip('_') + # Fallback if text becomes empty + if not sanitized_text: + sanitized_text = "Texture" + img_name = f"TextTexture_{sanitized_text}" # Remove existing diffuse texture if img_name in bpy.data.images: @@ -57,20 +65,15 @@ class TEXT_TEXTURE_OT_generate(Operator): # Create new diffuse image with USER SPECIFIED dimensions blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True) - # Convert PIL to Blender for diffuse texture - pixels = [] - for y in range(final_height): - for x in range(final_width): - r, g, b, a = img.getpixel((x, final_height - 1 - y)) - pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) - - blender_img.pixels = pixels + # Directly copy pixel data from generated image to Blender image + # The generated image is already in the correct format, just copy it + blender_img.pixels[:] = img.pixels[:] blender_img.pack() # PACK FINAL IMAGE # Create normal map texture if enabled and generated normal_map_blender_img = None if normal_map_img and props.generate_normal_map: - normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}" + normal_map_name = f"TextTexture_Normal_{sanitized_text}" # Remove existing normal map texture if normal_map_name in bpy.data.images: @@ -80,14 +83,12 @@ class TEXT_TEXTURE_OT_generate(Operator): normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False) normal_map_blender_img.colorspace_settings.name = 'Non-Color' # Important for normal maps - # Convert PIL to Blender for normal map - normal_pixels = [] - for y in range(final_height): - for x in range(final_width): - r, g, b = normal_map_img.getpixel((x, final_height - 1 - y)) - normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0]) # Alpha always 1.0 for normal maps - - normal_map_blender_img.pixels = normal_pixels + # Directly copy normal map pixel data + # Copy RGB channels and set alpha to 1.0 + temp_pixels = list(normal_map_img.pixels[:]) + for i in range(3, len(temp_pixels), 4): + temp_pixels[i] = 1.0 # Set alpha to 1.0 for normal maps + normal_map_blender_img.pixels[:] = temp_pixels normal_map_blender_img.pack() # PACK NORMAL MAP print(f"[Normal Map] Created and packed normal map texture: {normal_map_name}") @@ -134,82 +135,110 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): def execute(self, context): try: + print(f"[TTG SHADER DEBUG] === SHADER GENERATION STARTED ===") props = context.scene.text_texture_props # Generate texture first final_width = props.texture_width final_height = props.texture_height - print(f"Generating shader with texture at {final_width}x{final_height}px") + print(f"[TTG SHADER DEBUG] Step 1: Starting texture generation at {final_width}x{final_height}px") result = generate_texture_image(props, final_width, final_height) + print(f"[TTG SHADER DEBUG] Step 2: Texture generation returned: {type(result)}") + if not result: self.report({'ERROR'}, "Failed to generate texture image") return {'CANCELLED'} + print(f"[TTG SHADER DEBUG] Step 3: Processing result...") # Extract diffuse and normal map images if isinstance(result, tuple): img, normal_map_img = result + print(f"[TTG SHADER DEBUG] Step 3a: Got tuple result - img: {type(img)}, normal: {type(normal_map_img)}") else: # Backward compatibility img = result normal_map_img = None + print(f"[TTG SHADER DEBUG] Step 3b: Got single result - img: {type(img)}") if not img: self.report({'ERROR'}, "Failed to generate diffuse image") return {'CANCELLED'} - # Create diffuse texture name - img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}" + print(f"[TTG SHADER DEBUG] Step 4: Image validation passed, proceeding with shader creation...") + + print(f"[TTG SHADER DEBUG] Step 5: Creating diffuse texture...") + # Create diffuse texture name with proper sanitization + import re + # Remove non-ASCII and non-alphanumeric characters, keep spaces temporarily + sanitized_text = re.sub(r'[^\w\s-]', '', props.text[:20]) + # Replace spaces and multiple underscores/dashes with single underscores + sanitized_text = re.sub(r'[\s_-]+', '_', sanitized_text).strip('_') + # Fallback if text becomes empty + if not sanitized_text: + sanitized_text = "Texture" + img_name = f"TextTexture_{sanitized_text}" + print(f"[TTG SHADER DEBUG] Step 5a: Sanitized texture name: {img_name}") # Remove existing diffuse texture if it exists if img_name in bpy.data.images: + print(f"[TTG SHADER DEBUG] Step 5b: Removing existing texture") bpy.data.images.remove(bpy.data.images[img_name]) + print(f"[TTG SHADER DEBUG] Step 5c: Creating new Blender image...") # Create new diffuse texture blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True) + print(f"[TTG SHADER DEBUG] Step 5d: New image created, copying pixel data...") - # Convert PIL to Blender for diffuse texture - pixels = [] - for y in range(final_height): - for x in range(final_width): - r, g, b, a = img.getpixel((x, final_height - 1 - y)) - pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) - - blender_img.pixels = pixels + # Directly copy pixel data from generated image to Blender image + # The generated image is already in the correct format, just copy it + blender_img.pixels[:] = img.pixels[:] + print(f"[TTG SHADER DEBUG] Step 5e: Pixel data copied, packing image...") blender_img.pack() + print(f"[TTG SHADER DEBUG] Step 5f: Image packed successfully") + print(f"[TTG SHADER DEBUG] Step 6: Processing normal maps...") # Create normal map texture if enabled and generated normal_map_blender_img = None if normal_map_img and props.generate_normal_map: - normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}" + print(f"[TTG SHADER DEBUG] Step 6a: Normal map enabled and generated, processing...") + # Use the same sanitized text for consistency + normal_map_name = f"TextTexture_Normal_{sanitized_text}" + print(f"[TTG SHADER DEBUG] Step 6b: Sanitized normal map name: {normal_map_name}") # Remove existing normal map texture if normal_map_name in bpy.data.images: + print(f"[TTG SHADER DEBUG] Step 6c: Removing existing normal map") bpy.data.images.remove(bpy.data.images[normal_map_name]) + print(f"[TTG SHADER DEBUG] Step 6d: Creating normal map image...") # Create new normal map image normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False) normal_map_blender_img.colorspace_settings.name = 'Non-Color' # Important for normal maps + print(f"[TTG SHADER DEBUG] Step 6e: Normal map image created, copying pixels...") - # Convert PIL to Blender for normal map - normal_pixels = [] - for y in range(final_height): - for x in range(final_width): - r, g, b = normal_map_img.getpixel((x, final_height - 1 - y)) - normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0]) # Alpha always 1.0 for normal maps - - normal_map_blender_img.pixels = normal_pixels + # Directly copy normal map pixel data + # Copy RGB channels and set alpha to 1.0 + temp_pixels = list(normal_map_img.pixels[:]) + for i in range(3, len(temp_pixels), 4): + temp_pixels[i] = 1.0 # Set alpha to 1.0 for normal maps + normal_map_blender_img.pixels[:] = temp_pixels + print(f"[TTG SHADER DEBUG] Step 6f: Normal map pixels copied, packing...") normal_map_blender_img.pack() - print(f"[Normal Map] Created normal map texture for shader: {normal_map_name}") + print(f"[TTG SHADER DEBUG] Step 6g: Normal map packed successfully") + else: + print(f"[TTG SHADER DEBUG] Step 6: Normal map generation skipped") + print(f"[TTG SHADER DEBUG] Step 7: Creating material...") # Create or get material using custom name or fallback if props.custom_material_name.strip(): material_name = props.custom_material_name.strip() + print(f"[TTG SHADER DEBUG] Step 7a: Using custom material name: {material_name}") else: - # Fallback to text-based naming - clean_text = props.text[:15].replace(' ', '_').replace('\n', '_') - material_name = f"TextTexture_Mat_{clean_text}" + # Use the same sanitized text for consistency + material_name = f"TextTexture_Mat_{sanitized_text}" + print(f"[TTG SHADER DEBUG] Step 7b: Using sanitized material name: {material_name}") if material_name in bpy.data.materials: mat = bpy.data.materials[material_name]