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()