This commit is contained in:
2025-09-11 12:12:58 +03:00
parent 32fc626caf
commit 80a71c1dc1
33 changed files with 6051 additions and 6826 deletions

30
ui/__init__.py Normal file
View File

@@ -0,0 +1,30 @@
# Import all UI functionality
from .preview import *
from .panels import (
TEXT_TEXTURE_PT_panel,
TEXT_TEXTURE_PT_panel_3d,
TEXT_TEXTURE_PT_panel_properties,
)
# Menu functions for Blender integration
def menu_func_node_editor(self, context):
"""Add menu item to Node Editor's Add menu"""
self.layout.operator("text_texture.generate", text="Text Texture", icon='IMAGE_DATA')
def menu_func_view3d(self, context):
"""Add menu item to 3D View's Add menu"""
self.layout.operator("text_texture.generate", text="Text Texture", icon='IMAGE_DATA')
def menu_func_image(self, context):
"""Add menu item to Image Editor menu"""
self.layout.operator("text_texture.generate", text="Text Texture", icon='IMAGE_DATA')
# Export all UI functions
__all__ = [
'TEXT_TEXTURE_PT_panel',
'TEXT_TEXTURE_PT_panel_3d',
'TEXT_TEXTURE_PT_panel_properties',
'menu_func_node_editor',
'menu_func_view3d',
'menu_func_image'
]

Binary file not shown.

Binary file not shown.

940
ui/panels.py Normal file
View File

