Files
Text-Texture-Generator-for-…/__init__.py
2025-08-08 13:31:51 +07:00

1184 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <marc@mintel.me>",
"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()