From 80042751d83f19d7a529a38515ec56c195d80126 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 8 Aug 2025 13:31:51 +0700 Subject: [PATCH] init --- Archive.zip | Bin 0 -> 9626 bytes README.md | 167 ++++++ __init__.py | 1184 +++++++++++++++++++++++++++++++++++++++++ blender_manifest.toml | 19 + 4 files changed, 1370 insertions(+) create mode 100644 Archive.zip create mode 100644 README.md create mode 100644 __init__.py create mode 100644 blender_manifest.toml diff --git a/Archive.zip b/Archive.zip new file mode 100644 index 0000000000000000000000000000000000000000..9a05eb364808bb8b5b34fb1aa6f0cf640f73d3f8 GIT binary patch literal 9626 zcma)?WlSAF*XJ(|#a&8qcXxMpr+6=NDO%j!-Q6i#xHw$gy|_CRcZ%!w`SR|w$!4>2 za&l&p$(hOg=fmX8PfZa976*X%&jM@_4FUhvkO5=>6B8SI8y6E37DrE44`cuo>>U&U z@NYfQJF$d9HjLRO^ zR}@JmheF-H?BlciBIkJM$a$RlZq3{JdL{bdx#39hJNZnZvV;9jX)O3(+_?E7Z;HNm z1ek9;){hkZk!d;ti)*Ba#_e)3`R!l>E`*r;yOifzUl&x<{@yILbHx?9rqJv+{N+;k zRI^io^t0_*=jzEEZ}98e?{o`->ySf;%zHuiJF_bGoiAqiAk*Xlks8+_t8vFSggKim zFkNOT>(Djg2T=I_2`*Tb5$Uj_Y05y-&xGx@VS-QaAhO50iV_q$zKiE4-uYn0V|M8T z&;F9m|N6MZ|915INwxC+%an5lhZg$1z?p0Kc@uDy?_e~AvS2$LZ7uLaTRumnPl5~7 zDe|pb>J|GX(nv;A1dS6fr+H@ygKaWwC*zH_y#o~r?NpVLpj3UPl1iNzs`PbWGd$py zE(VCpy+}VrtKX<*G@kjRAnq!xUjswJS*9_#d%3K<6}a=Qz67Kx<`k4Xsh^<)HSCE= z0~cK^_#2O~JJ_Z?&(!agW|9OJZ_u>h3{$uKA%+y-&P|+y66Oz-)h)XW3vQSjKfKr- zOqm+^w&&3pvCH>{)A%qbWcVwP&h${Lr-)E-HYn8H3@2O+N-Y?Kw-Rg}L@Hl@7>w;T za7pW<4u%pwx6;o_)y4;#MT^}*XY0DnpSu~_8C9)wZ;UeMzw%^>?Qc_l&t>U8Oa=x@ z9)lOFJdxj7is0TU%c%0mCi1=71&tnST{rNOyLJW4i6bUtt; zA4SoMx33c8DQREUEr!!&{>;D8;TKpKv?c;BPs&z>fUoWS4V%}7%nsL_@iX$dE-Bzm z`SHN+Y5Y?`i~e?`OnBjDnD+Sq$%vLM*<_~4IsKg=Tf^;|UnXGXg`Xk4Od!UuX+WGp*L1>@u#;_216wG?c13iYiHwja1j}S7WVA0%Ky{(rs{{ zvYBRpm*iv+XO|w<8!_$gm%0amZ0hfTR%g=%kj?`l(Py&squqS5y9Zu12^R;x)Jszd zK+A?1A)9Q%mU{>9MA|wh)?FQ6nm;js6c?o@|AfzE0CP-TILRsFM>X~lX3H9>d(9Gj zb+ZJkHF#7^eN>HWhM^6%$#W(0HQ_gFK(Qm{! zsN(cb3P^N}Fy2cXkuqcAPsC7Ohn@+4Jr)b!9krWc1bhN2q_M-e2^Z$)`wAkbwNvG= z_QhVH%HKd zNv`8$!m8&r`uOnrxD&uF%Ul{ZOXTI&FL>6Vqm;(!pTdY3a+)a4dZZ$yS)RINq;wu1 z!@0H=&_%V5b$~ZsI{D822{eOJnoQEyl(nKSR^H69vD{Cu*sQ}i**-*9TO)Z{b) ze@Df&W$fg&7NTS$7NMLfozweRSCdcNhx#+b;$^HeV`QAguu|r-GNR$5At`5db{awb z&nRl}n+3B%ud$Suji8JSwmg40qvzM6kMV+!z%LJ~FZc-M%KpHnXPGQAN*kY;5geou z>+^tJKI!_l{h3KSGnU3i!ze?vMfo~dN!jM~pA(ETDHAv|S%KT%@ZY+>qOv@P`<1TG zTxH+Q#XTOfA&a^S5mmiI-^;VWk6oIT|3rmL?toFWKRJ5lxYEH?ePhm%A0R7GPxxDl?(oY5 zL5o40u2_q|iSxGB+}qfrAcQD^i)XwLjoDW>F&JbfYM1jtO{egc_q6;zLk|VnPiaM?a9=Ce8J3wkKv>8)nZaeoFKuu7 z34R_9ex`uE>=~-qNWG|iiUTMLQ)tDehrM{aqIIOGy)nd!nc=skrRLuV$&sE|k5$F- z$FACebCQo;!$CsyUQdp8+=#QWrt00peX@2dVbwPmYfOLlpQr$P=U$*4SLj3C3OsFOO=o?^E1{Db=LW Xc<58`A#Rn@N3SizUak-BS&E=NS0 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 L3SgP)KcoKzVMjO0 literal 0 HcmV?d00001 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