@@ -0,0 +1,940 @@
"""
Text Texture Generator - UI Panels Module
Contains all UI panel classes for different editor contexts.
"""
import bpy
from bpy.types import Panel
from ..core.generation_engine import generate_preview
# ============================================================================
# SHARED UI COMPONENTS
# ============================================================================
def draw_collapsible_header(layout, prop_name, text, icon='TRIA_DOWN'):
"""Draw a collapsible section header with expand/collapse arrow"""
props = bpy.context.scene.text_texture_props
expanded = getattr(props, prop_name)
box = layout.box()
header_row = box.row()
# Draw arrow icon and toggle expanded state
arrow_icon = 'TRIA_DOWN' if expanded else 'TRIA_RIGHT'
header_row.prop(props, prop_name, text="", icon=arrow_icon, emboss=False)
header_row.label(text=text, icon=icon)
return box if expanded else None
def draw_text_input_section(layout, props):
"""Draw the main text input field"""
layout.prop(props, "text", text="")
def draw_font_dropdown_section(layout, props):
"""Draw basic font dropdown for top level"""
row = layout.row(align=True)
row.prop(props, "font_path", text="")
row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH')
def draw_generate_button(layout):
"""Draw the main generate button"""
row = layout.row()
row.scale_y = 1.5
row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA')
def draw_image_overlays_section(layout, props):
"""Draw image overlays controls with collapsible individual overlays"""
# Add overlay button
row = layout.row()
row.operator("text_texture.add_overlay", text="Add Image Overlay", icon='ADD')
# Individual overlay controls
if props.image_overlays:
layout.separator()
for i, overlay in enumerate(props.image_overlays):
# Create collapsible box for each overlay
box = layout.box()
# Header row with controls
header_row = box.row(align=True)
# Enable/Disable checkbox with clear label
header_row.prop(overlay, "enabled", text="", icon='CHECKBOX_HLT' if overlay.enabled else 'CHECKBOX_DEHLT')
# Overlay name
name_row = header_row.row()
name_row.enabled = overlay.enabled # Gray out name if disabled
name_row.prop(overlay, "name", text="")
# Action buttons
# Duplicate button
dup_op = header_row.operator("text_texture.duplicate_overlay", text="", icon='DUPLICATE')
dup_op.overlay_index = i
# Delete button
del_op = header_row.operator("text_texture.delete_overlay", text="", icon='TRASH')
del_op.overlay_index = i
# Move buttons for reordering
if i > 0:
move_up_op = header_row.operator("text_texture.move_overlay", text="", icon='TRIA_UP')
move_up_op.overlay_index = i
move_up_op.direction = 'UP'
if i < len(props.image_overlays) - 1:
move_down_op = header_row.operator("text_texture.move_overlay", text="", icon='TRIA_DOWN')
move_down_op.overlay_index = i
move_down_op.direction = 'DOWN'
# Show details section with enable/disable state
details_col = box.column()
details_col.enabled = overlay.enabled # Gray out all controls if disabled
# Status indicator
if not overlay.enabled:
status_row = details_col.row()
status_row.label(text="⚠️ Overlay Disabled", icon='INFO')
details_col.separator()
# Image file selection
details_col.prop(overlay, "image_path", text="Image File")
# Positioning mode
details_col.prop(overlay, "positioning_mode", text="Position Mode")
if overlay.positioning_mode == 'ABSOLUTE':
# Position controls for absolute mode
pos_row = details_col.row(align=True)
pos_row.prop(overlay, "x_position", text="X Position")
pos_row.prop(overlay, "y_position", text="Y Position")
elif overlay.positioning_mode in ['APPEND', 'PREPEND']:
# Spacing controls for append/prepend modes with percentage labels
details_col.prop(overlay, "text_spacing", text="Text Spacing (%)")
details_col.prop(overlay, "image_spacing", text="Image Spacing (%)")
# Transform controls
trans_row = details_col.row(align=True)
trans_row.prop(overlay, "scale", text="Scale")
trans_row.prop(overlay, "rotation", text="Rotation")
trans_row.prop(overlay, "z_index", text="Z-Level")
def draw_positioning_section(layout, props):
"""Draw positioning and alignment controls"""
from ..utils.system import get_anchor_point_matrix
# Dimensions moved here from top level
layout.label(text="Dimensions:", icon='SETTINGS')
row = layout.row(align=True)
row.prop(props, "texture_width", text="Width")
row.prop(props, "texture_height", text="Height")
layout.separator()
# Prepend/Append Text Section
layout.label(text="Additional Text:", icon='TEXT')
# Compact layout selector
layout.prop(props, "prepend_append_layout", text="Layout")
# Minimal prepend/append controls
row = layout.row(align=True)
row.prop(props, "prepend_text", text="Prepend")
if props.prepend_text.strip():
row.prop(props, "prepend_margin", text="", slider=True)
row = layout.row(align=True)
row.prop(props, "append_text", text="Append")
if props.append_text.strip():
row.prop(props, "append_margin", text="", slider=True)
layout.separator()
layout.prop(props, "position_mode", text="Mode")
if props.position_mode == 'PRESET':
layout.separator()
layout.label(text="Anchor Point:", icon='ANCHOR_TOP')
anchor_matrix = get_anchor_point_matrix()
for row_idx, row in enumerate(anchor_matrix):
grid_row = layout.row(align=True)
grid_row.scale_y = 1.2
for col_idx, anchor in enumerate(row):
if anchor == props.anchor_point:
op = grid_row.operator("text_texture.set_anchor_point", text="", depress=True)
else:
op = grid_row.operator("text_texture.set_anchor_point", text="")
op.anchor_point = anchor
# Margins
layout.separator()
margin_row = layout.row(align=True)
margin_row.label(text="Margins:")
margin_row.prop(props, "margins_linked", text="", icon='LINKED' if props.margins_linked else 'UNLINKED', toggle=True)
margins_row = layout.row(align=True)
margins_row.prop(props, "margin_x", text="X")
if not props.margins_linked:
margins_row.prop(props, "margin_y", text="Y")
# Offsets
layout.separator()
layout.label(text="Fine-tune (pixels):")
offset_row = layout.row(align=True)
offset_row.prop(props, "offset_x", text="X")
offset_row.prop(props, "offset_y", text="Y")
else: # MANUAL mode
layout.separator()
layout.prop(props, "manual_position_unit", text="Unit")
manual_row = layout.row(align=True)
manual_row.prop(props, "manual_position_x", text="X")
manual_row.prop(props, "manual_position_y", text="Y")
layout.separator()
layout.prop(props, "constrain_to_canvas")
def draw_font_settings_section(layout, props):
"""Draw extended font settings"""
# Font size moved here from top level
layout.prop(props, "font_size")
layout.separator()
layout.label(text="Font Selection:")
# Default font dropdown moved here from top level
row = layout.row(align=True)
row.prop(props, "font_path", text="")
row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH')
layout.separator()
# Custom font controls moved here from top level
layout.prop(props, "use_custom_font")
if props.use_custom_font:
layout.prop(props, "custom_font_path", text="")
layout.separator()
row = layout.row(align=True)
row.prop(props, "padding")
row.prop(props, "text_align")
def draw_colors_section(layout, props):
"""Draw color controls for text and background"""
layout.label(text="Text Color:", icon='COLOR')
layout.prop(props, "text_color", text="")
layout.separator()
layout.label(text="Background Color:", icon='IMAGE_PLANE')
layout.prop(props, "background_transparent")
if not props.background_transparent:
layout.prop(props, "background_color", text="")
def draw_preview_options_section(layout, props):
"""Draw preview options (preview-specific settings only)"""
layout.prop(props, "preview_size", text="Preview Size")
layout.separator()
layout.label(text="Preview Background Display:", icon='SCENE')
layout.prop(props, "preview_bg_type", text="Background")
if props.preview_bg_type == 'color':
layout.prop(props, "preview_bg_color", text="Preview Display Color")
elif props.preview_bg_type == 'gradient':
layout.prop(props, "preview_bg_color", text="Color 1")
layout.prop(props, "preview_bg_color2", text="Color 2")
def draw_presets_section(layout, props):
"""Draw presets save/load controls"""
# Save preset section
save_row = layout.row(align=True)
save_row.prop(props, "new_preset_name", text="", placeholder="Preset Name")
save_row.operator("text_texture.save_preset", text="Save", icon='FILE_NEW')
layout.separator()
# Import/Export and refresh buttons
backup_row = layout.row(align=True)
backup_row.operator("text_texture.export_presets", text="Export Backup", icon='EXPORT')
backup_row.operator("text_texture.import_presets", text="Import Backup", icon='IMPORT')
utility_row = layout.row(align=True)
utility_row.operator("text_texture.refresh_presets", text="Refresh", icon='FILE_REFRESH')
utility_row.operator("text_texture.show_migration_report", text="Migration Report", icon='INFO')
layout.separator()
# Load presets section
if props.presets:
layout.label(text=f"📁 Saved Presets ({len(props.presets)}):", icon='PRESET')
# Separate presets by source
blend_presets = [p for p in props.presets if p.source == 'BLEND_FILE']
persistent_presets = [p for p in props.presets if p.source == 'PERSISTENT_FILE']
local_presets = [p for p in props.presets if p.source == 'LOCAL_FILE']
# Show blend file presets first
if blend_presets:
layout.label(text="🎯 Blend File Presets (travel with file):", icon='FILE_BLEND')
for preset in blend_presets:
row = layout.row()
load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}")
load_btn.preset_name = preset.name
# Quick overwrite button
overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH')
overwrite_btn.overwrite = True
# Set the preset name so it can be overwritten
overwrite_btn.preset_name = preset.name
delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH')
delete_btn.preset_name = preset.name
# Show persistent presets second
if persistent_presets:
if blend_presets:
layout.separator()
layout.label(text="🛡️ Persistent Presets (survive addon updates):", icon='PREFERENCES')
for preset in persistent_presets:
row = layout.row()
load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}")
load_btn.preset_name = preset.name
# Quick overwrite button
overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH')
overwrite_btn.overwrite = True
# Set the preset name so it can be overwritten
overwrite_btn.preset_name = preset.name
delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH')
delete_btn.preset_name = preset.name
# Show local presets third
if local_presets:
if blend_presets or persistent_presets:
layout.separator()
layout.label(text="💻 Local Presets (this machine only):", icon='DISK_DRIVE')
for preset in local_presets:
row = layout.row()
load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}")
load_btn.preset_name = preset.name
# Quick overwrite button
overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH')
overwrite_btn.overwrite = True
# Set the preset name so it can be overwritten
overwrite_btn.preset_name = preset.name
delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH')
delete_btn.preset_name = preset.name
else:
layout.label(text="📂 No presets saved yet", icon='INFO')
layout.label(text="💡 Save your first preset above!", icon='LIGHT_DATA')
layout.separator()
info_box = layout.box()
info_box.label(text=" Presets are now saved in the blend file", icon='INFO')
info_box.label(text="They will travel with your .blend file!")
def draw_shader_section(layout, props):
"""Draw shader generation controls"""
# Create Shader Button (as requested)
row = layout.row()
row.scale_y = 1.5
row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL')
layout.separator()
# Clean minimalist controls
col = layout.column(align=True)
col.prop(props, "shader_type", text="Type")
# Show relevant options based on shader type
if props.shader_type == 'PRINCIPLED':
col.prop(props, "shader_connection", text="Connect")
if props.shader_connection == 'EMISSION':
col.prop(props, "emission_strength", text="Strength")
elif props.shader_type == 'EMISSION':
col.prop(props, "emission_strength", text="Strength")
layout.separator()
# Simple toggles
col = layout.column(align=True)
col.prop(props, "use_alpha", text="Use Alpha Channel")
col.prop(props, "auto_assign_material", text="Auto-Assign to Object")
# Clean status info
if props.auto_assign_material:
layout.separator()
info_row = layout.row()
info_row.scale_y = 0.8
try:
context = bpy.context
if context and context.active_object and context.active_object.type in ['MESH', 'CURVE']:
info_row.label(text=f"{context.active_object.name}", icon='CHECKMARK')
else:
info_row.label(text="Select mesh/curve object", icon='INFO')
except:
info_row.label(text="Select mesh/curve object", icon='INFO')
def draw_shader_options_only(layout, props):
"""Draw only shader options without the Create button - for collapsible section"""
# Clean grid layout for shader options
grid = layout.grid_flow(row_major=True, columns=2, align=True)
grid.prop(props, "shader_type", text="Type")
if props.shader_type == 'PRINCIPLED':
grid.prop(props, "shader_connection", text="Connect")
if props.shader_connection == 'EMISSION':
grid.prop(props, "emission_strength", text="Strength")
elif props.shader_type == 'EMISSION':
grid.prop(props, "emission_strength", text="Strength")
# Simple row for toggles
layout.separator()
row = layout.row(align=True)
row.prop(props, "use_alpha", toggle=True)
row.prop(props, "auto_assign_material", toggle=True)
# Shadow, reflection, and backfacing toggles
row = layout.row(align=True)
row.prop(props, "disable_shadows", toggle=True)
row.prop(props, "disable_reflections", toggle=True)
row.prop(props, "disable_backfacing", toggle=True)
def draw_normal_maps_section(layout, props):
"""Draw normal map generation controls"""
# Main toggle for normal map generation
layout.prop(props, "generate_normal_map", text="Generate Normal Map", icon='TEXTURE')
# Show controls only if normal map generation is enabled
if props.generate_normal_map:
layout.separator()
# Normal map settings in a clean layout
col = layout.column(align=True)
col.prop(props, "normal_map_strength", text="Strength", slider=True)
col.prop(props, "normal_map_blur_radius", text="Blur Radius", slider=True)
layout.separator()
layout.prop(props, "normal_map_invert", text="Invert (Emboss ↔ Deboss)")
# Info box with usage tips
layout.separator()
info_box = layout.box()
info_box.scale_y = 0.8
info_box.label(text="💡 Normal maps add depth and detail", icon='INFO')
info_box.label(text="• Higher strength = more pronounced effect")
info_box.label(text="• Blur smooths sharp edges")
info_box.label(text="• Invert changes raised/recessed effect")
def draw_text_fitting_section(layout, props):
"""Draw text fitting controls"""
# Main toggle for text fitting
layout.prop(props, "enable_text_fitting", text="Enable Text Fitting", icon='FULLSCREEN_ENTER')
# Show controls only if text fitting is enabled
if props.enable_text_fitting:
layout.separator()
# Fitting mode selection
layout.prop(props, "text_fitting_mode", text="Fitting Mode")
layout.separator()
# Margin setting
layout.prop(props, "text_fitting_margin", text="Margin", slider=True)
layout.separator()
# Font size limits
col = layout.column(align=True)
col.label(text="Font Size Limits:", icon='RESTRICT_SELECT_ON')
row = col.row(align=True)
row.prop(props, "min_font_size", text="Min")
row.prop(props, "max_font_size", text="Max")
# Info box with usage tips
layout.separator()
info_box = layout.box()
info_box.scale_y = 0.8
info_box.label(text="💡 Text fitting automatically sizes text", icon='INFO')
if props.text_fitting_mode == 'FIT_WIDTH':
info_box.label(text="• Fits text width to canvas width")
elif props.text_fitting_mode == 'FIT_HEIGHT':
info_box.label(text="• Fits text height to canvas height")
elif props.text_fitting_mode == 'FIT_BOTH':
info_box.label(text="• Fits both width and height (may distort)")
elif props.text_fitting_mode == 'FIT_CONTAIN':
info_box.label(text="• Fits entirely within canvas (preserves ratio)")
info_box.label(text="• Respects min/max font size limits")
def draw_multiline_section(layout, props):
"""Draw multiline text controls"""
# Main toggle for multiline text
layout.prop(props, "enable_multiline", text="Enable Multiline", icon='TEXT')
# Show controls only if multiline is enabled
if props.enable_multiline:
layout.separator()
# Line height and alignment settings
col = layout.column(align=True)
col.prop(props, "line_height", text="Line Height", slider=True)
col.prop(props, "text_alignment", text="Alignment")
layout.separator()
# Auto wrap settings
layout.prop(props, "auto_wrap", text="Auto Wrap")
if props.auto_wrap:
layout.prop(props, "wrap_width", text="Wrap Width (0 = texture width)")
layout.separator()
# Line limits and spacing
col = layout.column(align=True)
col.prop(props, "max_lines", text="Max Lines (0 = unlimited)")
col.prop(props, "line_spacing_pixels", text="Line Spacing (px)")
# Info box with usage tips
layout.separator()
info_box = layout.box()
info_box.scale_y = 0.8
info_box.label(text="💡 Multiline text supports:", icon='INFO')
info_box.label(text="• Use \\n for manual line breaks")
info_box.label(text="• Auto-wrap fits text to width")
info_box.label(text="• Alignment works per line")
info_box.label(text="• Line height controls spacing")
def draw_stroke_section(layout, props):
"""Draw stroke and outline controls"""
# Main toggle for stroke
layout.prop(props, "enable_stroke", text="Enable Stroke", icon='MOD_OUTLINE')
# Show controls only if stroke is enabled
if props.enable_stroke:
layout.separator()
# Stroke width and color
col = layout.column(align=True)
col.prop(props, "stroke_width", text="Width", slider=True)
col.prop(props, "stroke_color", text="Color")
layout.separator()
# Stroke position
layout.prop(props, "stroke_position", text="Position")
# Info box with usage tips
layout.separator()
info_box = layout.box()
info_box.scale_y = 0.8
info_box.label(text="💡 Stroke adds outline to text:", icon='INFO')
if props.stroke_position == 'OUTER':
info_box.label(text="• Outer: Expands text bounds outward")
elif props.stroke_position == 'INNER':
info_box.label(text="• Inner: Shrinks text, stroke inside")
elif props.stroke_position == 'CENTER':
info_box.label(text="• Center: Stroke on text edge")
info_box.label(text="• Width 0 = no stroke")
info_box.label(text="• Works with multiline text")
def draw_shadow_glow_section(layout, props):
"""Draw shadow and glow effects controls"""
# Drop shadow section
layout.prop(props, "enable_shadow", text="Enable Drop Shadow", icon='SHADERFX')
if props.enable_shadow:
layout.separator()
# Shadow offset controls in a row
offset_row = layout.row(align=True)
offset_row.prop(props, "shadow_offset_x", text="X Offset")
offset_row.prop(props, "shadow_offset_y", text="Y Offset")
# Shadow properties
col = layout.column(align=True)
col.prop(props, "shadow_blur", text="Shadow Blur", slider=True)
col.prop(props, "shadow_spread", text="Shadow Spread", slider=True)
col.prop(props, "shadow_color", text="Shadow Color")
layout.separator()
# Glow section
layout.prop(props, "enable_glow", text="Enable Glow", icon='LIGHT_SUN')
if props.enable_glow:
layout.separator()
# Glow properties
col = layout.column(align=True)
col.prop(props, "glow_blur", text="Glow Blur", slider=True)
col.prop(props, "glow_color", text="Glow Color")
# Info box with usage tips
if props.enable_shadow or props.enable_glow:
layout.separator()
info_box = layout.box()
info_box.scale_y = 0.8
info_box.label(text="💡 Shadow & Glow effects:", icon='INFO')
if props.enable_shadow:
info_box.label(text="• Drop shadow: offset + blur for depth")
if props.enable_glow:
info_box.label(text="• Glow: blur without offset for aura effect")
info_box.label(text="• Higher blur = softer effect")
info_box.label(text="• Alpha controls effect intensity")
def draw_blur_section(layout, props):
"""Draw blur effects controls"""
# Main toggle for blur
layout.prop(props, "enable_blur", text="Enable Blur", icon='MOD_SMOOTH')
# Show controls only if blur is enabled
if props.enable_blur:
layout.separator()
# Blur radius setting
layout.prop(props, "blur_radius", text="Blur Radius", slider=True)
# Info box with usage tips
layout.separator()
info_box = layout.box()
info_box.scale_y = 0.8
info_box.label(text="💡 Blur effect softens text edges:", icon='INFO')
info_box.label(text="• Applied to main text content only")
info_box.label(text="• Higher radius = softer/more blurred")
info_box.label(text="• Works with all other effects")
info_box.label(text="• Radius 0 = no blur effect")
# ============================================================================
# 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):
# DEBUG: Log poll method calls
print(f"[TTG DEBUG] Panel poll called - space_data: {context.space_data}")
print(f"[TTG DEBUG] space_data.type: {context.space_data.type if context.space_data else 'None'}")
if hasattr(context.space_data, 'tree_type'):
print(f"[TTG DEBUG] tree_type: {context.space_data.tree_type}")
else:
print("[TTG DEBUG] No tree_type attribute")
# More permissive poll condition to ensure panel shows up
if context.space_data.type == 'NODE_EDITOR':
if hasattr(context.space_data, 'tree_type'):
result = context.space_data.tree_type == 'ShaderNodeTree'
print(f"[TTG DEBUG] Poll result: {result}")
return result
else:
# Allow panel even without tree_type (sometimes happens)
print("[TTG DEBUG] Poll result: True (no tree_type but NODE_EDITOR)")
return True
print("[TTG DEBUG] Poll result: False (not NODE_EDITOR)")
return False
def draw(self, context):
layout = self.layout
props = context.scene.text_texture_props
# TOP LEVEL - Always Visible Controls
# Text Input
box = layout.box()
box.label(text="Text:", icon='TEXT')
draw_text_input_section(box, props)
# Generate Button
layout.separator(factor=0.5)
draw_generate_button(layout)
# CREATE SHADER BUTTON AT TOP LEVEL - AS REQUESTED
layout.separator(factor=0.5)
create_shader_row = layout.row()
create_shader_row.scale_y = 1.5
create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL')
# Preview Button at top level
layout.separator(factor=0.5)
preview_row = layout.row()
preview_row.scale_y = 1.5
preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
layout.separator(factor=1.2)
# COLLAPSIBLE SECTIONS
# Multiline Section (collapsed by default)
multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT')
if multiline_section:
draw_multiline_section(multiline_section, props)
# Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON
shader_section = draw_collapsible_header(layout, "expand_shader", "Shader Options", 'SETTINGS')
if shader_section:
draw_shader_options_only(shader_section, props)
# Image Overlays Section (collapsed by default)
overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA')
if overlay_section:
draw_image_overlays_section(overlay_section, props)
# Positioning & Layout Section (collapsed by default)
position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER')
if position_section:
draw_positioning_section(position_section, props)
# Font Settings Section (collapsed by default)
font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
if font_section:
draw_font_settings_section(font_section, props)
# Text Fitting Section (collapsed by default)
text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER')
if text_fitting_section:
draw_text_fitting_section(text_fitting_section, props)
# Stroke Section (collapsed by default)
stroke_section = draw_collapsible_header(layout, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE')
if stroke_section:
draw_stroke_section(stroke_section, props)
# Shadow & Glow Section (collapsed by default)
shadow_glow_section = draw_collapsible_header(layout, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX')
if shadow_glow_section:
draw_shadow_glow_section(shadow_glow_section, props)
# Blur Section (collapsed by default)
blur_section = draw_collapsible_header(layout, "expand_blur", "Blur Effects", 'MOD_SMOOTH')
if blur_section:
draw_blur_section(blur_section, props)
# Normal Maps Section (collapsed by default)
normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE')
if normal_maps_section:
draw_normal_maps_section(normal_maps_section, props)
# Colors Section (collapsed by default)
colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
if colors_section:
draw_colors_section(colors_section, props)
# Presets Section (collapsed by default)
presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
if presets_section:
draw_presets_section(presets_section, props)
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
# TOP LEVEL - Always Visible Controls
# Text Input
box = layout.box()
box.label(text="Text:", icon='TEXT')
draw_text_input_section(box, props)
# Generate Button
layout.separator(factor=0.5)
draw_generate_button(layout)
# CREATE SHADER BUTTON AT TOP LEVEL - AS REQUESTED
layout.separator(factor=0.5)
create_shader_row = layout.row()
create_shader_row.scale_y = 1.5
create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL')
# Preview Button at top level
layout.separator(factor=0.5)
preview_row = layout.row()
preview_row.scale_y = 1.5
preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
layout.separator(factor=1.2)
# COLLAPSIBLE SECTIONS
# Multiline Section (collapsed by default)
multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT')
if multiline_section:
draw_multiline_section(multiline_section, props)
# Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON
shader_section = draw_collapsible_header(layout, "expand_shader", "Shader Options", 'SETTINGS')
if shader_section:
draw_shader_options_only(shader_section, props)
# Image Overlays Section (collapsed by default)
overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA')
if overlay_section:
draw_image_overlays_section(overlay_section, props)
# Positioning & Layout Section (collapsed by default)
position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER')
if position_section:
draw_positioning_section(position_section, props)
# Font Settings Section (collapsed by default)
font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
if font_section:
draw_font_settings_section(font_section, props)
# Text Fitting Section (collapsed by default)
text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER')
if text_fitting_section:
draw_text_fitting_section(text_fitting_section, props)
# Stroke Section (collapsed by default)
stroke_section = draw_collapsible_header(layout, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE')
if stroke_section:
draw_stroke_section(stroke_section, props)
# Shadow & Glow Section (collapsed by default)
shadow_glow_section = draw_collapsible_header(layout, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX')
if shadow_glow_section:
draw_shadow_glow_section(shadow_glow_section, props)
# Tiling Section removed - feature was removed from addon
# Blur Section (collapsed by default)
blur_section = draw_collapsible_header(layout, "expand_blur", "Blur Effects", 'MOD_SMOOTH')
if blur_section:
draw_blur_section(blur_section, props)
# Batch Processing Section removed - feature was removed from addon
# Normal Maps Section (collapsed by default)
normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE')
if normal_maps_section:
draw_normal_maps_section(normal_maps_section, props)
# Colors Section (collapsed by default)
colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
if colors_section:
draw_colors_section(colors_section, props)
# Presets Section (collapsed by default)
presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
if presets_section:
draw_presets_section(presets_section, props)
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
# TOP LEVEL - Always Visible Controls
# Text Input
box = layout.box()
box.label(text="Text:", icon='TEXT')
draw_text_input_section(box, props)
# Generate Button
layout.separator(factor=0.5)
draw_generate_button(layout)
# Preview Button at top level
layout.separator(factor=0.5)
preview_row = layout.row()
preview_row.scale_y = 1.5
preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
layout.separator(factor=1.2)
# COLLAPSIBLE SECTIONS
# Multiline Section (collapsed by default)
multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT')
if multiline_section:
draw_multiline_section(multiline_section, props)
# Image Overlays Section (collapsed by default)
overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA')
if overlay_section:
draw_image_overlays_section(overlay_section, props)
# Positioning & Layout Section (collapsed by default)
position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER')
if position_section:
draw_positioning_section(position_section, props)
# Font Settings Section (collapsed by default)
font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
if font_section:
draw_font_settings_section(font_section, props)
# Text Fitting Section (collapsed by default)
text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER')
if text_fitting_section:
draw_text_fitting_section(text_fitting_section, props)
# Stroke Section (collapsed by default)
stroke_section = draw_collapsible_header(layout, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE')
if stroke_section:
draw_stroke_section(stroke_section, props)
# Shadow & Glow Section (collapsed by default)
shadow_glow_section = draw_collapsible_header(layout, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX')
if shadow_glow_section:
draw_shadow_glow_section(shadow_glow_section, props)
# Tiling Section removed - feature was removed from addon
# Blur Section (collapsed by default)
blur_section = draw_collapsible_header(layout, "expand_blur", "Blur Effects", 'MOD_SMOOTH')
if blur_section:
draw_blur_section(blur_section, props)
# Batch Processing Section removed - feature was removed from addon
# Normal Maps Section (collapsed by default)
normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE')
if normal_maps_section:
draw_normal_maps_section(normal_maps_section, props)
# Colors Section (collapsed by default)
colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
if colors_section:
draw_colors_section(colors_section, props)
# Presets Section (collapsed by default)
presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
if presets_section:
draw_presets_section(presets_section, props)

153
ui/preview.py Normal file
View File

@@ -0,0 +1,153 @@
# Font validation and preview functionality
import bpy
import os
import platform
from PIL import Image, ImageDraw, ImageFont
def test_font_mixed_case_support(font_path):
"""
Test if a font properly supports mixed case rendering.
Returns True if the font supports mixed case, False if it only renders uppercase.
"""
try:
# Test string with mixed case
test_text = "AaBbCc"
# Create a test image to render the font
test_size = (200, 100)
test_image = Image.new('RGBA', test_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(test_image)
# Try to load the font
try:
font = ImageFont.truetype(font_path, 24)
except (OSError, IOError):
print(f"Text Texture Generator: Could not load font for testing: {font_path}")
return False
# Render the test text
draw.text((10, 10), test_text, font=font, fill=(255, 255, 255, 255))
# Get the rendered pixels
pixels = list(test_image.getdata())
# Create another image with uppercase version
test_image_upper = Image.new('RGBA', test_size, (0, 0, 0, 0))
draw_upper = ImageDraw.Draw(test_image_upper)
draw_upper.text((10, 10), test_text.upper(), font=font, fill=(255, 255, 255, 255))
pixels_upper = list(test_image_upper.getdata())
# Compare the two renderings - if they're identical, the font only supports uppercase
if pixels == pixels_upper:
print(f"Text Texture Generator: Font only supports uppercase rendering: {os.path.basename(font_path)}")
return False
else:
print(f"Text Texture Generator: Font supports mixed case rendering: {os.path.basename(font_path)}")
return True
except Exception as e:
print(f"Text Texture Generator: Error testing font mixed case support: {str(e)}")
return False
def get_validated_font(font_path, fallback_fonts=None):
"""
Get a validated font that supports mixed case rendering.
Returns the original font path if valid, otherwise returns a fallback font.
"""
if not font_path or not os.path.exists(font_path):
print(f"Text Texture Generator: Font file not found: {font_path}")
return get_system_fallback_fonts()[0] if get_system_fallback_fonts() else None
# Test if the font supports mixed case
if test_font_mixed_case_support(font_path):
return font_path
else:
print(f"Text Texture Generator: Font does not support mixed case, using fallback: {font_path}")
# Try fallback fonts if provided
if fallback_fonts:
for fallback_font in fallback_fonts:
if os.path.exists(fallback_font) and test_font_mixed_case_support(fallback_font):
print(f"Text Texture Generator: Using fallback font: {fallback_font}")
return fallback_font
# Use system fallback fonts
system_fallbacks = get_system_fallback_fonts()
for fallback_font in system_fallbacks:
if test_font_mixed_case_support(fallback_font):
print(f"Text Texture Generator: Using system fallback font: {fallback_font}")
return fallback_font
# If no valid font found, return the original (better than nothing)
print("Text Texture Generator: Warning - No valid mixed case font found, using original")
return font_path
def get_system_fallback_fonts():
"""
Get a list of system fallback fonts based on the operating system.
Returns a list of font paths that are likely to be available.
"""
system = platform.system()
fallback_fonts = []
if system == "Windows":
windows_fonts = [
"C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/calibri.ttf",
"C:/Windows/Fonts/verdana.ttf",
"C:/Windows/Fonts/tahoma.ttf",
"C:/Windows/Fonts/segoeui.ttf",
"C:/Windows/Fonts/times.ttf"
]
fallback_fonts.extend([f for f in windows_fonts if os.path.exists(f)])
elif system == "Darwin": # macOS
macos_fonts = [
"/System/Library/Fonts/Helvetica.ttc",
"/System/Library/Fonts/Arial.ttf",
"/System/Library/Fonts/Times.ttc",
"/System/Library/Fonts/Verdana.ttf",
"/Library/Fonts/Arial.ttf",
"/System/Library/Fonts/Courier.dfont"
]
fallback_fonts.extend([f for f in macos_fonts if os.path.exists(f)])
elif system == "Linux":
linux_fonts = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/TTF/arial.ttf",
"/usr/share/fonts/truetype/ttf-bitstream-vera/Vera.ttf"
]
fallback_fonts.extend([f for f in linux_fonts if os.path.exists(f)])
# Also check common font directories
common_directories = [
"/usr/share/fonts",
"/usr/local/share/fonts",
os.path.expanduser("~/.fonts"),
os.path.expanduser("~/Library/Fonts")
]
for font_dir in common_directories:
if os.path.exists(font_dir):
for root, dirs, files in os.walk(font_dir):
for file in files:
if file.lower().endswith(('.ttf', '.otf')):
font_path = os.path.join(root, file)
if font_path not in fallback_fonts:
fallback_fonts.append(font_path)
# Limit the number of fallback fonts to prevent excessive scanning
if len(fallback_fonts) >= 20:
break
if len(fallback_fonts) >= 20:
break
print(f"Text Texture Generator: Found {len(fallback_fonts)} system fallback fonts")
for i, font in enumerate(fallback_fonts[:5]): # Log first 5 fonts
print(f"Text Texture Generator: Fallback font {i+1}: {font}")
return fallback_fonts