commit 80042751d83f19d7a529a38515ec56c195d80126 Author: Marc Mintel Date: Fri Aug 8 13:31:51 2025 +0700 init diff --git a/Archive.zip b/Archive.zip new file mode 100644 index 0000000..9a05eb3 Binary files /dev/null and b/Archive.zip differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..b701295 --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# Text Texture Generator - Blender Addon + +A modern Blender addon that generates image textures from text with extensive customization options. + +## Features + +- **Text Input**: Enter any text to convert into a texture +- **Customizable Size**: Adjust texture dimensions (64x64 to 4096x4096 pixels) +- **Font Controls**: + - Multiple font options (system-dependent) + - Adjustable font size (8-200px) + - Text alignment (left, center, right) +- **Color Controls**: + - Text color picker with alpha + - Background color picker with alpha + - Transparent background option +- **Layout Options**: + - Adjustable padding around text + - Professional text positioning +- **Blender Integration**: + - Automatic image texture node creation in Shader Editor + - Works in both Shader Editor and 3D Viewport + - Generated textures are packed into the .blend file + +## Installation + +### Method 1: Modern Blender 4.0+ (Recommended) + +1. Download the addon files (`blender_manifest.toml` and `__init__.py`) +2. Create a folder named `text_texture_generator` +3. Place both files in this folder +4. In Blender, go to `Edit > Preferences > Add-ons` +5. Click `Install from Disk` and select the folder +6. Enable the "Text Texture Generator" addon + +### Method 2: Traditional Installation + +1. Download the `__init__.py` file +2. In Blender, go to `Edit > Preferences > Add-ons` +3. Click `Install from Disk` and select the `__init__.py` file +4. Enable the "Text Texture Generator" addon + +## Usage + +### In Shader Editor: + +1. Select a material with nodes enabled +2. Open the Shader Editor +3. Press `N` to open the side panel +4. Navigate to the "Text Texture" tab +5. Configure your text and settings +6. Click "Generate Texture" +7. The texture node will be automatically added to your material + +### In 3D Viewport: + +1. Press `N` to open the side panel +2. Navigate to the "Text Texture" tab +3. Configure your text and settings +4. Click "Generate Texture" +5. Find your texture in the Image Editor or use it in materials + +## Settings Reference + +### Text Settings + +- **Text**: The text to render (up to 1024 characters) +- **Font**: System font selection (varies by OS) +- **Font Size**: Size of the text in pixels (8-200) +- **Alignment**: Text alignment (left, center, right) +- **Padding**: Space around text in pixels (0-100) + +### Texture Settings + +- **Width/Height**: Texture dimensions in pixels (64-4096) +- **Text Color**: RGBA color picker for text +- **Transparent Background**: Enable/disable background transparency +- **Background Color**: RGBA color picker for background (when not transparent) + +## System Requirements + +- Blender 4.0.0 or newer +- Python 3.10+ (included with Blender) +- Pillow library (automatically installed via pip) + +## Supported Fonts + +The addon includes several built-in font options that vary by operating system: + +### Windows + +- Arial, Times New Roman, Courier New, Calibri, Segoe UI + +### macOS + +- Arial, Times New Roman, Courier New, Helvetica, SF Pro Display, Menlo + +### Linux + +- DejaVu Sans, DejaVu Serif, DejaVu Sans Mono, Ubuntu + +## Dependencies + +The addon automatically installs required dependencies: + +- **Pillow** (>=10.0.0): For image generation and text rendering + +## Troubleshooting + +### Common Issues + +1. **"Failed to generate texture" error**: + + - Ensure Pillow is installed (addon does this automatically) + - Check that your text isn't empty + - Verify texture dimensions are within limits + +2. **Font not displaying correctly**: + + - Try using "Default" font option + - Some system fonts may not be available + - Custom font paths vary by system + +3. **Texture appears in wrong location**: + - In Shader Editor: Ensure you have a material selected + - Check the generated image in the Image Editor + - Look for the texture in `bpy.data.images` + +### Performance Notes + +- Large textures (2048x2048+) may take longer to generate +- Very long text strings may affect performance +- Generated textures are stored in memory and packed with the .blend file + +## Development + +### File Structure + +``` +text_texture_generator/ +├── blender_manifest.toml # Modern addon manifest +├── __init__.py # Main addon code +└── README.md # This file +``` + +### Contributing + +Feel free to submit issues and enhancement requests. The addon is designed to be simple and focused on core text texture generation functionality. + +## License + +GPL-3.0-or-later + +## Author + +Marc Mintel + +## Changelog + +### Version 1.0.0 + +- Initial release +- Text to texture conversion +- Customizable fonts, colors, and sizing +- Transparent background support +- Automatic Blender integration +- Cross-platform font support diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b8bf351 --- /dev/null +++ b/__init__.py @@ -0,0 +1,1184 @@ +import bpy +import bmesh +from bpy.types import Operator, Panel, PropertyGroup, UIList +from bpy.props import StringProperty, IntProperty, FloatVectorProperty, FloatProperty, BoolProperty, EnumProperty, CollectionProperty, PointerProperty +import os +import tempfile +import subprocess +import sys +import json +import platform +import time + +# Addon info +bl_info = { + "name": "Text Texture Generator", + "author": "Marc Mintel ", + "version": (2, 3, 2), + "blender": (4, 0, 0), + "location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab | Properties > Material > Tool", + "description": "Generate image textures from text with accurate dimensions and instant live preview", + "category": "Material", +} + +# ============================================================================ +# UTILITY FUNCTIONS +# ============================================================================ + +def install_pillow(): + """Install Pillow if not available""" + try: + import PIL + except ImportError: + print("Installing Pillow...") + subprocess.check_call([sys.executable, "-m", "pip", "install", "pillow>=10.0.0"]) + +def get_system_fonts(): + """Get detailed list of available system fonts with paths""" + try: + from PIL import ImageFont + import glob + + fonts = {} + system = platform.system() + + if system == "Windows": + font_dirs = [ + "C:/Windows/Fonts/", + os.path.expanduser("~/AppData/Local/Microsoft/Windows/Fonts/") + ] + extensions = ["*.ttf", "*.ttc", "*.otf"] + elif system == "Darwin": # macOS + font_dirs = [ + "/System/Library/Fonts/", + "/Library/Fonts/", + os.path.expanduser("~/Library/Fonts/") + ] + extensions = ["*.ttf", "*.ttc", "*.otf", "*.dfont"] + else: # Linux + font_dirs = [ + "/usr/share/fonts/", + "/usr/local/share/fonts/", + os.path.expanduser("~/.local/share/fonts/"), + os.path.expanduser("~/.fonts/") + ] + extensions = ["*.ttf", "*.ttc", "*.otf"] + + for font_dir in font_dirs: + if os.path.exists(font_dir): + for ext in extensions: + for font_path in glob.glob(os.path.join(font_dir, "**", ext), recursive=True): + try: + font_name = os.path.splitext(os.path.basename(font_path))[0] + font_name = font_name.replace("-", " ").replace("_", " ") + fonts[font_name] = font_path + except Exception: + continue + + return fonts + except ImportError: + return {} + +def get_font_enum_items(self, context): + """Dynamic enum items for font selection""" + items = [("default", "Default Font", "Use system default font", 0)] + + if not hasattr(get_font_enum_items, 'cached_fonts'): + get_font_enum_items.cached_fonts = get_system_fonts() + + fonts = get_font_enum_items.cached_fonts + + for i, (font_name, font_path) in enumerate(sorted(fonts.items())[:50]): + items.append((font_path, font_name, f"Font: {font_name}", i + 1)) + + return items + +def get_preset_path(): + """Get path for storing presets""" + addon_dir = os.path.dirname(os.path.realpath(__file__)) + presets_dir = os.path.join(addon_dir, "presets") + if not os.path.exists(presets_dir): + os.makedirs(presets_dir) + return presets_dir + +# ============================================================================ +# PREVIEW GENERATION FUNCTIONS +# ============================================================================ + +def generate_texture_image(props, width, height): + """Generate the actual texture image - width and height are REQUIRED""" + try: + install_pillow() + from PIL import Image, ImageDraw, ImageFont + + # Use the exact dimensions provided + print(f"Generating PIL image at {width}x{height}") + + # Ensure minimum dimensions + width = max(16, width) + height = max(16, height) + + print(f"DEBUG: Generating texture - requested: {width}x{height}, texture settings: {props.texture_width}x{props.texture_height}") + + # Scale font size proportionally for preview + # If this is a preview (smaller than texture size), scale the font accordingly + scale_factor = width / props.texture_width + font_size_pixels = max(6, int(props.font_size * scale_factor)) + padding = max(0, int(props.padding * scale_factor)) + + print(f"DEBUG: Scale factor: {scale_factor}, scaled font size: {font_size_pixels}, scaled padding: {padding}") + + print(f"Font size: {font_size_pixels}, Padding: {padding}") + + # Create image with background + if props.background_transparent: + img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) + else: + bg_color = tuple(int(c * 255) for c in props.background_color[:3]) + if len(props.background_color) > 3: + bg_color += (int(props.background_color[3] * 255),) + img = Image.new('RGBA', (width, height), bg_color) + + print(f"Image created: {img.size}") + + draw = ImageDraw.Draw(img) + + # Load font with proper size handling + font = None + font_loaded = False + + try: + if props.use_custom_font and props.custom_font_path and os.path.exists(props.custom_font_path): + test_font = ImageFont.truetype(props.custom_font_path, font_size_pixels) + # Test the font before accepting it + test_draw = ImageDraw.Draw(Image.new('RGB', (10, 10))) + test_draw.textbbox((0, 0), "A", font=test_font) + font = test_font + font_loaded = True + print(f"Loaded custom font: {props.custom_font_path}") + elif props.font_path != "default" and os.path.exists(props.font_path): + test_font = ImageFont.truetype(props.font_path, font_size_pixels) + # Test the font before accepting it + test_draw = ImageDraw.Draw(Image.new('RGB', (10, 10))) + test_draw.textbbox((0, 0), "A", font=test_font) + font = test_font + font_loaded = True + print(f"Loaded font: {props.font_path}") + except Exception as e: + print(f"Font loading error: {e}") + font_loaded = False + + # Better fallback - try to find a system TrueType font + if not font_loaded: + try: + # Try to find a basic system font + system = platform.system() + fallback_fonts = [] + + if system == "Windows": + fallback_fonts = [ + "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/Arial.ttf", + "C:/Windows/Fonts/calibri.ttf", + "C:/Windows/Fonts/tahoma.ttf" + ] + elif system == "Darwin": # macOS + fallback_fonts = [ + "/System/Library/Fonts/Helvetica.ttc", + "/Library/Fonts/Arial.ttf", + "/System/Library/Fonts/Avenir.ttc" + ] + else: # Linux + fallback_fonts = [ + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf" + ] + + for fallback_path in fallback_fonts: + if os.path.exists(fallback_path): + try: + font = ImageFont.truetype(fallback_path, font_size_pixels) + # Test font by trying to get bbox of a simple character + test_draw = ImageDraw.Draw(Image.new('RGB', (10, 10))) + test_draw.textbbox((0, 0), "A", font=font) + font_loaded = True + print(f"Using fallback font: {fallback_path}") + break + except Exception as font_error: + print(f"Failed to load fallback font {fallback_path}: {font_error}") + continue + except Exception: + pass + + # Last resort - use default (but it won't respect size) + if not font_loaded: + font = ImageFont.load_default() + print("Warning: Using default font - size will not be accurate!") + + # Calculate text position with error handling + text = props.text if props.text and props.text.strip() else "Hello World" + + try: + bbox = draw.textbbox((0, 0), text, font=font) + text_width = max(1, bbox[2] - bbox[0]) + text_height = max(1, bbox[3] - bbox[1]) + except (OSError, ZeroDivisionError, ValueError) as e: + print(f"Error getting text bbox: {e}") + # Fallback: estimate text size + text_width = int(len(text) * font_size_pixels * 0.6) + text_height = font_size_pixels + + # Calculate X position based on alignment + if props.text_align == 'center': + x = (width - text_width) // 2 + elif props.text_align == 'right': + x = width - text_width - padding + else: + x = padding + + # Center vertically + y = (height - text_height) // 2 + + # Draw text + text_color = tuple(int(c * 255) for c in props.text_color[:3]) + if len(props.text_color) > 3: + text_color += (int(props.text_color[3] * 255),) + + draw.text((x, y), text, fill=text_color, font=font) + print(f"Text drawn at ({x}, {y})") + + return img + + except Exception as e: + print(f"Error in generate_texture_image: {e}") + import traceback + traceback.print_exc() + return None + +def generate_preview(context): + """Generate preview texture""" + if not context or not hasattr(context, 'scene'): + print("No context or scene") + return None + + props = context.scene.text_texture_props + + # Always generate preview - removed live_preview_enabled check + + try: + # Generate at preview size for performance, not full texture size + preview_size = props.preview_size # Default is 256 + # Calculate aspect ratio preserving dimensions + aspect_ratio = props.texture_width / props.texture_height + if aspect_ratio > 1: # Wider than tall + preview_w = preview_size + preview_h = int(preview_size / aspect_ratio) + else: # Taller than wide or square + preview_w = int(preview_size * aspect_ratio) + preview_h = preview_size + + # Ensure minimum dimensions + preview_w = max(16, preview_w) + preview_h = max(16, preview_h) + + print(f"Starting preview generation at {preview_w}x{preview_h} (preview size: {preview_size})") + print(f"DEBUG: Texture size is {props.texture_width}x{props.texture_height}, preview at {preview_w}x{preview_h}") + + # Generate the image + img = generate_texture_image(props, preview_w, preview_h) + if not img: + print("Failed to generate PIL image") + return None + + # Apply preview background if needed + if props.preview_bg_type != 'transparent' or not props.background_transparent: + from PIL import Image, ImageDraw + + # Create background image + bg_img = Image.new('RGBA', (preview_w, preview_h)) + draw = ImageDraw.Draw(bg_img) + + if props.preview_bg_type == 'color': + # Solid color background + bg_color = tuple(int(c * 255) for c in props.preview_bg_color[:3]) + bg_color += (int(props.preview_bg_color[3] * 255),) + bg_img.paste(bg_color, (0, 0, preview_w, preview_h)) + elif props.preview_bg_type == 'gradient': + # Gradient background + color1 = tuple(int(c * 255) for c in props.preview_bg_color[:3]) + color2 = tuple(int(c * 255) for c in props.preview_bg_color2[:3]) + + for y in range(preview_h): + ratio = y / preview_h + r = int(color1[0] * (1 - ratio) + color2[0] * ratio) + g = int(color1[1] * (1 - ratio) + color2[1] * ratio) + b = int(color1[2] * (1 - ratio) + color2[2] * ratio) + draw.rectangle([(0, y), (preview_w, y+1)], fill=(r, g, b, 255)) + else: # transparent - create checkerboard + # Checkerboard pattern for transparent + checker_size = 16 + color1 = (128, 128, 128, 255) + color2 = (192, 192, 192, 255) + + for y in range(0, preview_h, checker_size): + for x in range(0, preview_w, checker_size): + color = color1 if ((x // checker_size) + (y // checker_size)) % 2 == 0 else color2 + draw.rectangle([(x, y), (min(x + checker_size, preview_w), min(y + checker_size, preview_h))], + fill=color) + + # Composite the text image over the background + bg_img.alpha_composite(img) + img = bg_img + + # Name for preview image + preview_img_name = "TextTexturePreview" + + # Remove old preview if it exists + if preview_img_name in bpy.data.images: + print("Removing old preview") + old_img = bpy.data.images[preview_img_name] + bpy.data.images.remove(old_img) + + # Create new Blender image for preview + print("Creating new Blender image") + blender_img = bpy.data.images.new(preview_img_name, preview_w, preview_h, alpha=True) + + # Convert PIL to Blender + print("Converting pixels") + pixels = [] + for y in range(preview_h): + for x in range(preview_w): + pixel = img.getpixel((x, preview_h - 1 - y)) + if len(pixel) == 4: + r, g, b, a = pixel + else: + r, g, b = pixel + a = 255 + pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) + + print("Setting pixels") + blender_img.pixels[:] = pixels + + # Store reference to the preview image only if not in draw context + try: + props.preview_image = blender_img + print(f"Preview stored: {props.preview_image}") + except AttributeError as attr_err: + if "Writing to ID classes in this context is not allowed" in str(attr_err): + print("Cannot set preview_image during draw - will be set later") + else: + raise + + # Force UI redraw + for area in context.screen.areas: + area.tag_redraw() + + print(f"Preview generated successfully: {blender_img.name}") + return blender_img + + except Exception as e: + print(f"Preview generation error: {e}") + import traceback + traceback.print_exc() + try: + props.preview_image = None + except AttributeError as attr_err: + if "Writing to ID classes in this context is not allowed" not in str(attr_err): + raise + return None + +# Removed update_preview_enabled function - preview is always enabled + +def update_live_preview(self, context): + """Update preview when properties change""" + if not hasattr(context, 'scene'): + return + + props = context.scene.text_texture_props + + print(f"DEBUG: update_live_preview called, is_updating: {props.is_updating}") + + # Always update preview unless already updating + if not props.is_updating: + print("DEBUG: Triggering preview generation from property update") + # Use a timer to debounce rapid property changes for better performance + if hasattr(update_live_preview, '_timer'): + bpy.app.timers.unregister(update_live_preview._timer) + + def delayed_preview(): + generate_preview(context) + return None # Stop timer + + update_live_preview._timer = delayed_preview + bpy.app.timers.register(delayed_preview, first_interval=0.1) + +# ============================================================================ +# PROPERTY GROUPS +# ============================================================================ + +class TEXT_TEXTURE_Preset(PropertyGroup): + """Single preset item""" + name: StringProperty( + name="Preset Name", + description="Name of the preset", + default="New Preset" + ) + +class TEXT_TEXTURE_Properties(PropertyGroup): + """Properties for text texture generation""" + + is_updating: BoolProperty(default=False) + + preview_image: PointerProperty( + type=bpy.types.Image, + name="Preview Image" + ) + + text: StringProperty( + name="Text", + description="Text to render as texture", + default="Hello World", + maxlen=1024, + update=update_live_preview + ) + + texture_width: IntProperty( + name="Width", + description="Texture width in pixels", + default=1024, + min=64, + max=4096, + update=update_live_preview + ) + + texture_height: IntProperty( + name="Height", + description="Texture height in pixels", + default=1024, + min=64, + max=4096, + update=update_live_preview + ) + + font_size: IntProperty( + name="Font Size", + description="Font size in pixels", + default=96, + min=8, + max=500, + update=update_live_preview + ) + + padding: IntProperty( + name="Padding", + description="Padding around text in pixels", + default=20, + min=0, + max=200, + update=update_live_preview + ) + + text_color: FloatVectorProperty( + name="Text Color", + description="Color of the text", + subtype='COLOR', + default=(1.0, 1.0, 1.0, 1.0), + size=4, + min=0.0, + max=1.0, + update=update_live_preview + ) + + background_transparent: BoolProperty( + name="Transparent Background", + description="Use transparent background", + default=True, + update=update_live_preview + ) + + background_color: FloatVectorProperty( + name="Background Color", + description="Background color (when not transparent)", + subtype='COLOR', + default=(0.0, 0.0, 0.0, 1.0), + size=4, + min=0.0, + max=1.0, + update=update_live_preview + ) + + font_path: EnumProperty( + name="Font", + description="Font to use for text rendering", + items=get_font_enum_items, + update=update_live_preview + ) + + custom_font_path: StringProperty( + name="Custom Font File", + description="Path to custom font file (.ttf, .otf)", + subtype='FILE_PATH', + default="", + update=update_live_preview + ) + + use_custom_font: BoolProperty( + name="Use Custom Font", + description="Use custom font file instead of system fonts", + default=False, + update=update_live_preview + ) + + text_align: EnumProperty( + name="Alignment", + description="Text alignment", + items=[ + ('left', 'Left', 'Align text to the left'), + ('center', 'Center', 'Center text'), + ('right', 'Right', 'Align text to the right'), + ], + default='center', + update=update_live_preview + ) + + # Removed live_preview_enabled - preview is always on + + preview_size: IntProperty( + name="Preview Size", + description="Size of the preview image (does not affect final output)", + default=256, + min=128, + max=512, + update=update_live_preview + ) + + # Preview background options + preview_bg_type: EnumProperty( + name="Preview Background", + description="Background type for preview display", + items=[ + ('transparent', 'Transparent', 'Transparent checkerboard pattern'), + ('color', 'Solid Color', 'Use a solid color background'), + ('gradient', 'Gradient', 'Use a gradient background'), + ], + default='transparent', + update=update_live_preview + ) + + preview_bg_color: FloatVectorProperty( + name="Preview BG Color", + description="Background color for preview", + subtype='COLOR', + default=(0.2, 0.2, 0.2, 1.0), + size=4, + min=0.0, + max=1.0, + update=update_live_preview + ) + + preview_bg_color2: FloatVectorProperty( + name="Preview BG Color 2", + description="Second color for gradient background", + subtype='COLOR', + default=(0.4, 0.4, 0.4, 1.0), + size=4, + min=0.0, + max=1.0, + update=update_live_preview + ) + + presets: CollectionProperty(type=TEXT_TEXTURE_Preset) + active_preset_index: IntProperty(default=0) + + new_preset_name: StringProperty( + name="Preset Name", + description="Name for new preset", + default="New Preset" + ) + +# ============================================================================ +# OPERATORS +# ============================================================================ + +class TEXT_TEXTURE_OT_generate(Operator): + """Generate final texture and pack it""" + bl_idname = "text_texture.generate" + bl_label = "Generate Text Texture" + bl_description = "Generate final high-quality texture and pack it into the blend file" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + try: + props = context.scene.text_texture_props + + # IMPORTANT: Generate at EXACT user-specified dimensions + final_width = props.texture_width + final_height = props.texture_height + + print(f"Generating texture at {final_width}x{final_height}px") + + # Generate at FULL resolution specified by user + img = generate_texture_image(props, final_width, final_height) + + # Create final name + img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}" + + # Remove existing + if img_name in bpy.data.images: + bpy.data.images.remove(bpy.data.images[img_name]) + + # Create new image with USER SPECIFIED dimensions + blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True) + + # Convert PIL to Blender + pixels = [] + for y in range(final_height): + for x in range(final_width): + r, g, b, a = img.getpixel((x, final_height - 1 - y)) + pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) + + blender_img.pixels = pixels + blender_img.pack() # PACK FINAL IMAGE + + # Add to shader + if context.space_data and context.space_data.type == 'NODE_EDITOR': + if context.space_data.tree_type == 'ShaderNodeTree': + material = context.space_data.id + if material and material.use_nodes: + nodes = material.node_tree.nodes + img_node = nodes.new(type='ShaderNodeTexImage') + img_node.image = blender_img + img_node.location = (0, 0) + img_node.select = True + nodes.active = img_node + + self.report({'INFO'}, f"Generated and packed: {img_name} ({final_width}x{final_height}px)") + return {'FINISHED'} + + except Exception as e: + self.report({'ERROR'}, f"Failed: {str(e)}") + return {'CANCELLED'} + +class TEXT_TEXTURE_OT_refresh_preview(Operator): + """Force refresh the preview""" + bl_idname = "text_texture.refresh_preview" + bl_label = "Refresh Preview" + bl_description = "Force refresh the preview image" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + props = context.scene.text_texture_props + + # Clear existing preview first + props.preview_image = None + + # Generate new preview + result = generate_preview(context) + + if result: + self.report({'INFO'}, f"Preview refreshed at {props.preview_size}px") + else: + self.report({'ERROR'}, "Failed to generate preview - check console for details") + + # Force UI update + for area in context.screen.areas: + area.tag_redraw() + + return {'FINISHED'} + +# Removed view_preview operator - we want inline display only + +class TEXT_TEXTURE_OT_save_preset(Operator): + """Save current settings as preset""" + bl_idname = "text_texture.save_preset" + bl_label = "Save Preset" + bl_description = "Save current settings as preset" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + props = context.scene.text_texture_props + + if not props.new_preset_name.strip(): + self.report({'ERROR'}, "Enter a preset name") + return {'CANCELLED'} + + preset_data = { + 'text': props.text, + 'texture_width': props.texture_width, + 'texture_height': props.texture_height, + 'font_size': props.font_size, + 'padding': props.padding, + 'text_color': list(props.text_color), + 'background_transparent': props.background_transparent, + 'background_color': list(props.background_color), + 'font_path': props.font_path, + 'custom_font_path': props.custom_font_path, + 'use_custom_font': props.use_custom_font, + 'text_align': props.text_align, + } + + try: + preset_file = os.path.join(get_preset_path(), f"{props.new_preset_name}.json") + with open(preset_file, 'w') as f: + json.dump(preset_data, f, indent=2) + + preset = props.presets.add() + preset.name = props.new_preset_name + + self.report({'INFO'}, f"Saved: {props.new_preset_name}") + props.new_preset_name = "New Preset" + + except Exception as e: + self.report({'ERROR'}, f"Failed: {str(e)}") + return {'CANCELLED'} + + return {'FINISHED'} + +class TEXT_TEXTURE_OT_load_preset(Operator): + """Load preset""" + bl_idname = "text_texture.load_preset" + bl_label = "Load Preset" + bl_description = "Load selected preset" + bl_options = {'REGISTER', 'UNDO'} + + preset_name: StringProperty() + + def execute(self, context): + props = context.scene.text_texture_props + + try: + preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") + with open(preset_file, 'r') as f: + preset_data = json.load(f) + + props.is_updating = True + + props.text = preset_data.get('text', props.text) + props.texture_width = preset_data.get('texture_width', props.texture_width) + props.texture_height = preset_data.get('texture_height', props.texture_height) + props.font_size = preset_data.get('font_size', props.font_size) + props.padding = preset_data.get('padding', props.padding) + props.text_color = preset_data.get('text_color', props.text_color) + props.background_transparent = preset_data.get('background_transparent', props.background_transparent) + props.background_color = preset_data.get('background_color', props.background_color) + props.font_path = preset_data.get('font_path', props.font_path) + props.custom_font_path = preset_data.get('custom_font_path', props.custom_font_path) + props.use_custom_font = preset_data.get('use_custom_font', props.use_custom_font) + props.text_align = preset_data.get('text_align', props.text_align) + + props.is_updating = False + # Always generate preview after loading preset + generate_preview(context) + + self.report({'INFO'}, f"Loaded: {self.preset_name}") + + except Exception as e: + props.is_updating = False + self.report({'ERROR'}, f"Failed: {str(e)}") + return {'CANCELLED'} + + return {'FINISHED'} + +class TEXT_TEXTURE_OT_delete_preset(Operator): + """Delete preset""" + bl_idname = "text_texture.delete_preset" + bl_label = "Delete Preset" + bl_description = "Delete selected preset" + bl_options = {'REGISTER', 'UNDO'} + + preset_name: StringProperty() + + def execute(self, context): + props = context.scene.text_texture_props + + try: + preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") + if os.path.exists(preset_file): + os.remove(preset_file) + + for i, preset in enumerate(props.presets): + if preset.name == self.preset_name: + props.presets.remove(i) + break + + self.report({'INFO'}, f"Deleted: {self.preset_name}") + + except Exception as e: + self.report({'ERROR'}, f"Failed: {str(e)}") + return {'CANCELLED'} + + return {'FINISHED'} + +class TEXT_TEXTURE_OT_open_panel(Operator): + """Open Text Texture Generator panel""" + bl_idname = "text_texture.open_panel" + bl_label = "Open Text Texture Panel" + bl_description = "Open the Text Texture Generator panel in the sidebar" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + # Ensure the sidebar is visible + for area in context.screen.areas: + if area.type == 'VIEW_3D': + for space in area.spaces: + if space.type == 'VIEW_3D': + space.show_region_ui = True + # Switch to Tool tab + area.tag_redraw() + self.report({'INFO'}, "Text Texture panel opened in 3D View sidebar (press N if not visible)") + return {'FINISHED'} + elif area.type == 'NODE_EDITOR': + for space in area.spaces: + if space.type == 'NODE_EDITOR': + space.show_region_ui = True + area.tag_redraw() + self.report({'INFO'}, "Text Texture panel opened in Shader Editor sidebar") + return {'FINISHED'} + + self.report({'WARNING'}, "Please open a 3D View or Shader Editor first") + return {'CANCELLED'} + +class TEXT_TEXTURE_OT_refresh_fonts(Operator): + """Refresh fonts""" + bl_idname = "text_texture.refresh_fonts" + bl_label = "Refresh Fonts" + bl_description = "Refresh font list" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + if hasattr(get_font_enum_items, 'cached_fonts'): + del get_font_enum_items.cached_fonts + get_font_enum_items.cached_fonts = get_system_fonts() + self.report({'INFO'}, f"Found {len(get_font_enum_items.cached_fonts)} fonts") + return {'FINISHED'} + +# ============================================================================ +# MENUS +# ============================================================================ + +def menu_func_node_editor(self, context): + """Add menu item to Node Editor""" + self.layout.separator() + self.layout.label(text="Text Texture Generator", icon='TEXT') + self.layout.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA') + +def menu_func_view3d(self, context): + """Add menu item to 3D View""" + self.layout.separator() + self.layout.operator("text_texture.generate", text="Generate Text Texture", icon='TEXT') + +def menu_func_image(self, context): + """Add to Image menu""" + self.layout.separator() + self.layout.operator("text_texture.generate", text="Generate Text Texture", icon='TEXT') + +# ============================================================================ +# UI PANELS +# ============================================================================ + +class TEXT_TEXTURE_PT_panel(Panel): + """Main Panel in Shader Editor""" + bl_label = "Text Texture Generator" + bl_idname = "TEXT_TEXTURE_PT_panel" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_category = "Tool" + + @classmethod + def poll(cls, context): + return context.space_data.type == 'NODE_EDITOR' and context.space_data.tree_type == 'ShaderNodeTree' + + def draw(self, context): + layout = self.layout + props = context.scene.text_texture_props + + # Always ensure preview exists and is up to date + # Generate initial preview on first draw + if not props.preview_image: + # Schedule preview generation using timer + def generate_preview_delayed(): + generate_preview(context) + return None # Stop timer + bpy.app.timers.register(generate_preview_delayed, first_interval=0.1) + + # LIVE PREVIEW - Simplified and clean + if props.preview_image: + box = layout.box() + box.template_ID_preview(props, "preview_image", rows=8, cols=8) + else: + box = layout.box() + box.label(text="Generating preview...", icon='TIME') + + # TEXT INPUT - BIG + box = layout.box() + box.label(text="Text Input:", icon='TEXT') + row = box.row() + row.scale_y = 2.0 + row.prop(props, "text", text="") + + # SETTINGS + box = layout.box() + box.label(text="Texture Settings:", icon='SETTINGS') + + # Dimensions + col = box.column() + row = col.row(align=True) + row.prop(props, "texture_width", text="Width") + row.prop(props, "texture_height", text="Height") + col.label(text=f"Output: {props.texture_width}×{props.texture_height}px", icon='INFO') + + # Font Settings + col.separator() + col.label(text="Font:", icon='FONT_DATA') + col.prop(props, "use_custom_font") + if props.use_custom_font: + col.prop(props, "custom_font_path", text="") + else: + row = col.row(align=True) + row.prop(props, "font_path", text="") + row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH') + + row = col.row(align=True) + row.prop(props, "font_size") + row.prop(props, "padding") + col.prop(props, "text_align") + + # Colors + col.separator() + col.label(text="Colors:", icon='COLOR') + col.prop(props, "text_color") + col.prop(props, "background_transparent") + if not props.background_transparent: + col.prop(props, "background_color") + + # Preview settings (compact) + col.separator() + row = col.row(align=True) + row.prop(props, "preview_size", text="Preview") + row.prop(props, "preview_bg_type", text="") + if props.preview_bg_type == 'color': + col.prop(props, "preview_bg_color", text="") + elif props.preview_bg_type == 'gradient': + row = col.row(align=True) + row.prop(props, "preview_bg_color", text="") + row.prop(props, "preview_bg_color2", text="") + + # GENERATE BUTTON + layout.separator() + row = layout.row() + row.scale_y = 2.0 + row.operator("text_texture.generate", text="GENERATE FINAL TEXTURE", icon='IMAGE_DATA') + +class TEXT_TEXTURE_PT_presets(Panel): + """Presets Panel""" + bl_label = "Presets" + bl_idname = "TEXT_TEXTURE_PT_presets" + bl_parent_id = "TEXT_TEXTURE_PT_panel" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + props = context.scene.text_texture_props + + # Load presets + if not props.presets: + preset_dir = get_preset_path() + if os.path.exists(preset_dir): + for filename in os.listdir(preset_dir): + if filename.endswith('.json'): + preset_name = os.path.splitext(filename)[0] + if preset_name not in [p.name for p in props.presets]: + preset = props.presets.add() + preset.name = preset_name + + # Save preset + box = layout.box() + box.label(text="Save Current Settings:", icon='EXPORT') + row = box.row(align=True) + row.prop(props, "new_preset_name", text="") + row.operator("text_texture.save_preset", text="Save", icon='ADD') + + # Load preset + if props.presets: + box = layout.box() + box.label(text="Load Preset:", icon='IMPORT') + for preset in props.presets: + row = box.row(align=True) + row.operator("text_texture.load_preset", text=preset.name).preset_name = preset.name + row.operator("text_texture.delete_preset", text="", icon='X').preset_name = preset.name + +class TEXT_TEXTURE_PT_panel_3d(Panel): + """3D View Panel - Access from sidebar (N key)""" + bl_label = "Text Texture Generator" + bl_idname = "TEXT_TEXTURE_PT_panel_3d" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "Tool" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + props = context.scene.text_texture_props + + # Generate initial preview if needed + if not props.preview_image: + # Schedule preview generation using timer + def generate_preview_delayed(): + generate_preview(context) + return None # Stop timer + bpy.app.timers.register(generate_preview_delayed, first_interval=0.1) + + # LIVE PREVIEW - Simplified + if props.preview_image: + box = layout.box() + box.template_ID_preview(props, "preview_image", rows=6, cols=6) + else: + box = layout.box() + box.label(text="Generating preview...", icon='TIME') + + # TEXT INPUT + box = layout.box() + box.label(text="Text:", icon='TEXT') + row = box.row() + row.scale_y = 1.5 + row.prop(props, "text", text="") + + # Quick Settings + col = layout.column() + row = col.row(align=True) + row.prop(props, "texture_width", text="W") + row.prop(props, "texture_height", text="H") + col.prop(props, "font_size") + col.prop(props, "text_color") + col.prop(props, "background_transparent") + + # Preview Toggle + # Preview is always enabled - removed toggle + + # Generate + layout.separator() + row = layout.row() + row.scale_y = 1.5 + row.operator("text_texture.generate", text="Generate Texture", icon='IMAGE_DATA') + +class TEXT_TEXTURE_PT_panel_properties(Panel): + """Properties Panel""" + bl_label = "Text Texture Generator" + bl_idname = "TEXT_TEXTURE_PT_panel_properties" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "material" + bl_category = "Tool" + + def draw(self, context): + layout = self.layout + props = context.scene.text_texture_props + + # Generate initial preview if needed + if not props.preview_image: + # Schedule preview generation using timer + def generate_preview_delayed(): + generate_preview(context) + return None # Stop timer + bpy.app.timers.register(generate_preview_delayed, first_interval=0.1) + + # LIVE PREVIEW - Simplified + if props.preview_image: + box = layout.box() + box.template_ID_preview(props, "preview_image", rows=6, cols=6) + else: + box = layout.box() + box.label(text="Generating preview...", icon='TIME') + + # TEXT INPUT + box = layout.box() + box.label(text="Text:", icon='TEXT') + row = box.row() + row.scale_y = 1.5 + row.prop(props, "text", text="") + + # Quick Settings + col = layout.column() + row = col.row(align=True) + row.prop(props, "texture_width", text="W") + row.prop(props, "texture_height", text="H") + col.prop(props, "font_size") + col.prop(props, "text_color") + col.prop(props, "background_transparent") + if not props.background_transparent: + col.prop(props, "background_color") + + # Generate + layout.separator() + row = layout.row() + row.scale_y = 1.5 + row.operator("text_texture.generate", text="Generate Texture", icon='IMAGE_DATA') + +# ============================================================================ +# REGISTRATION +# ============================================================================ + +classes = ( + TEXT_TEXTURE_Preset, + TEXT_TEXTURE_Properties, + TEXT_TEXTURE_OT_generate, + TEXT_TEXTURE_OT_refresh_preview, + TEXT_TEXTURE_OT_open_panel, + TEXT_TEXTURE_OT_save_preset, + TEXT_TEXTURE_OT_load_preset, + TEXT_TEXTURE_OT_delete_preset, + TEXT_TEXTURE_OT_refresh_fonts, + TEXT_TEXTURE_PT_panel, + TEXT_TEXTURE_PT_presets, + TEXT_TEXTURE_PT_panel_3d, + TEXT_TEXTURE_PT_panel_properties, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.text_texture_props = PointerProperty(type=TEXT_TEXTURE_Properties) + + # Add to menus + bpy.types.NODE_MT_add.append(menu_func_node_editor) + bpy.types.VIEW3D_MT_add.append(menu_func_view3d) + bpy.types.VIEW3D_MT_mesh_add.append(menu_func_view3d) + bpy.types.NODE_MT_node.append(menu_func_node_editor) + + # Add to Image menu if available + if hasattr(bpy.types, 'IMAGE_MT_image'): + bpy.types.IMAGE_MT_image.append(menu_func_image) + +def unregister(): + # Clean up preview + try: + if bpy.context.scene: + props = bpy.context.scene.text_texture_props + if props and props.preview_image: + bpy.data.images.remove(props.preview_image) + except Exception: + pass + + if "TextTexturePreview" in bpy.data.images: + bpy.data.images.remove(bpy.data.images["TextTexturePreview"]) + + # Remove from menus + bpy.types.NODE_MT_add.remove(menu_func_node_editor) + bpy.types.VIEW3D_MT_add.remove(menu_func_view3d) + bpy.types.VIEW3D_MT_mesh_add.remove(menu_func_view3d) + bpy.types.NODE_MT_node.remove(menu_func_node_editor) + + if hasattr(bpy.types, 'IMAGE_MT_image'): + bpy.types.IMAGE_MT_image.remove(menu_func_image) + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.text_texture_props + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/blender_manifest.toml b/blender_manifest.toml new file mode 100644 index 0000000..9301be6 --- /dev/null +++ b/blender_manifest.toml @@ -0,0 +1,19 @@ +schema_version = "1.0.0" + +id = "text_texture_generator" +version = "1.0.0" +name = "Text Texture Generator" +tagline = "Generate image textures from text with customizable styling" +maintainer = "Marc Mintel " +type = "add-on" +support = "COMMUNITY" + +tags = ["Material", "Shader", "Texture", "Text", "Image"] + +blender_version_min = "4.0.0" + +license = ["GPL-3.0-or-later"] + +# Dependencies +[dependencies] +pillow = ">=10.0.0" \ No newline at end of file