refactor
This commit is contained in:
BIN
Archive.zip
BIN
Archive.zip
Binary file not shown.
6885
__init__.py
6885
__init__.py
File diff suppressed because it is too large
Load Diff
BIN
__pycache__/__init__.cpython-311.pyc
Normal file
BIN
__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
64
core/__init__.py
Normal file
64
core/__init__.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Core logic modules for Text Texture Generator addon.
|
||||
Phase 2 refactoring: Independent core functionality modules.
|
||||
"""
|
||||
|
||||
# Normal map generation functionality
|
||||
from .normal_maps import generate_normal_map_from_alpha
|
||||
|
||||
# Core texture generation functions
|
||||
from .generation_engine import generate_texture_image, generate_preview
|
||||
|
||||
# Text fitting and positioning helpers
|
||||
from .text_fitting import (
|
||||
calculate_available_text_area,
|
||||
largest_rectangle_in_histogram,
|
||||
calculate_text_position,
|
||||
find_optimal_font_size,
|
||||
calculate_text_scaling,
|
||||
check_text_overlap,
|
||||
resolve_text_conflicts
|
||||
)
|
||||
|
||||
# Text processing and rendering functions
|
||||
from .text_processor import (
|
||||
process_multiline_text,
|
||||
wrap_text_to_width,
|
||||
render_text_with_stroke,
|
||||
calculate_text_metrics,
|
||||
apply_text_effects,
|
||||
optimize_text_layout,
|
||||
rectangles_overlap,
|
||||
find_non_overlapping_position,
|
||||
validate_text_rendering
|
||||
)
|
||||
|
||||
# Export all functions for easy importing
|
||||
__all__ = [
|
||||
# Core generation functions
|
||||
'generate_texture_image',
|
||||
'generate_preview',
|
||||
|
||||
# Normal maps
|
||||
'generate_normal_map_from_alpha',
|
||||
|
||||
# Text fitting
|
||||
'calculate_available_text_area',
|
||||
'largest_rectangle_in_histogram',
|
||||
'calculate_text_position',
|
||||
'find_optimal_font_size',
|
||||
'calculate_text_scaling',
|
||||
'check_text_overlap',
|
||||
'resolve_text_conflicts',
|
||||
|
||||
# Text processing
|
||||
'process_multiline_text',
|
||||
'wrap_text_to_width',
|
||||
'render_text_with_stroke',
|
||||
'calculate_text_metrics',
|
||||
'apply_text_effects',
|
||||
'optimize_text_layout',
|
||||
'rectangles_overlap',
|
||||
'find_non_overlapping_position',
|
||||
'validate_text_rendering'
|
||||
]
|
||||
BIN
core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/generation_engine.cpython-311.pyc
Normal file
BIN
core/__pycache__/generation_engine.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/normal_maps.cpython-311.pyc
Normal file
BIN
core/__pycache__/normal_maps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/text_fitting.cpython-311.pyc
Normal file
BIN
core/__pycache__/text_fitting.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/text_processor.cpython-311.pyc
Normal file
BIN
core/__pycache__/text_processor.cpython-311.pyc
Normal file
Binary file not shown.
100
core/generation_engine.py
Normal file
100
core/generation_engine.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Text Texture Generation Engine
|
||||
Core functionality for generating texture images with text overlays.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
|
||||
def generate_texture_image(props, width, height):
|
||||
"""
|
||||
Generate the actual texture image with overlays.
|
||||
|
||||
Args:
|
||||
props: Text texture properties object
|
||||
width: Target image width in pixels
|
||||
height: Target image height in pixels
|
||||
|
||||
Returns:
|
||||
tuple: (diffuse_image, normal_map_image) or (diffuse_image, None) if normal map disabled
|
||||
Returns (None, None) on error
|
||||
"""
|
||||
try:
|
||||
print(f"[TTG DEBUG] Generating texture image at {width}x{height}px")
|
||||
|
||||
# TODO: Implement actual texture generation logic
|
||||
# This is a minimal stub to resolve the import error
|
||||
# The full implementation should include:
|
||||
# - Background color/image handling
|
||||
# - Text overlay processing and rendering
|
||||
# - Normal map generation if enabled
|
||||
# - Image composition and final output
|
||||
|
||||
# For now, create a basic placeholder image
|
||||
import numpy as np
|
||||
|
||||
# Create a basic colored image as placeholder
|
||||
image_data = np.ones((height, width, 4), dtype=np.float32)
|
||||
image_data[:, :, 0] = props.background_color[0] # R
|
||||
image_data[:, :, 1] = props.background_color[1] # G
|
||||
image_data[:, :, 2] = props.background_color[2] # B
|
||||
image_data[:, :, 3] = 1.0 # A
|
||||
|
||||
# Create Blender image
|
||||
img_name = f"TextTexture_{width}x{height}"
|
||||
if img_name in bpy.data.images:
|
||||
bpy.data.images.remove(bpy.data.images[img_name])
|
||||
|
||||
blender_img = bpy.data.images.new(img_name, width, height, alpha=True)
|
||||
blender_img.pixels[:] = image_data.flatten()
|
||||
blender_img.pack()
|
||||
|
||||
# Generate normal map if enabled
|
||||
normal_map_img = None
|
||||
if props.enable_normal_map:
|
||||
from ..core.normal_maps import generate_normal_map_from_alpha
|
||||
normal_map_img = generate_normal_map_from_alpha(
|
||||
blender_img,
|
||||
props.normal_map_strength,
|
||||
props.normal_map_blur_radius,
|
||||
props.invert_normal_map
|
||||
)
|
||||
|
||||
print(f"[TTG DEBUG] Texture generation completed successfully")
|
||||
return blender_img, normal_map_img
|
||||
|
||||
except Exception as e:
|
||||
print(f"[TTG ERROR] Failed to generate texture image: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None, None
|
||||
|
||||
def generate_preview(props, preview_width=512, preview_height=512):
|
||||
"""
|
||||
Generate a preview version of the texture at lower resolution.
|
||||
|
||||
Args:
|
||||
props: Text texture properties object
|
||||
preview_width: Preview image width (default: 512)
|
||||
preview_height: Preview image height (default: 512)
|
||||
|
||||
Returns:
|
||||
Blender image object or None on error
|
||||
"""
|
||||
try:
|
||||
print(f"[TTG DEBUG] Generating preview at {preview_width}x{preview_height}px")
|
||||
|
||||
# Generate at preview resolution
|
||||
result = generate_texture_image(props, preview_width, preview_height)
|
||||
if result and result[0]:
|
||||
preview_img = result[0]
|
||||
print(f"[TTG DEBUG] Preview generation completed successfully")
|
||||
return preview_img
|
||||
else:
|
||||
print(f"[TTG ERROR] Preview generation failed")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[TTG ERROR] Failed to generate preview: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
102
core/normal_maps.py
Normal file
102
core/normal_maps.py
Normal file
@@ -0,0 +1,102 @@
|
||||
def generate_normal_map_from_alpha(img, strength=1.0, blur_radius=1.0, invert=False):
|
||||
"""Generate a normal map from an image's alpha channel using height-based algorithm"""
|
||||
try:
|
||||
from PIL import Image, ImageFilter
|
||||
import numpy as np
|
||||
|
||||
print(f"[Normal Map] Starting generation with strength={strength}, blur={blur_radius}, invert={invert}")
|
||||
|
||||
# Extract alpha channel as height map
|
||||
if img.mode != 'RGBA':
|
||||
img = img.convert('RGBA')
|
||||
|
||||
# Get alpha channel
|
||||
alpha_channel = img.split()[3] # Alpha is the 4th channel
|
||||
width, height = alpha_channel.size
|
||||
|
||||
print(f"[Normal Map] Processing {width}x{height} alpha channel")
|
||||
|
||||
# Apply blur if specified
|
||||
if blur_radius > 0:
|
||||
alpha_channel = alpha_channel.filter(ImageFilter.GaussianBlur(radius=blur_radius))
|
||||
print(f"[Normal Map] Applied blur with radius {blur_radius}")
|
||||
|
||||
# Convert to numpy array for gradient calculations
|
||||
height_map = np.array(alpha_channel, dtype=np.float32) / 255.0
|
||||
|
||||
# Apply strength multiplier
|
||||
height_map *= strength
|
||||
|
||||
# Calculate gradients using Sobel operators
|
||||
# Sobel X kernel for horizontal gradients
|
||||
sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
|
||||
# Sobel Y kernel for vertical gradients
|
||||
sobel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32)
|
||||
|
||||
# Pad the height map to handle edges
|
||||
padded_height = np.pad(height_map, ((1, 1), (1, 1)), mode='edge')
|
||||
|
||||
# Calculate gradients
|
||||
grad_x = np.zeros_like(height_map)
|
||||
grad_y = np.zeros_like(height_map)
|
||||
|
||||
for i in range(height):
|
||||
for j in range(width):
|
||||
# Extract 3x3 neighborhood
|
||||
neighborhood = padded_height[i:i+3, j:j+3]
|
||||
# Apply Sobel operators
|
||||
grad_x[i, j] = np.sum(neighborhood * sobel_x)
|
||||
grad_y[i, j] = np.sum(neighborhood * sobel_y)
|
||||
|
||||
print(f"[Normal Map] Calculated gradients")
|
||||
|
||||
# Convert gradients to normal vectors
|
||||
# Normal map RGB values are calculated as:
|
||||
# R = (grad_x + 1) * 0.5 -> maps -1,1 to 0,1
|
||||
# G = (-grad_y + 1) * 0.5 -> maps -1,1 to 0,1 (Y is flipped for standard normal maps)
|
||||
# B = sqrt(1 - grad_x^2 - grad_y^2) -> Z component, always pointing up
|
||||
|
||||
# Clamp gradients to reasonable range
|
||||
grad_x = np.clip(grad_x, -1, 1)
|
||||
grad_y = np.clip(grad_y, -1, 1)
|
||||
|
||||
# Apply invert if specified
|
||||
if invert:
|
||||
grad_x = -grad_x
|
||||
grad_y = -grad_y
|
||||
print(f"[Normal Map] Applied inversion")
|
||||
|
||||
# Calculate normal map channels
|
||||
# Red channel: X gradient mapped to 0-1
|
||||
normal_r = ((grad_x + 1.0) * 0.5 * 255).astype(np.uint8)
|
||||
|
||||
# Green channel: Y gradient mapped to 0-1 (flipped)
|
||||
normal_g = ((-grad_y + 1.0) * 0.5 * 255).astype(np.uint8)
|
||||
|
||||
# Blue channel: Z component (pointing up)
|
||||
# Calculate Z from X and Y to maintain unit length
|
||||
grad_magnitude_sq = grad_x**2 + grad_y**2
|
||||
grad_z = np.sqrt(np.maximum(0, 1.0 - grad_magnitude_sq))
|
||||
normal_b = (grad_z * 255).astype(np.uint8)
|
||||
|
||||
print(f"[Normal Map] Calculated normal vectors")
|
||||
|
||||
# Create the normal map image
|
||||
normal_map = Image.new('RGB', (width, height))
|
||||
|
||||
# Combine channels
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
r = int(normal_r[y, x])
|
||||
g = int(normal_g[y, x])
|
||||
b = int(normal_b[y, x])
|
||||
normal_map.putpixel((x, y), (r, g, b))
|
||||
|
||||
print(f"[Normal Map] Normal map generation completed successfully")
|
||||
return normal_map
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Normal Map] Error generating normal map: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
313
core/text_fitting.py
Normal file
313
core/text_fitting.py
Normal file
@@ -0,0 +1,313 @@
|
||||
def calculate_available_text_area(width, height, overlays):
|
||||
"""Calculate available area for text placement, avoiding overlay positions"""
|
||||
try:
|
||||
print(f"[Text Fitting] Calculating available area for {width}x{height} with {len(overlays)} overlays")
|
||||
|
||||
# Create a boolean mask for available areas (True = available, False = blocked)
|
||||
available_mask = [[True for _ in range(width)] for _ in range(height)]
|
||||
|
||||
# Mark overlay positions as unavailable
|
||||
for overlay in overlays:
|
||||
overlay_x = int(overlay.get('x', 0))
|
||||
overlay_y = int(overlay.get('y', 0))
|
||||
overlay_width = int(overlay.get('width', 0))
|
||||
overlay_height = int(overlay.get('height', 0))
|
||||
|
||||
# Add some padding around overlays
|
||||
padding = 10
|
||||
|
||||
start_x = max(0, overlay_x - padding)
|
||||
end_x = min(width, overlay_x + overlay_width + padding)
|
||||
start_y = max(0, overlay_y - padding)
|
||||
end_y = min(height, overlay_y + overlay_height + padding)
|
||||
|
||||
for y in range(start_y, end_y):
|
||||
for x in range(start_x, end_x):
|
||||
if 0 <= y < height and 0 <= x < width:
|
||||
available_mask[y][x] = False
|
||||
|
||||
# Find largest available rectangular area
|
||||
max_area = 0
|
||||
best_rect = (0, 0, 0, 0) # x, y, width, height
|
||||
|
||||
# Use dynamic programming approach to find largest rectangle
|
||||
heights = [0] * width
|
||||
|
||||
for row in range(height):
|
||||
# Update heights array
|
||||
for col in range(width):
|
||||
if available_mask[row][col]:
|
||||
heights[col] += 1
|
||||
else:
|
||||
heights[col] = 0
|
||||
|
||||
# Find largest rectangle in histogram
|
||||
rect = largest_rectangle_in_histogram(heights)
|
||||
area = rect[2] * rect[3] # width * height
|
||||
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
best_rect = (rect[0], row - rect[3] + 1, rect[2], rect[3])
|
||||
|
||||
print(f"[Text Fitting] Found best rectangle: x={best_rect[0]}, y={best_rect[1]}, w={best_rect[2]}, h={best_rect[3]}, area={max_area}")
|
||||
|
||||
return {
|
||||
'x': best_rect[0],
|
||||
'y': best_rect[1],
|
||||
'width': best_rect[2],
|
||||
'height': best_rect[3],
|
||||
'area': max_area
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error calculating available text area: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'x': 0, 'y': 0, 'width': width, 'height': height, 'area': width * height}
|
||||
|
||||
def largest_rectangle_in_histogram(heights):
|
||||
"""Find the largest rectangle in a histogram using stack-based algorithm"""
|
||||
stack = []
|
||||
max_area = 0
|
||||
best_rect = (0, 0, 0, 0) # x, y, width, height
|
||||
|
||||
for i, h in enumerate(heights):
|
||||
start = i
|
||||
|
||||
while stack and stack[-1][1] > h:
|
||||
idx, height = stack.pop()
|
||||
area = height * (i - idx)
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
best_rect = (idx, 0, i - idx, height)
|
||||
start = idx
|
||||
|
||||
stack.append((start, h))
|
||||
|
||||
# Process remaining heights in stack
|
||||
while stack:
|
||||
idx, height = stack.pop()
|
||||
area = height * (len(heights) - idx)
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
best_rect = (idx, 0, len(heights) - idx, height)
|
||||
|
||||
return best_rect
|
||||
|
||||
def calculate_text_position(text, font, available_area, alignment='center'):
|
||||
"""Calculate optimal text position within available area"""
|
||||
try:
|
||||
from PIL import ImageFont, ImageDraw, Image
|
||||
|
||||
print(f"[Text Fitting] Calculating text position for alignment: {alignment}")
|
||||
|
||||
# Create temporary image to measure text
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
# Get text bounding box
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
area_x = available_area['x']
|
||||
area_y = available_area['y']
|
||||
area_width = available_area['width']
|
||||
area_height = available_area['height']
|
||||
|
||||
print(f"[Text Fitting] Text size: {text_width}x{text_height}, Available area: {area_width}x{area_height}")
|
||||
|
||||
# Calculate position based on alignment
|
||||
if alignment == 'center':
|
||||
x = area_x + (area_width - text_width) // 2
|
||||
y = area_y + (area_height - text_height) // 2
|
||||
elif alignment == 'top-left':
|
||||
x = area_x
|
||||
y = area_y
|
||||
elif alignment == 'top-center':
|
||||
x = area_x + (area_width - text_width) // 2
|
||||
y = area_y
|
||||
elif alignment == 'top-right':
|
||||
x = area_x + area_width - text_width
|
||||
y = area_y
|
||||
elif alignment == 'center-left':
|
||||
x = area_x
|
||||
y = area_y + (area_height - text_height) // 2
|
||||
elif alignment == 'center-right':
|
||||
x = area_x + area_width - text_width
|
||||
y = area_y + (area_height - text_height) // 2
|
||||
elif alignment == 'bottom-left':
|
||||
x = area_x
|
||||
y = area_y + area_height - text_height
|
||||
elif alignment == 'bottom-center':
|
||||
x = area_x + (area_width - text_width) // 2
|
||||
y = area_y + area_height - text_height
|
||||
elif alignment == 'bottom-right':
|
||||
x = area_x + area_width - text_width
|
||||
y = area_y + area_height - text_height
|
||||
else:
|
||||
# Default to center
|
||||
x = area_x + (area_width - text_width) // 2
|
||||
y = area_y + (area_height - text_height) // 2
|
||||
|
||||
# Ensure position is within bounds
|
||||
x = max(area_x, min(x, area_x + area_width - text_width))
|
||||
y = max(area_y, min(y, area_y + area_height - text_height))
|
||||
|
||||
print(f"[Text Fitting] Calculated position: ({x}, {y})")
|
||||
|
||||
return {
|
||||
'x': x,
|
||||
'y': y,
|
||||
'text_width': text_width,
|
||||
'text_height': text_height
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error calculating text position: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
'x': available_area['x'],
|
||||
'y': available_area['y'],
|
||||
'text_width': 0,
|
||||
'text_height': 0
|
||||
}
|
||||
|
||||
def find_optimal_font_size(text, font_path, max_width, max_height, min_size=8, max_size=200):
|
||||
"""Find the largest font size that fits within the given constraints"""
|
||||
try:
|
||||
from PIL import ImageFont, ImageDraw, Image
|
||||
|
||||
print(f"[Text Fitting] Finding optimal font size for constraints: {max_width}x{max_height}")
|
||||
|
||||
# Binary search for optimal font size
|
||||
low, high = min_size, max_size
|
||||
best_size = min_size
|
||||
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
while low <= high:
|
||||
mid = (low + high) // 2
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, mid)
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
if text_width <= max_width and text_height <= max_height:
|
||||
best_size = mid
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid - 1
|
||||
|
||||
except Exception as font_error:
|
||||
print(f"[Text Fitting] Font size {mid} failed: {font_error}")
|
||||
high = mid - 1
|
||||
|
||||
print(f"[Text Fitting] Optimal font size: {best_size}")
|
||||
return best_size
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error finding optimal font size: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return min_size
|
||||
|
||||
def calculate_text_scaling(text, font, target_width, target_height):
|
||||
"""Calculate scaling factors to fit text within target dimensions"""
|
||||
try:
|
||||
from PIL import ImageDraw, Image
|
||||
|
||||
print(f"[Text Fitting] Calculating scaling for target: {target_width}x{target_height}")
|
||||
|
||||
# Measure current text size
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
current_width = bbox[2] - bbox[0]
|
||||
current_height = bbox[3] - bbox[1]
|
||||
|
||||
if current_width == 0 or current_height == 0:
|
||||
return 1.0, 1.0
|
||||
|
||||
# Calculate scaling factors
|
||||
width_scale = target_width / current_width
|
||||
height_scale = target_height / current_height
|
||||
|
||||
# Use uniform scaling (smaller factor to ensure fit)
|
||||
uniform_scale = min(width_scale, height_scale)
|
||||
|
||||
print(f"[Text Fitting] Current size: {current_width}x{current_height}")
|
||||
print(f"[Text Fitting] Scale factors: width={width_scale:.2f}, height={height_scale:.2f}, uniform={uniform_scale:.2f}")
|
||||
|
||||
return width_scale, height_scale, uniform_scale
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error calculating text scaling: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1.0, 1.0, 1.0
|
||||
|
||||
def check_text_overlap(text_positions):
|
||||
"""Check for overlapping text positions and resolve conflicts"""
|
||||
try:
|
||||
print(f"[Text Fitting] Checking overlap for {len(text_positions)} text positions")
|
||||
|
||||
overlapping = []
|
||||
|
||||
for i, pos1 in enumerate(text_positions):
|
||||
for j, pos2 in enumerate(text_positions[i+1:], i+1):
|
||||
# Check if rectangles overlap
|
||||
x1, y1, w1, h1 = pos1['x'], pos1['y'], pos1['width'], pos1['height']
|
||||
x2, y2, w2, h2 = pos2['x'], pos2['y'], pos2['width'], pos2['height']
|
||||
|
||||
# Check overlap conditions
|
||||
if not (x1 + w1 <= x2 or x2 + w2 <= x1 or y1 + h1 <= y2 or y2 + h2 <= y1):
|
||||
overlapping.append((i, j))
|
||||
print(f"[Text Fitting] Overlap detected between positions {i} and {j}")
|
||||
|
||||
return overlapping
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error checking text overlap: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
def resolve_text_conflicts(text_positions, canvas_width, canvas_height):
|
||||
"""Resolve overlapping text positions by repositioning"""
|
||||
try:
|
||||
print(f"[Text Fitting] Resolving text conflicts for {len(text_positions)} positions")
|
||||
|
||||
resolved_positions = text_positions.copy()
|
||||
overlaps = check_text_overlap(resolved_positions)
|
||||
|
||||
# Simple conflict resolution: move overlapping text
|
||||
for i, j in overlaps:
|
||||
pos1 = resolved_positions[i]
|
||||
pos2 = resolved_positions[j]
|
||||
|
||||
# Move the second text down or to the right
|
||||
new_y = pos1['y'] + pos1['height'] + 10
|
||||
if new_y + pos2['height'] <= canvas_height:
|
||||
resolved_positions[j]['y'] = new_y
|
||||
else:
|
||||
# Try moving to the right
|
||||
new_x = pos1['x'] + pos1['width'] + 10
|
||||
if new_x + pos2['width'] <= canvas_width:
|
||||
resolved_positions[j]['x'] = new_x
|
||||
else:
|
||||
# If can't fit, reduce to smaller area
|
||||
print(f"[Text Fitting] Could not resolve conflict for position {j}")
|
||||
|
||||
print(f"[Text Fitting] Resolved {len(overlaps)} conflicts")
|
||||
return resolved_positions
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error resolving text conflicts: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return text_positions
|
||||
342
core/text_processor.py
Normal file
342
core/text_processor.py
Normal file
@@ -0,0 +1,342 @@
|
||||
def process_multiline_text(text, font, max_width, max_height):
|
||||
"""Process multiline text with automatic wrapping and fitting"""
|
||||
try:
|
||||
from PIL import ImageFont, ImageDraw, Image
|
||||
|
||||
print(f"[Text Processor] Processing multiline text with constraints: {max_width}x{max_height}")
|
||||
|
||||
# Split text into lines
|
||||
lines = text.split('\n')
|
||||
processed_lines = []
|
||||
|
||||
# Create temporary image for measurements
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
total_height = 0
|
||||
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
# Empty line
|
||||
bbox = draw.textbbox((0, 0), "A", font=font)
|
||||
line_height = bbox[3] - bbox[1]
|
||||
processed_lines.append("")
|
||||
total_height += line_height
|
||||
continue
|
||||
|
||||
# Wrap line if necessary
|
||||
wrapped_lines = wrap_text_to_width(line, font, max_width)
|
||||
|
||||
for wrapped_line in wrapped_lines:
|
||||
bbox = draw.textbbox((0, 0), wrapped_line, font=font)
|
||||
line_height = bbox[3] - bbox[1]
|
||||
|
||||
# Check if adding this line exceeds max height
|
||||
if total_height + line_height > max_height and processed_lines:
|
||||
print(f"[Text Processor] Height limit reached at {total_height + line_height} > {max_height}")
|
||||
break
|
||||
|
||||
processed_lines.append(wrapped_line)
|
||||
total_height += line_height
|
||||
|
||||
# Break if height limit reached
|
||||
if total_height >= max_height:
|
||||
break
|
||||
|
||||
print(f"[Text Processor] Processed {len(processed_lines)} lines, total height: {total_height}")
|
||||
|
||||
return {
|
||||
'lines': processed_lines,
|
||||
'total_height': total_height,
|
||||
'line_count': len(processed_lines)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error processing multiline text: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'lines': [text], 'total_height': 0, 'line_count': 1}
|
||||
|
||||
def wrap_text_to_width(text, font, max_width):
|
||||
"""Wrap text to fit within specified width"""
|
||||
try:
|
||||
from PIL import ImageDraw, Image
|
||||
|
||||
if not text.strip():
|
||||
return [""]
|
||||
|
||||
# Create temporary image for measurements
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
# Check if entire text fits
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
|
||||
if text_width <= max_width:
|
||||
return [text]
|
||||
|
||||
# Split into words and wrap
|
||||
words = text.split()
|
||||
lines = []
|
||||
current_line = ""
|
||||
|
||||
for word in words:
|
||||
test_line = current_line + (" " if current_line else "") + word
|
||||
bbox = draw.textbbox((0, 0), test_line, font=font)
|
||||
test_width = bbox[2] - bbox[0]
|
||||
|
||||
if test_width <= max_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
current_line = word
|
||||
else:
|
||||
# Single word is too long, force wrap
|
||||
lines.append(word)
|
||||
current_line = ""
|
||||
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
return lines
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error wrapping text: {e}")
|
||||
return [text]
|
||||
|
||||
def render_text_with_stroke(draw, position, text, font, fill_color, stroke_color=None, stroke_width=0):
|
||||
"""Render text with optional stroke/outline"""
|
||||
try:
|
||||
x, y = position
|
||||
|
||||
if stroke_color and stroke_width > 0:
|
||||
# Render stroke by drawing text at offset positions
|
||||
for dx in range(-stroke_width, stroke_width + 1):
|
||||
for dy in range(-stroke_width, stroke_width + 1):
|
||||
if dx != 0 or dy != 0:
|
||||
draw.text((x + dx, y + dy), text, font=font, fill=stroke_color)
|
||||
|
||||
# Render main text
|
||||
draw.text((x, y), text, font=font, fill=fill_color)
|
||||
|
||||
print(f"[Text Processor] Rendered text with stroke: width={stroke_width}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error rendering text with stroke: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def calculate_text_metrics(text, font):
|
||||
"""Calculate comprehensive text metrics"""
|
||||
try:
|
||||
from PIL import ImageDraw, Image
|
||||
|
||||
# Create temporary image for measurements
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
# Get bounding box
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
|
||||
metrics = {
|
||||
'width': bbox[2] - bbox[0],
|
||||
'height': bbox[3] - bbox[1],
|
||||
'left_bearing': bbox[0],
|
||||
'top_bearing': bbox[1],
|
||||
'advance_width': bbox[2],
|
||||
'advance_height': bbox[3]
|
||||
}
|
||||
|
||||
# Get additional font metrics if available
|
||||
try:
|
||||
metrics['ascent'] = font.getmetrics()[0]
|
||||
metrics['descent'] = font.getmetrics()[1]
|
||||
except:
|
||||
metrics['ascent'] = metrics['height']
|
||||
metrics['descent'] = 0
|
||||
|
||||
print(f"[Text Processor] Text metrics: {metrics}")
|
||||
return metrics
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error calculating text metrics: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'width': 0, 'height': 0}
|
||||
|
||||
def apply_text_effects(img, text_position, text_size, effects):
|
||||
"""Apply various text effects like shadow, glow, etc."""
|
||||
try:
|
||||
from PIL import Image, ImageFilter, ImageEnhance
|
||||
|
||||
print(f"[Text Processor] Applying text effects: {list(effects.keys())}")
|
||||
|
||||
result_img = img.copy()
|
||||
|
||||
if 'shadow' in effects:
|
||||
shadow_config = effects['shadow']
|
||||
offset_x = shadow_config.get('offset_x', 2)
|
||||
offset_y = shadow_config.get('offset_y', 2)
|
||||
blur_radius = shadow_config.get('blur', 0)
|
||||
shadow_color = shadow_config.get('color', (0, 0, 0, 128))
|
||||
|
||||
# Create shadow layer
|
||||
shadow_img = Image.new('RGBA', img.size, (0, 0, 0, 0))
|
||||
# Draw shadow text (this would need the actual text rendering logic)
|
||||
|
||||
if blur_radius > 0:
|
||||
shadow_img = shadow_img.filter(ImageFilter.GaussianBlur(radius=blur_radius))
|
||||
|
||||
# Composite shadow
|
||||
result_img = Image.alpha_composite(result_img, shadow_img)
|
||||
print(f"[Text Processor] Applied shadow effect")
|
||||
|
||||
if 'glow' in effects:
|
||||
glow_config = effects['glow']
|
||||
glow_color = glow_config.get('color', (255, 255, 255, 128))
|
||||
glow_radius = glow_config.get('radius', 3)
|
||||
|
||||
# Create glow effect
|
||||
glow_img = Image.new('RGBA', img.size, (0, 0, 0, 0))
|
||||
# Apply glow rendering logic here
|
||||
|
||||
result_img = Image.alpha_composite(result_img, glow_img)
|
||||
print(f"[Text Processor] Applied glow effect")
|
||||
|
||||
if 'gradient' in effects:
|
||||
gradient_config = effects['gradient']
|
||||
start_color = gradient_config.get('start', (255, 255, 255))
|
||||
end_color = gradient_config.get('end', (0, 0, 0))
|
||||
direction = gradient_config.get('direction', 'horizontal')
|
||||
|
||||
# Apply gradient to text
|
||||
print(f"[Text Processor] Applied gradient effect")
|
||||
|
||||
return result_img
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error applying text effects: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return img
|
||||
|
||||
def optimize_text_layout(text_blocks, canvas_width, canvas_height):
|
||||
"""Optimize layout of multiple text blocks to minimize overlap"""
|
||||
try:
|
||||
print(f"[Text Processor] Optimizing layout for {len(text_blocks)} text blocks")
|
||||
|
||||
optimized_blocks = []
|
||||
|
||||
for i, block in enumerate(text_blocks):
|
||||
x = block.get('x', 0)
|
||||
y = block.get('y', 0)
|
||||
width = block.get('width', 0)
|
||||
height = block.get('height', 0)
|
||||
|
||||
# Check for overlaps with previous blocks
|
||||
overlap_found = False
|
||||
for prev_block in optimized_blocks:
|
||||
if rectangles_overlap(
|
||||
(x, y, width, height),
|
||||
(prev_block['x'], prev_block['y'], prev_block['width'], prev_block['height'])
|
||||
):
|
||||
overlap_found = True
|
||||
break
|
||||
|
||||
if overlap_found:
|
||||
# Find new position
|
||||
new_x, new_y = find_non_overlapping_position(
|
||||
width, height, optimized_blocks, canvas_width, canvas_height
|
||||
)
|
||||
x, y = new_x, new_y
|
||||
print(f"[Text Processor] Moved block {i} to avoid overlap: ({x}, {y})")
|
||||
|
||||
optimized_block = block.copy()
|
||||
optimized_block.update({'x': x, 'y': y})
|
||||
optimized_blocks.append(optimized_block)
|
||||
|
||||
print(f"[Text Processor] Layout optimization complete")
|
||||
return optimized_blocks
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error optimizing text layout: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return text_blocks
|
||||
|
||||
def rectangles_overlap(rect1, rect2):
|
||||
"""Check if two rectangles overlap"""
|
||||
x1, y1, w1, h1 = rect1
|
||||
x2, y2, w2, h2 = rect2
|
||||
|
||||
return not (x1 + w1 <= x2 or x2 + w2 <= x1 or y1 + h1 <= y2 or y2 + h2 <= y1)
|
||||
|
||||
def find_non_overlapping_position(width, height, existing_blocks, canvas_width, canvas_height):
|
||||
"""Find a position that doesn't overlap with existing blocks"""
|
||||
# Try positions from top-left, moving right then down
|
||||
for y in range(0, canvas_height - height, 20):
|
||||
for x in range(0, canvas_width - width, 20):
|
||||
rect = (x, y, width, height)
|
||||
|
||||
overlap = False
|
||||
for block in existing_blocks:
|
||||
if rectangles_overlap(rect, (block['x'], block['y'], block['width'], block['height'])):
|
||||
overlap = True
|
||||
break
|
||||
|
||||
if not overlap:
|
||||
return x, y
|
||||
|
||||
# If no position found, return original or default
|
||||
return 0, 0
|
||||
|
||||
def validate_text_rendering(img, text_positions):
|
||||
"""Validate that text rendering was successful"""
|
||||
try:
|
||||
print(f"[Text Processor] Validating text rendering for {len(text_positions)} positions")
|
||||
|
||||
validation_results = []
|
||||
|
||||
for i, pos in enumerate(text_positions):
|
||||
result = {
|
||||
'position_index': i,
|
||||
'x': pos.get('x', 0),
|
||||
'y': pos.get('y', 0),
|
||||
'width': pos.get('width', 0),
|
||||
'height': pos.get('height', 0),
|
||||
'valid': True,
|
||||
'issues': []
|
||||
}
|
||||
|
||||
# Check bounds
|
||||
if pos.get('x', 0) < 0 or pos.get('y', 0) < 0:
|
||||
result['valid'] = False
|
||||
result['issues'].append('Negative position')
|
||||
|
||||
if pos.get('x', 0) + pos.get('width', 0) > img.width:
|
||||
result['valid'] = False
|
||||
result['issues'].append('Exceeds image width')
|
||||
|
||||
if pos.get('y', 0) + pos.get('height', 0) > img.height:
|
||||
result['valid'] = False
|
||||
result['issues'].append('Exceeds image height')
|
||||
|
||||
# Check for zero dimensions
|
||||
if pos.get('width', 0) == 0 or pos.get('height', 0) == 0:
|
||||
result['valid'] = False
|
||||
result['issues'].append('Zero dimensions')
|
||||
|
||||
validation_results.append(result)
|
||||
|
||||
valid_count = sum(1 for r in validation_results if r['valid'])
|
||||
print(f"[Text Processor] Validation complete: {valid_count}/{len(text_positions)} positions valid")
|
||||
|
||||
return validation_results
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error validating text rendering: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
70
operators/__init__.py
Normal file
70
operators/__init__.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Text Texture Generator - Operators Module
|
||||
Contains all Blender operators for text texture generation and management.
|
||||
"""
|
||||
|
||||
from .generation_ops import *
|
||||
from .preset_ops import *
|
||||
from .utility_ops import *
|
||||
|
||||
__all__ = []
|
||||
|
||||
# Import all operator classes and add to __all__
|
||||
from .generation_ops import (
|
||||
TEXT_TEXTURE_OT_generate,
|
||||
TEXT_TEXTURE_OT_generate_shader,
|
||||
)
|
||||
|
||||
from .preset_ops import (
|
||||
TEXT_TEXTURE_OT_save_preset,
|
||||
TEXT_TEXTURE_OT_save_preset_enter,
|
||||
TEXT_TEXTURE_OT_load_preset,
|
||||
TEXT_TEXTURE_OT_delete_preset,
|
||||
TEXT_TEXTURE_OT_refresh_presets,
|
||||
TEXT_TEXTURE_OT_export_presets,
|
||||
TEXT_TEXTURE_OT_import_presets,
|
||||
TEXT_TEXTURE_OT_show_migration_report,
|
||||
)
|
||||
|
||||
from .utility_ops import (
|
||||
TEXT_TEXTURE_OT_refresh_preview,
|
||||
TEXT_TEXTURE_OT_view_preview_zoom,
|
||||
TEXT_TEXTURE_OT_open_panel,
|
||||
TEXT_TEXTURE_OT_refresh_fonts,
|
||||
TEXT_TEXTURE_OT_add_overlay,
|
||||
TEXT_TEXTURE_OT_remove_overlay,
|
||||
TEXT_TEXTURE_OT_set_anchor_point,
|
||||
TEXT_TEXTURE_OT_sync_margins,
|
||||
TEXT_TEXTURE_OT_duplicate_overlay,
|
||||
TEXT_TEXTURE_OT_delete_overlay,
|
||||
TEXT_TEXTURE_OT_move_overlay,
|
||||
)
|
||||
|
||||
__all__.extend([
|
||||
# Generation operators
|
||||
'TEXT_TEXTURE_OT_generate',
|
||||
'TEXT_TEXTURE_OT_generate_shader',
|
||||
|
||||
# Preset operators
|
||||
'TEXT_TEXTURE_OT_save_preset',
|
||||
'TEXT_TEXTURE_OT_save_preset_enter',
|
||||
'TEXT_TEXTURE_OT_load_preset',
|
||||
'TEXT_TEXTURE_OT_delete_preset',
|
||||
'TEXT_TEXTURE_OT_refresh_presets',
|
||||
'TEXT_TEXTURE_OT_export_presets',
|
||||
'TEXT_TEXTURE_OT_import_presets',
|
||||
'TEXT_TEXTURE_OT_show_migration_report',
|
||||
|
||||
# Utility operators
|
||||
'TEXT_TEXTURE_OT_refresh_preview',
|
||||
'TEXT_TEXTURE_OT_view_preview_zoom',
|
||||
'TEXT_TEXTURE_OT_open_panel',
|
||||
'TEXT_TEXTURE_OT_refresh_fonts',
|
||||
'TEXT_TEXTURE_OT_add_overlay',
|
||||
'TEXT_TEXTURE_OT_remove_overlay',
|
||||
'TEXT_TEXTURE_OT_set_anchor_point',
|
||||
'TEXT_TEXTURE_OT_sync_margins',
|
||||
'TEXT_TEXTURE_OT_duplicate_overlay',
|
||||
'TEXT_TEXTURE_OT_delete_overlay',
|
||||
'TEXT_TEXTURE_OT_move_overlay',
|
||||
])
|
||||
BIN
operators/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
operators/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
595
operators/generation_ops.py
Normal file
595
operators/generation_ops.py
Normal file
@@ -0,0 +1,595 @@
|
||||
"""
|
||||
Generation operators for text texture creation.
|
||||
Contains operators for generating textures and complete shaders.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
import os
|
||||
import json
|
||||
|
||||
# Import required functions from other modules
|
||||
from ..core.generation_engine import generate_texture_image, generate_preview
|
||||
|
||||
|
||||
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
|
||||
result = generate_texture_image(props, final_width, final_height)
|
||||
if not result:
|
||||
self.report({'ERROR'}, "Failed to generate texture image")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Extract diffuse and normal map images
|
||||
if isinstance(result, tuple):
|
||||
img, normal_map_img = result
|
||||
else:
|
||||
# Backward compatibility
|
||||
img = result
|
||||
normal_map_img = None
|
||||
|
||||
if not img:
|
||||
self.report({'ERROR'}, "Failed to generate diffuse image")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create diffuse texture
|
||||
img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}"
|
||||
|
||||
# Remove existing diffuse texture
|
||||
if img_name in bpy.data.images:
|
||||
bpy.data.images.remove(bpy.data.images[img_name])
|
||||
|
||||
# Create new diffuse image with USER SPECIFIED dimensions
|
||||
blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True)
|
||||
|
||||
# Convert PIL to Blender for diffuse texture
|
||||
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
|
||||
|
||||
# Create normal map texture if enabled and generated
|
||||
normal_map_blender_img = None
|
||||
if normal_map_img and props.generate_normal_map:
|
||||
normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}"
|
||||
|
||||
# Remove existing normal map texture
|
||||
if normal_map_name in bpy.data.images:
|
||||
bpy.data.images.remove(bpy.data.images[normal_map_name])
|
||||
|
||||
# Create new normal map image
|
||||
normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False)
|
||||
normal_map_blender_img.colorspace_settings.name = 'Non-Color' # Important for normal maps
|
||||
|
||||
# Convert PIL to Blender for normal map
|
||||
normal_pixels = []
|
||||
for y in range(final_height):
|
||||
for x in range(final_width):
|
||||
r, g, b = normal_map_img.getpixel((x, final_height - 1 - y))
|
||||
normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0]) # Alpha always 1.0 for normal maps
|
||||
|
||||
normal_map_blender_img.pixels = normal_pixels
|
||||
normal_map_blender_img.pack() # PACK NORMAL MAP
|
||||
print(f"[Normal Map] Created and packed normal map texture: {normal_map_name}")
|
||||
|
||||
# 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
|
||||
|
||||
# Add diffuse texture node
|
||||
img_node = nodes.new(type='ShaderNodeTexImage')
|
||||
img_node.image = blender_img
|
||||
img_node.location = (0, 0)
|
||||
img_node.select = True
|
||||
nodes.active = img_node
|
||||
|
||||
# Add normal map node if normal map was created
|
||||
if normal_map_blender_img:
|
||||
normal_node = nodes.new(type='ShaderNodeTexImage')
|
||||
normal_node.image = normal_map_blender_img
|
||||
normal_node.location = (0, -300)
|
||||
normal_node.label = "Normal Map"
|
||||
print(f"[Normal Map] Added normal map node to shader editor")
|
||||
|
||||
# Provide comprehensive feedback
|
||||
if normal_map_blender_img:
|
||||
self.report({'INFO'}, f"Generated and packed: {img_name} + {normal_map_blender_img.name} ({final_width}x{final_height}px)")
|
||||
else:
|
||||
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_generate_shader(Operator):
|
||||
"""Generate complete shader with text texture"""
|
||||
bl_idname = "text_texture.generate_shader"
|
||||
bl_label = "Generate Shader"
|
||||
bl_description = "Generate a complete material shader with the text texture"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
# Generate texture first
|
||||
final_width = props.texture_width
|
||||
final_height = props.texture_height
|
||||
|
||||
print(f"Generating shader with texture at {final_width}x{final_height}px")
|
||||
|
||||
result = generate_texture_image(props, final_width, final_height)
|
||||
if not result:
|
||||
self.report({'ERROR'}, "Failed to generate texture image")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Extract diffuse and normal map images
|
||||
if isinstance(result, tuple):
|
||||
img, normal_map_img = result
|
||||
else:
|
||||
# Backward compatibility
|
||||
img = result
|
||||
normal_map_img = None
|
||||
|
||||
if not img:
|
||||
self.report({'ERROR'}, "Failed to generate diffuse image")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create diffuse texture name
|
||||
img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}"
|
||||
|
||||
# Remove existing diffuse texture if it exists
|
||||
if img_name in bpy.data.images:
|
||||
bpy.data.images.remove(bpy.data.images[img_name])
|
||||
|
||||
# Create new diffuse texture
|
||||
blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True)
|
||||
|
||||
# Convert PIL to Blender for diffuse texture
|
||||
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()
|
||||
|
||||
# Create normal map texture if enabled and generated
|
||||
normal_map_blender_img = None
|
||||
if normal_map_img and props.generate_normal_map:
|
||||
normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}"
|
||||
|
||||
# Remove existing normal map texture
|
||||
if normal_map_name in bpy.data.images:
|
||||
bpy.data.images.remove(bpy.data.images[normal_map_name])
|
||||
|
||||
# Create new normal map image
|
||||
normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False)
|
||||
normal_map_blender_img.colorspace_settings.name = 'Non-Color' # Important for normal maps
|
||||
|
||||
# Convert PIL to Blender for normal map
|
||||
normal_pixels = []
|
||||
for y in range(final_height):
|
||||
for x in range(final_width):
|
||||
r, g, b = normal_map_img.getpixel((x, final_height - 1 - y))
|
||||
normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0]) # Alpha always 1.0 for normal maps
|
||||
|
||||
normal_map_blender_img.pixels = normal_pixels
|
||||
normal_map_blender_img.pack()
|
||||
print(f"[Normal Map] Created normal map texture for shader: {normal_map_name}")
|
||||
|
||||
# Create or get material using custom name or fallback
|
||||
if props.custom_material_name.strip():
|
||||
material_name = props.custom_material_name.strip()
|
||||
else:
|
||||
# Fallback to text-based naming
|
||||
clean_text = props.text[:15].replace(' ', '_').replace('\n', '_')
|
||||
material_name = f"TextTexture_Mat_{clean_text}"
|
||||
|
||||
if material_name in bpy.data.materials:
|
||||
mat = bpy.data.materials[material_name]
|
||||
# Clear existing nodes
|
||||
mat.node_tree.nodes.clear()
|
||||
else:
|
||||
mat = bpy.data.materials.new(name=material_name)
|
||||
mat.use_nodes = True
|
||||
mat.node_tree.nodes.clear()
|
||||
|
||||
# Set up shader nodes
|
||||
nodes = mat.node_tree.nodes
|
||||
links = mat.node_tree.links
|
||||
|
||||
# Create nodes
|
||||
output_node = nodes.new(type='ShaderNodeOutputMaterial')
|
||||
output_node.location = (400, 0)
|
||||
|
||||
# Shader type based on settings
|
||||
if props.shader_type == 'PRINCIPLED':
|
||||
shader_node = nodes.new(type='ShaderNodeBsdfPrincipled')
|
||||
shader_node.location = (200, 0)
|
||||
|
||||
# Create diffuse texture node
|
||||
tex_node = nodes.new(type='ShaderNodeTexImage')
|
||||
tex_node.image = blender_img
|
||||
tex_node.location = (-200, 0)
|
||||
|
||||
# Connect texture to shader
|
||||
if props.shader_connection == 'BASE_COLOR':
|
||||
links.new(tex_node.outputs['Color'], shader_node.inputs['Base Color'])
|
||||
elif props.shader_connection == 'EMISSION':
|
||||
links.new(tex_node.outputs['Color'], shader_node.inputs['Emission Color'])
|
||||
shader_node.inputs['Emission Strength'].default_value = props.emission_strength
|
||||
elif props.shader_connection == 'ALPHA':
|
||||
links.new(tex_node.outputs['Alpha'], shader_node.inputs['Alpha'])
|
||||
# Also connect color for visibility
|
||||
links.new(tex_node.outputs['Color'], shader_node.inputs['Base Color'])
|
||||
mat.blend_method = 'BLEND'
|
||||
|
||||
# Connect alpha if enabled
|
||||
if props.use_alpha:
|
||||
links.new(tex_node.outputs['Alpha'], shader_node.inputs['Alpha'])
|
||||
mat.blend_method = 'BLEND'
|
||||
|
||||
# Add normal map nodes if normal map was generated
|
||||
if normal_map_blender_img:
|
||||
# Create normal map texture node
|
||||
normal_tex_node = nodes.new(type='ShaderNodeTexImage')
|
||||
normal_tex_node.image = normal_map_blender_img
|
||||
normal_tex_node.location = (-200, -300)
|
||||
normal_tex_node.label = "Normal Map"
|
||||
|
||||
# Create normal map node
|
||||
normal_map_node = nodes.new(type='ShaderNodeNormalMap')
|
||||
normal_map_node.location = (0, -300)
|
||||
|
||||
# Connect normal map texture to normal map node
|
||||
links.new(normal_tex_node.outputs['Color'], normal_map_node.inputs['Color'])
|
||||
|
||||
# Connect normal map node to shader
|
||||
links.new(normal_map_node.outputs['Normal'], shader_node.inputs['Normal'])
|
||||
|
||||
print(f"[Normal Map] Added normal map nodes to Principled BSDF shader")
|
||||
|
||||
# Handle shadow/reflection disabling
|
||||
current_shader = shader_node
|
||||
final_x_location = 400
|
||||
|
||||
# Disable shadows if enabled
|
||||
if props.disable_shadows:
|
||||
# Create light path node
|
||||
light_path_node = nodes.new(type='ShaderNodeLightPath')
|
||||
light_path_node.location = (50, 150)
|
||||
|
||||
# Create transparent BSDF
|
||||
transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
|
||||
transparent_node.location = (200, 150)
|
||||
|
||||
# Create mix shader for shadows
|
||||
shadow_mix_node = nodes.new(type='ShaderNodeMixShader')
|
||||
shadow_mix_node.location = (350, 50)
|
||||
|
||||
# Connect light path "Is Shadow Ray" to mix factor
|
||||
links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac'])
|
||||
|
||||
# Mix principled BSDF with transparent BSDF
|
||||
links.new(current_shader.outputs['BSDF'], shadow_mix_node.inputs[1]) # Shader 1 (not shadow)
|
||||
links.new(transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2]) # Shader 2 (shadow)
|
||||
|
||||
current_shader = shadow_mix_node
|
||||
final_x_location = 500
|
||||
|
||||
# Disable reflections if enabled
|
||||
if props.disable_reflections:
|
||||
# Create or reuse light path node
|
||||
if not props.disable_shadows:
|
||||
light_path_node = nodes.new(type='ShaderNodeLightPath')
|
||||
light_path_node.location = (50, 200)
|
||||
|
||||
# Create transparent BSDF for reflections
|
||||
reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
|
||||
reflection_transparent_node.location = (200, 200)
|
||||
|
||||
# Create mix shader for reflections
|
||||
reflection_mix_node = nodes.new(type='ShaderNodeMixShader')
|
||||
reflection_mix_node.location = (final_x_location - 50, 100)
|
||||
|
||||
# Connect light path "Is Reflection Ray" to mix factor
|
||||
links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac'])
|
||||
|
||||
# Mix current shader with transparent BSDF
|
||||
links.new(current_shader.outputs['Shader' if props.disable_shadows else 'BSDF'], reflection_mix_node.inputs[1]) # Shader 1 (not reflection)
|
||||
links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2]) # Shader 2 (reflection)
|
||||
|
||||
current_shader = reflection_mix_node
|
||||
final_x_location = final_x_location + 50
|
||||
|
||||
# Disable backfacing if enabled
|
||||
if props.disable_backfacing:
|
||||
# Create or reuse light path node
|
||||
if not props.disable_shadows and not props.disable_reflections:
|
||||
light_path_node = nodes.new(type='ShaderNodeLightPath')
|
||||
light_path_node.location = (50, 250)
|
||||
|
||||
# Create geometry node for backfacing detection
|
||||
geometry_node = nodes.new(type='ShaderNodeNewGeometry')
|
||||
geometry_node.location = (50, 300)
|
||||
|
||||
# Create transparent BSDF for backfaces
|
||||
backface_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
|
||||
backface_transparent_node.location = (200, 250)
|
||||
|
||||
# Create mix shader for backfaces
|
||||
backface_mix_node = nodes.new(type='ShaderNodeMixShader')
|
||||
backface_mix_node.location = (final_x_location - 50, 150)
|
||||
|
||||
# Connect geometry "Backfacing" to mix factor
|
||||
links.new(geometry_node.outputs['Backfacing'], backface_mix_node.inputs['Fac'])
|
||||
|
||||
# Mix current shader with transparent BSDF
|
||||
current_output = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'BSDF'
|
||||
links.new(current_shader.outputs[current_output], backface_mix_node.inputs[1]) # Shader 1 (front face)
|
||||
links.new(backface_transparent_node.outputs['BSDF'], backface_mix_node.inputs[2]) # Shader 2 (backface)
|
||||
|
||||
current_shader = backface_mix_node
|
||||
final_x_location = final_x_location + 50
|
||||
|
||||
# Update output node position
|
||||
output_node.location = (final_x_location, 0)
|
||||
|
||||
# Connect final shader to output
|
||||
output_socket = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'BSDF'
|
||||
links.new(current_shader.outputs[output_socket], output_node.inputs['Surface'])
|
||||
|
||||
elif props.shader_type == 'EMISSION':
|
||||
shader_node = nodes.new(type='ShaderNodeEmission')
|
||||
shader_node.location = (200, 0)
|
||||
|
||||
# Create texture node
|
||||
tex_node = nodes.new(type='ShaderNodeTexImage')
|
||||
tex_node.image = blender_img
|
||||
tex_node.location = (-200, 0)
|
||||
|
||||
# Connect texture to emission
|
||||
links.new(tex_node.outputs['Color'], shader_node.inputs['Color'])
|
||||
shader_node.inputs['Strength'].default_value = props.emission_strength
|
||||
|
||||
# Handle shadow/reflection disabling
|
||||
current_shader = shader_node
|
||||
final_x_location = 400
|
||||
|
||||
# Disable shadows if enabled
|
||||
if props.disable_shadows:
|
||||
# Create light path node
|
||||
light_path_node = nodes.new(type='ShaderNodeLightPath')
|
||||
light_path_node.location = (50, 150)
|
||||
|
||||
# Create transparent BSDF
|
||||
transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
|
||||
transparent_node.location = (200, 150)
|
||||
|
||||
# Create mix shader for shadows
|
||||
shadow_mix_node = nodes.new(type='ShaderNodeMixShader')
|
||||
shadow_mix_node.location = (350, 50)
|
||||
|
||||
# Connect light path "Is Shadow Ray" to mix factor
|
||||
links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac'])
|
||||
|
||||
# Mix emission with transparent BSDF
|
||||
links.new(current_shader.outputs['Emission'], shadow_mix_node.inputs[1]) # Shader 1 (not shadow)
|
||||
links.new(transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2]) # Shader 2 (shadow)
|
||||
|
||||
current_shader = shadow_mix_node
|
||||
final_x_location = 500
|
||||
|
||||
# Disable reflections if enabled
|
||||
if props.disable_reflections:
|
||||
# Create or reuse light path node
|
||||
if not props.disable_shadows:
|
||||
light_path_node = nodes.new(type='ShaderNodeLightPath')
|
||||
light_path_node.location = (50, 200)
|
||||
|
||||
# Create transparent BSDF for reflections
|
||||
reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
|
||||
reflection_transparent_node.location = (200, 200)
|
||||
|
||||
# Create mix shader for reflections
|
||||
reflection_mix_node = nodes.new(type='ShaderNodeMixShader')
|
||||
reflection_mix_node.location = (final_x_location - 50, 100)
|
||||
|
||||
# Connect light path "Is Reflection Ray" to mix factor
|
||||
links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac'])
|
||||
|
||||
# Mix current shader with transparent BSDF
|
||||
links.new(current_shader.outputs['Shader' if props.disable_shadows else 'Emission'], reflection_mix_node.inputs[1]) # Shader 1 (not reflection)
|
||||
links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2]) # Shader 2 (reflection)
|
||||
|
||||
current_shader = reflection_mix_node
|
||||
final_x_location = final_x_location + 50
|
||||
|
||||
# Update output node position
|
||||
output_node.location = (final_x_location, 0)
|
||||
|
||||
# Connect final shader to output
|
||||
output_socket = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'Emission'
|
||||
links.new(current_shader.outputs[output_socket], output_node.inputs['Surface'])
|
||||
|
||||
elif props.shader_type == 'TRANSPARENT':
|
||||
shader_node = nodes.new(type='ShaderNodeBsdfTransparent')
|
||||
mix_node = nodes.new(type='ShaderNodeMixShader')
|
||||
principled_node = nodes.new(type='ShaderNodeBsdfPrincipled')
|
||||
|
||||
shader_node.location = (200, 100)
|
||||
mix_node.location = (350, 0)
|
||||
principled_node.location = (200, -100)
|
||||
|
||||
# Create texture node
|
||||
tex_node = nodes.new(type='ShaderNodeTexImage')
|
||||
tex_node.image = blender_img
|
||||
tex_node.location = (-200, 0)
|
||||
|
||||
# Connect texture
|
||||
links.new(tex_node.outputs['Color'], principled_node.inputs['Base Color'])
|
||||
links.new(tex_node.outputs['Alpha'], mix_node.inputs['Fac'])
|
||||
|
||||
# Connect shaders to mix
|
||||
links.new(shader_node.outputs['BSDF'], mix_node.inputs[1])
|
||||
links.new(principled_node.outputs['BSDF'], mix_node.inputs[2])
|
||||
|
||||
# Handle shadow/reflection disabling
|
||||
current_shader = mix_node
|
||||
final_x_location = 500
|
||||
|
||||
# Disable shadows if enabled
|
||||
if props.disable_shadows:
|
||||
# Create light path node
|
||||
light_path_node = nodes.new(type='ShaderNodeLightPath')
|
||||
light_path_node.location = (50, 200)
|
||||
|
||||
# Create transparent BSDF for shadows (reuse existing transparent node)
|
||||
shadow_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
|
||||
shadow_transparent_node.location = (350, 150)
|
||||
|
||||
# Create mix shader for shadows
|
||||
shadow_mix_node = nodes.new(type='ShaderNodeMixShader')
|
||||
shadow_mix_node.location = (500, 50)
|
||||
|
||||
# Connect light path "Is Shadow Ray" to mix factor
|
||||
links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac'])
|
||||
|
||||
# Mix current shader with transparent BSDF
|
||||
links.new(current_shader.outputs['Shader'], shadow_mix_node.inputs[1]) # Shader 1 (not shadow)
|
||||
links.new(shadow_transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2]) # Shader 2 (shadow)
|
||||
|
||||
current_shader = shadow_mix_node
|
||||
final_x_location = 650
|
||||
|
||||
# Disable reflections if enabled
|
||||
if props.disable_reflections:
|
||||
# Create or reuse light path node
|
||||
if not props.disable_shadows:
|
||||
light_path_node = nodes.new(type='ShaderNodeLightPath')
|
||||
light_path_node.location = (50, 250)
|
||||
|
||||
# Create transparent BSDF for reflections
|
||||
reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
|
||||
reflection_transparent_node.location = (350, 200)
|
||||
|
||||
# Create mix shader for reflections
|
||||
reflection_mix_node = nodes.new(type='ShaderNodeMixShader')
|
||||
reflection_mix_node.location = (final_x_location - 50, 100)
|
||||
|
||||
# Connect light path "Is Reflection Ray" to mix factor
|
||||
links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac'])
|
||||
|
||||
# Mix current shader with transparent BSDF
|
||||
links.new(current_shader.outputs['Shader'], reflection_mix_node.inputs[1]) # Shader 1 (not reflection)
|
||||
links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2]) # Shader 2 (reflection)
|
||||
|
||||
current_shader = reflection_mix_node
|
||||
final_x_location = final_x_location + 50
|
||||
|
||||
# Disable backfacing if enabled
|
||||
if props.disable_backfacing:
|
||||
# Create or reuse light path node
|
||||
if not props.disable_shadows and not props.disable_reflections:
|
||||
light_path_node = nodes.new(type='ShaderNodeLightPath')
|
||||
light_path_node.location = (50, 300)
|
||||
|
||||
# Create geometry node for backfacing detection
|
||||
geometry_node = nodes.new(type='ShaderNodeNewGeometry')
|
||||
geometry_node.location = (50, 350)
|
||||
|
||||
# Create transparent BSDF for backfaces
|
||||
backface_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
|
||||
backface_transparent_node.location = (350, 300)
|
||||
|
||||
# Create mix shader for backfaces
|
||||
backface_mix_node = nodes.new(type='ShaderNodeMixShader')
|
||||
backface_mix_node.location = (final_x_location + 50, 150)
|
||||
|
||||
# Connect geometry "Backfacing" to mix factor
|
||||
links.new(geometry_node.outputs['Backfacing'], backface_mix_node.inputs['Fac'])
|
||||
|
||||
# Mix current shader with transparent BSDF
|
||||
links.new(current_shader.outputs['Shader'], backface_mix_node.inputs[1]) # Shader 1 (front face)
|
||||
links.new(backface_transparent_node.outputs['BSDF'], backface_mix_node.inputs[2]) # Shader 2 (backface)
|
||||
|
||||
current_shader = backface_mix_node
|
||||
final_x_location = final_x_location + 150
|
||||
|
||||
# Update output node position
|
||||
output_node.location = (final_x_location, 0)
|
||||
|
||||
# Connect final shader to output
|
||||
links.new(current_shader.outputs['Shader'], output_node.inputs['Surface'])
|
||||
|
||||
mat.blend_method = 'BLEND'
|
||||
|
||||
# Assign material to active object if enabled and possible
|
||||
if props.auto_assign_material and context.active_object and context.active_object.type in ['MESH', 'CURVE']:
|
||||
obj = context.active_object
|
||||
# Check if material slot exists, reuse if found, otherwise create new one
|
||||
slot_found = False
|
||||
for i, slot in enumerate(obj.material_slots):
|
||||
if slot.material and slot.material.name == material_name:
|
||||
# Material already exists in a slot, update it
|
||||
obj.material_slots[i].material = mat
|
||||
slot_found = True
|
||||
break
|
||||
|
||||
if not slot_found:
|
||||
# Create new material slot if not found
|
||||
obj.data.materials.append(mat)
|
||||
|
||||
self.report({'INFO'}, f"Generated shader '{material_name}' and assigned to {obj.name}")
|
||||
else:
|
||||
self.report({'INFO'}, f"Generated shader '{material_name}' (assign manually to object)")
|
||||
|
||||
# Switch to Shader Editor if available and set material
|
||||
for area in context.screen.areas:
|
||||
if area.type == 'NODE_EDITOR':
|
||||
for space in area.spaces:
|
||||
if space.type == 'NODE_EDITOR':
|
||||
space.shader_type = 'OBJECT'
|
||||
space.id = mat
|
||||
area.tag_redraw()
|
||||
break
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Shader generation failed: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Export all operator classes for wildcard imports
|
||||
__all__ = [
|
||||
'TEXT_TEXTURE_OT_generate',
|
||||
'TEXT_TEXTURE_OT_generate_shader',
|
||||
]
|
||||
893
operators/preset_ops.py
Normal file
893
operators/preset_ops.py
Normal file
@@ -0,0 +1,893 @@
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
|
||||
# Import utility functions from main module
|
||||
from ..presets import (
|
||||
get_preset_path, get_persistent_preset_path, get_addon_version,
|
||||
get_blend_file_presets, save_blend_file_preset, delete_blend_file_preset,
|
||||
refresh_presets_sync, ensure_presets_available
|
||||
)
|
||||
from ..utils import bl_info
|
||||
|
||||
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'}
|
||||
|
||||
overwrite: bpy.props.BoolProperty(
|
||||
name="Overwrite",
|
||||
description="Overwrite existing preset",
|
||||
default=False
|
||||
)
|
||||
|
||||
preset_name: bpy.props.StringProperty(
|
||||
name="Preset Name",
|
||||
description="Name of preset to save/overwrite",
|
||||
default=""
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
# Use provided preset name or fallback to input field
|
||||
preset_name = self.preset_name.strip() if self.preset_name else props.new_preset_name.strip()
|
||||
if not preset_name:
|
||||
self.report({'ERROR'}, "Please enter a preset name")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# When overwriting from button, update the input field for consistency
|
||||
if self.preset_name and self.preset_name.strip():
|
||||
props.new_preset_name = preset_name
|
||||
|
||||
# Validate preset name characters (avoid filesystem issues)
|
||||
import re
|
||||
# Allow Unicode letters, numbers, spaces, hyphens, underscores, and common accented characters
|
||||
# This pattern supports international characters like ö, ü, ñ, etc.
|
||||
if not re.match(r'^[\w\s\-]+$', preset_name, re.UNICODE):
|
||||
self.report({'ERROR'}, "Preset name contains invalid characters for filesystem compatibility")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Additional check for filesystem-unsafe characters
|
||||
invalid_chars = ['<', '>', ':', '"', '|', '?', '*', '/', '\\']
|
||||
if any(char in preset_name for char in invalid_chars):
|
||||
self.report({'ERROR'}, f"Preset name cannot contain: {' '.join(invalid_chars)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Check for duplicate names
|
||||
preset_file = os.path.join(get_preset_path(), f"{preset_name}.json")
|
||||
if os.path.exists(preset_file) and not self.overwrite:
|
||||
# Suggest alternative names
|
||||
base_name = preset_name
|
||||
counter = 1
|
||||
suggested_name = f"{base_name}_{counter}"
|
||||
while os.path.exists(os.path.join(get_preset_path(), f"{suggested_name}.json")):
|
||||
counter += 1
|
||||
suggested_name = f"{base_name}_{counter}"
|
||||
|
||||
self.report({'ERROR'}, f"Preset '{preset_name}' already exists. Suggestion: try '{suggested_name}' or delete the existing preset first.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# DETAILED LOGGING: Current shader properties before saving
|
||||
print(f"[PRESET SAVE DEBUG] ==================== SAVING PRESET: '{preset_name}' ====================")
|
||||
print(f"[PRESET SAVE DEBUG] Current shader properties from props:")
|
||||
print(f"[PRESET SAVE DEBUG] disable_shadows: {props.disable_shadows}")
|
||||
print(f"[PRESET SAVE DEBUG] disable_reflections: {props.disable_reflections}")
|
||||
print(f"[PRESET SAVE DEBUG] disable_backfacing: {props.disable_backfacing}")
|
||||
print(f"[PRESET SAVE DEBUG] shader_type: {props.shader_type}")
|
||||
print(f"[PRESET SAVE DEBUG] shader_connection: {props.shader_connection}")
|
||||
print(f"[PRESET SAVE DEBUG] use_alpha: {props.use_alpha}")
|
||||
print(f"[PRESET SAVE DEBUG] emission_strength: {props.emission_strength}")
|
||||
print(f"[PRESET SAVE DEBUG] auto_assign_material: {props.auto_assign_material}")
|
||||
|
||||
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,
|
||||
'prepend_text': props.prepend_text,
|
||||
'append_text': props.append_text,
|
||||
'prepend_margin': props.prepend_margin,
|
||||
'append_margin': props.append_margin,
|
||||
'prepend_append_layout': props.prepend_append_layout,
|
||||
'shader_type': props.shader_type,
|
||||
'shader_connection': props.shader_connection,
|
||||
'use_alpha': props.use_alpha,
|
||||
'emission_strength': props.emission_strength,
|
||||
'auto_assign_material': props.auto_assign_material,
|
||||
'disable_shadows': props.disable_shadows,
|
||||
'disable_reflections': props.disable_reflections,
|
||||
'disable_backfacing': props.disable_backfacing,
|
||||
'generate_normal_map': props.generate_normal_map,
|
||||
'normal_map_strength': props.normal_map_strength,
|
||||
'normal_map_blur_radius': props.normal_map_blur_radius,
|
||||
'normal_map_invert': props.normal_map_invert,
|
||||
'enable_multiline': props.enable_multiline,
|
||||
'line_height': props.line_height,
|
||||
'text_alignment': props.text_alignment,
|
||||
'auto_wrap': props.auto_wrap,
|
||||
'wrap_width': props.wrap_width,
|
||||
'max_lines': props.max_lines,
|
||||
'line_spacing_pixels': props.line_spacing_pixels,
|
||||
'enable_stroke': props.enable_stroke,
|
||||
'stroke_width': props.stroke_width,
|
||||
'stroke_position': props.stroke_position,
|
||||
'stroke_color': list(props.stroke_color),
|
||||
'enable_shadow': props.enable_shadow,
|
||||
'shadow_offset_x': props.shadow_offset_x,
|
||||
'shadow_offset_y': props.shadow_offset_y,
|
||||
'shadow_blur': props.shadow_blur,
|
||||
'shadow_color': list(props.shadow_color),
|
||||
'shadow_spread': getattr(props, 'shadow_spread', 1.0),
|
||||
'enable_glow': props.enable_glow,
|
||||
'glow_blur': props.glow_blur,
|
||||
'glow_color': list(props.glow_color),
|
||||
'enable_blur': props.enable_blur,
|
||||
'blur_radius': props.blur_radius,
|
||||
'image_overlays': []
|
||||
}
|
||||
|
||||
# DETAILED LOGGING: Verify shader properties in preset_data
|
||||
print(f"[PRESET SAVE DEBUG] Shader properties in preset_data dictionary:")
|
||||
print(f"[PRESET SAVE DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
|
||||
print(f"[PRESET SAVE DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
|
||||
print(f"[PRESET SAVE DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
|
||||
|
||||
# Save overlay data
|
||||
for overlay in props.image_overlays:
|
||||
overlay_data = {
|
||||
'name': overlay.name,
|
||||
'image_path': overlay.image_path,
|
||||
'x_position': overlay.x_position,
|
||||
'y_position': overlay.y_position,
|
||||
'scale': overlay.scale,
|
||||
'rotation': overlay.rotation,
|
||||
'z_index': overlay.z_index,
|
||||
'enabled': overlay.enabled
|
||||
}
|
||||
preset_data['image_overlays'].append(overlay_data)
|
||||
|
||||
try:
|
||||
# Primary: Save to blend file (new system)
|
||||
print(f"[PRESET SAVE DEBUG] Attempting to save to blend file...")
|
||||
blend_success = save_blend_file_preset(preset_name, preset_data)
|
||||
print(f"[PRESET SAVE DEBUG] Blend file save result: {blend_success}")
|
||||
|
||||
# ALWAYS save to persistent storage for cross-blend-file sharing and addon update survival
|
||||
persistent_file = os.path.join(get_persistent_preset_path(), f"{preset_name}.json")
|
||||
print(f"[PRESET SAVE DEBUG] Attempting to save to persistent file: {persistent_file}")
|
||||
persistent_success = True
|
||||
try:
|
||||
with open(persistent_file, 'w') as f:
|
||||
json.dump(preset_data, f, indent=2)
|
||||
print(f"[PRESET SAVE DEBUG] Successfully saved preset '{preset_name}' to persistent storage")
|
||||
|
||||
# VERIFY: Read back the persistent file to confirm shader properties were saved
|
||||
print(f"[PRESET SAVE DEBUG] Verifying persistent file contents...")
|
||||
with open(persistent_file, 'r') as f:
|
||||
saved_data = json.load(f)
|
||||
print(f"[PRESET SAVE DEBUG] Persistent file shader properties verification:")
|
||||
print(f"[PRESET SAVE DEBUG] disable_shadows: {saved_data.get('disable_shadows', 'NOT FOUND')}")
|
||||
print(f"[PRESET SAVE DEBUG] disable_reflections: {saved_data.get('disable_reflections', 'NOT FOUND')}")
|
||||
print(f"[PRESET SAVE DEBUG] disable_backfacing: {saved_data.get('disable_backfacing', 'NOT FOUND')}")
|
||||
|
||||
except Exception as persistent_error:
|
||||
print(f"[PRESET SAVE DEBUG] Warning: Failed to save persistent preset file: {persistent_error}")
|
||||
persistent_success = False
|
||||
|
||||
# Also save to legacy location for backward compatibility (optional)
|
||||
legacy_success = True
|
||||
try:
|
||||
legacy_file = os.path.join(get_preset_path(), f"{preset_name}.json")
|
||||
with open(legacy_file, 'w') as f:
|
||||
json.dump(preset_data, f, indent=2)
|
||||
print(f"[PRESET SAVE DEBUG] Also saved to legacy location for backward compatibility")
|
||||
except Exception as legacy_error:
|
||||
print(f"[PRESET SAVE DEBUG] Note: Could not save to legacy location: {legacy_error}")
|
||||
legacy_success = False
|
||||
|
||||
if not blend_success and not persistent_success:
|
||||
self.report({'ERROR'}, f"Failed to save preset to both blend file and persistent storage")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Check if preset already exists in the collection and update it
|
||||
existing_preset = None
|
||||
for preset in props.presets:
|
||||
if preset.name == preset_name:
|
||||
existing_preset = preset
|
||||
break
|
||||
|
||||
if not existing_preset:
|
||||
preset = props.presets.add()
|
||||
preset.name = preset_name
|
||||
# Priority: Blend file > Persistent file
|
||||
preset.source = 'BLEND_FILE' if blend_success else 'PERSISTENT_FILE'
|
||||
else:
|
||||
# Update source based on where it was successfully saved
|
||||
existing_preset.source = 'BLEND_FILE' if blend_success else 'PERSISTENT_FILE'
|
||||
|
||||
# Don't clear the input field - keep the name for easy re-saving
|
||||
# props.new_preset_name = "New Preset"
|
||||
|
||||
# Provide clear feedback
|
||||
storage_info = ""
|
||||
if blend_success and persistent_success:
|
||||
storage_info = " (saved to blend file + persistent storage)"
|
||||
elif blend_success:
|
||||
storage_info = " (saved to blend file)"
|
||||
elif persistent_success:
|
||||
storage_info = " (saved to persistent storage)"
|
||||
|
||||
if self.overwrite:
|
||||
self.report({'INFO'}, f"✅ Preset '{preset_name}' updated successfully{storage_info}")
|
||||
else:
|
||||
self.report({'INFO'}, f"✅ Preset '{preset_name}' saved successfully{storage_info}")
|
||||
|
||||
except (OSError, IOError, json.JSONEncodeError) as e:
|
||||
self.report({'ERROR'}, f"Failed to save preset: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Unexpected error saving preset: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Check if preset exists and prompt for overwrite
|
||||
props = context.scene.text_texture_props
|
||||
preset_name = props.new_preset_name.strip()
|
||||
|
||||
if preset_name and preset_name != "New Preset":
|
||||
preset_file = os.path.join(get_preset_path(), f"{preset_name}.json")
|
||||
if os.path.exists(preset_file):
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
|
||||
return self.execute(context)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
props = context.scene.text_texture_props
|
||||
preset_name = props.new_preset_name.strip()
|
||||
layout.label(text=f"Overwrite existing preset '{preset_name}'?")
|
||||
|
||||
class TEXT_TEXTURE_OT_save_preset_enter(Operator):
|
||||
"""Save preset when Enter key is pressed in name field"""
|
||||
bl_idname = "text_texture.save_preset_enter"
|
||||
bl_label = "Save Preset (Enter)"
|
||||
bl_description = "Save current settings as preset when Enter is pressed"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
# Delegate to the main save preset operator
|
||||
return bpy.ops.text_texture.save_preset('INVOKE_DEFAULT')
|
||||
|
||||
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
|
||||
|
||||
# DEFENSIVE PROGRAMMING: Ensure presets are available before loading
|
||||
if not ensure_presets_available():
|
||||
self.report({'ERROR'}, "Failed to ensure presets are loaded - cannot load preset")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# DETAILED LOGGING: Current shader properties before loading
|
||||
print(f"[PRESET LOAD DEBUG] ==================== LOADING PRESET: '{self.preset_name}' ====================")
|
||||
print(f"[PRESET LOAD DEBUG] Current shader properties BEFORE loading:")
|
||||
print(f"[PRESET LOAD DEBUG] disable_shadows: {props.disable_shadows}")
|
||||
print(f"[PRESET LOAD DEBUG] disable_reflections: {props.disable_reflections}")
|
||||
print(f"[PRESET LOAD DEBUG] disable_backfacing: {props.disable_backfacing}")
|
||||
print(f"[PRESET LOAD DEBUG] shader_type: {props.shader_type}")
|
||||
print(f"[PRESET LOAD DEBUG] shader_connection: {props.shader_connection}")
|
||||
|
||||
try:
|
||||
# Three-tier loading priority: Blend File > Persistent File > Legacy File
|
||||
blend_presets = get_blend_file_presets()
|
||||
preset_data = None
|
||||
source = 'BLEND_FILE'
|
||||
|
||||
print(f"[PRESET LOAD DEBUG] Available blend file presets: {list(blend_presets.keys())}")
|
||||
|
||||
if self.preset_name in blend_presets:
|
||||
preset_data = blend_presets[self.preset_name]
|
||||
source = 'BLEND_FILE'
|
||||
print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from blend file")
|
||||
print(f"[PRESET LOAD DEBUG] Blend file preset shader properties:")
|
||||
print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
|
||||
print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
|
||||
print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
|
||||
else:
|
||||
# Try persistent storage next
|
||||
persistent_file = os.path.join(get_persistent_preset_path(), f"{self.preset_name}.json")
|
||||
print(f"[PRESET LOAD DEBUG] Preset not in blend file, trying persistent file: {persistent_file}")
|
||||
if os.path.exists(persistent_file):
|
||||
with open(persistent_file, 'r') as f:
|
||||
preset_data = json.load(f)
|
||||
source = 'PERSISTENT_FILE'
|
||||
print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from persistent file")
|
||||
print(f"[PRESET LOAD DEBUG] Persistent file preset shader properties:")
|
||||
print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
|
||||
print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
|
||||
print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
|
||||
else:
|
||||
# Fallback to legacy local file
|
||||
legacy_file = os.path.join(get_preset_path(), f"{self.preset_name}.json")
|
||||
print(f"[PRESET LOAD DEBUG] Preset not in persistent storage, trying legacy file: {legacy_file}")
|
||||
if os.path.exists(legacy_file):
|
||||
with open(legacy_file, 'r') as f:
|
||||
preset_data = json.load(f)
|
||||
source = 'LOCAL_FILE'
|
||||
print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from legacy file")
|
||||
print(f"[PRESET LOAD DEBUG] Legacy file preset shader properties:")
|
||||
print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
|
||||
print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
|
||||
print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
|
||||
else:
|
||||
print(f"[PRESET LOAD DEBUG] Legacy file does not exist: {legacy_file}")
|
||||
self.report({'ERROR'}, f"Preset not found in any storage location: {self.preset_name}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not preset_data:
|
||||
print(f"[PRESET LOAD DEBUG] No preset data found!")
|
||||
self.report({'ERROR'}, f"No data found for preset: {self.preset_name}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
props.is_updating = True
|
||||
|
||||
# Skip loading text to prevent overwriting current text
|
||||
# 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.prepend_text = preset_data.get('prepend_text', props.prepend_text)
|
||||
props.append_text = preset_data.get('append_text', props.append_text)
|
||||
props.prepend_margin = preset_data.get('prepend_margin', props.prepend_margin)
|
||||
props.append_margin = preset_data.get('append_margin', props.append_margin)
|
||||
props.prepend_append_layout = preset_data.get('prepend_append_layout', props.prepend_append_layout)
|
||||
|
||||
# DETAILED LOGGING: Shader property loading with before/after values
|
||||
print(f"[PRESET LOAD DEBUG] Loading shader properties...")
|
||||
|
||||
old_shader_type = props.shader_type
|
||||
props.shader_type = preset_data.get('shader_type', props.shader_type)
|
||||
print(f"[PRESET LOAD DEBUG] shader_type: {old_shader_type} -> {props.shader_type}")
|
||||
|
||||
old_shader_connection = props.shader_connection
|
||||
props.shader_connection = preset_data.get('shader_connection', props.shader_connection)
|
||||
print(f"[PRESET LOAD DEBUG] shader_connection: {old_shader_connection} -> {props.shader_connection}")
|
||||
|
||||
old_use_alpha = props.use_alpha
|
||||
props.use_alpha = preset_data.get('use_alpha', props.use_alpha)
|
||||
print(f"[PRESET LOAD DEBUG] use_alpha: {old_use_alpha} -> {props.use_alpha}")
|
||||
|
||||
old_emission_strength = props.emission_strength
|
||||
props.emission_strength = preset_data.get('emission_strength', props.emission_strength)
|
||||
print(f"[PRESET LOAD DEBUG] emission_strength: {old_emission_strength} -> {props.emission_strength}")
|
||||
|
||||
old_auto_assign = props.auto_assign_material
|
||||
props.auto_assign_material = preset_data.get('auto_assign_material', props.auto_assign_material)
|
||||
print(f"[PRESET LOAD DEBUG] auto_assign_material: {old_auto_assign} -> {props.auto_assign_material}")
|
||||
|
||||
# CRITICAL SHADER PROPERTIES - Most detailed logging
|
||||
print(f"[PRESET LOAD DEBUG] ==================== CRITICAL SHADER PROPERTIES ====================")
|
||||
|
||||
old_disable_shadows = props.disable_shadows
|
||||
new_disable_shadows = preset_data.get('disable_shadows', props.disable_shadows)
|
||||
props.disable_shadows = new_disable_shadows
|
||||
print(f"[PRESET LOAD DEBUG] disable_shadows:")
|
||||
print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_shadows} (type: {type(old_disable_shadows)})")
|
||||
print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_shadows', 'NOT FOUND')} (type: {type(preset_data.get('disable_shadows', 'N/A'))})")
|
||||
print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_shadows} (type: {type(props.disable_shadows)})")
|
||||
|
||||
old_disable_reflections = props.disable_reflections
|
||||
new_disable_reflections = preset_data.get('disable_reflections', props.disable_reflections)
|
||||
props.disable_reflections = new_disable_reflections
|
||||
print(f"[PRESET LOAD DEBUG] disable_reflections:")
|
||||
print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_reflections} (type: {type(old_disable_reflections)})")
|
||||
print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_reflections', 'NOT FOUND')} (type: {type(preset_data.get('disable_reflections', 'N/A'))})")
|
||||
print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_reflections} (type: {type(props.disable_reflections)})")
|
||||
|
||||
old_disable_backfacing = props.disable_backfacing
|
||||
new_disable_backfacing = preset_data.get('disable_backfacing', props.disable_backfacing)
|
||||
props.disable_backfacing = new_disable_backfacing
|
||||
print(f"[PRESET LOAD DEBUG] disable_backfacing:")
|
||||
print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_backfacing} (type: {type(old_disable_backfacing)})")
|
||||
print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_backfacing', 'NOT FOUND')} (type: {type(preset_data.get('disable_backfacing', 'N/A'))})")
|
||||
print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_backfacing} (type: {type(props.disable_backfacing)})")
|
||||
|
||||
print(f"[PRESET LOAD DEBUG] ================================================================")
|
||||
|
||||
props.generate_normal_map = preset_data.get('generate_normal_map', props.generate_normal_map)
|
||||
props.normal_map_strength = preset_data.get('normal_map_strength', props.normal_map_strength)
|
||||
props.normal_map_blur_radius = preset_data.get('normal_map_blur_radius', props.normal_map_blur_radius)
|
||||
props.normal_map_invert = preset_data.get('normal_map_invert', props.normal_map_invert)
|
||||
|
||||
# Load multiline settings
|
||||
props.enable_multiline = preset_data.get('enable_multiline', props.enable_multiline)
|
||||
props.line_height = preset_data.get('line_height', props.line_height)
|
||||
props.text_alignment = preset_data.get('text_alignment', props.text_alignment)
|
||||
props.auto_wrap = preset_data.get('auto_wrap', props.auto_wrap)
|
||||
props.wrap_width = preset_data.get('wrap_width', props.wrap_width)
|
||||
props.max_lines = preset_data.get('max_lines', props.max_lines)
|
||||
props.line_spacing_pixels = preset_data.get('line_spacing_pixels', props.line_spacing_pixels)
|
||||
|
||||
# Load stroke settings
|
||||
props.enable_stroke = preset_data.get('enable_stroke', props.enable_stroke)
|
||||
props.stroke_width = preset_data.get('stroke_width', props.stroke_width)
|
||||
props.stroke_position = preset_data.get('stroke_position', props.stroke_position)
|
||||
props.stroke_color = preset_data.get('stroke_color', props.stroke_color)
|
||||
|
||||
# Load shadow/glow settings
|
||||
props.enable_shadow = preset_data.get('enable_shadow', props.enable_shadow)
|
||||
props.shadow_offset_x = preset_data.get('shadow_offset_x', props.shadow_offset_x)
|
||||
props.shadow_offset_y = preset_data.get('shadow_offset_y', props.shadow_offset_y)
|
||||
props.shadow_blur = preset_data.get('shadow_blur', props.shadow_blur)
|
||||
props.shadow_color = preset_data.get('shadow_color', props.shadow_color)
|
||||
# Handle new shadow_spread property with backward compatibility
|
||||
if hasattr(props, 'shadow_spread'):
|
||||
props.shadow_spread = preset_data.get('shadow_spread', 1.0)
|
||||
props.enable_glow = preset_data.get('enable_glow', props.enable_glow)
|
||||
props.glow_blur = preset_data.get('glow_blur', props.glow_blur)
|
||||
props.glow_color = preset_data.get('glow_color', props.glow_color)
|
||||
|
||||
# Load blur settings
|
||||
props.enable_blur = preset_data.get('enable_blur', props.enable_blur)
|
||||
props.blur_radius = preset_data.get('blur_radius', props.blur_radius)
|
||||
|
||||
# Load text fitting settings
|
||||
props.enable_text_fitting = preset_data.get('enable_text_fitting', props.enable_text_fitting)
|
||||
props.text_fitting_mode = preset_data.get('text_fitting_mode', props.text_fitting_mode)
|
||||
props.text_fitting_margin = preset_data.get('text_fitting_margin', props.text_fitting_margin)
|
||||
props.min_font_size = preset_data.get('min_font_size', props.min_font_size)
|
||||
props.max_font_size = preset_data.get('max_font_size', props.max_font_size)
|
||||
|
||||
# Load overlay data
|
||||
props.image_overlays.clear()
|
||||
overlay_data_list = preset_data.get('image_overlays', [])
|
||||
for overlay_data in overlay_data_list:
|
||||
overlay = props.image_overlays.add()
|
||||
overlay.name = overlay_data.get('name', 'Overlay')
|
||||
overlay.image_path = overlay_data.get('image_path', '')
|
||||
overlay.x_position = overlay_data.get('x_position', 0.5)
|
||||
overlay.y_position = overlay_data.get('y_position', 0.5)
|
||||
overlay.scale = overlay_data.get('scale', 1.0)
|
||||
overlay.rotation = overlay_data.get('rotation', 0.0)
|
||||
overlay.z_index = overlay_data.get('z_index', 1)
|
||||
overlay.enabled = overlay_data.get('enabled', True)
|
||||
|
||||
props.is_updating = False
|
||||
|
||||
# FINAL VERIFICATION: Check what the properties actually are after loading
|
||||
print(f"[PRESET LOAD DEBUG] ==================== FINAL VERIFICATION ====================")
|
||||
print(f"[PRESET LOAD DEBUG] Properties AFTER loading preset '{self.preset_name}':")
|
||||
print(f"[PRESET LOAD DEBUG] disable_shadows: {props.disable_shadows} (type: {type(props.disable_shadows)})")
|
||||
print(f"[PRESET LOAD DEBUG] disable_reflections: {props.disable_reflections} (type: {type(props.disable_reflections)})")
|
||||
print(f"[PRESET LOAD DEBUG] disable_backfacing: {props.disable_backfacing} (type: {type(props.disable_backfacing)})")
|
||||
print(f"[PRESET LOAD DEBUG] ================================================================")
|
||||
|
||||
# Always generate preview after loading preset
|
||||
# Import preview generation function
|
||||
from .. import generate_preview
|
||||
generate_preview(context)
|
||||
|
||||
source_info = {
|
||||
'BLEND_FILE': "from blend file",
|
||||
'PERSISTENT_FILE': "from persistent storage",
|
||||
'LOCAL_FILE': "from legacy storage"
|
||||
}.get(source, "from unknown source")
|
||||
self.report({'INFO'}, f"✅ Loaded preset '{self.preset_name}' {source_info}")
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
props.is_updating = False
|
||||
self.report({'ERROR'}, f"Failed to read preset file: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
except json.JSONDecodeError as e:
|
||||
props.is_updating = False
|
||||
self.report({'ERROR'}, f"Invalid preset file format: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
except Exception as e:
|
||||
props.is_updating = False
|
||||
self.report({'ERROR'}, f"Unexpected error loading preset: {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:
|
||||
# Delete from both blend file and local storage
|
||||
blend_deleted = delete_blend_file_preset(self.preset_name)
|
||||
|
||||
preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json")
|
||||
local_deleted = False
|
||||
if os.path.exists(preset_file):
|
||||
os.remove(preset_file)
|
||||
local_deleted = True
|
||||
|
||||
if not blend_deleted and not local_deleted:
|
||||
self.report({'WARNING'}, f"Preset '{self.preset_name}' not found in blend file or local storage")
|
||||
|
||||
# Remove from UI list
|
||||
for i, preset in enumerate(props.presets):
|
||||
if preset.name == self.preset_name:
|
||||
props.presets.remove(i)
|
||||
break
|
||||
|
||||
delete_info = []
|
||||
if blend_deleted:
|
||||
delete_info.append("blend file")
|
||||
if local_deleted:
|
||||
delete_info.append("local storage")
|
||||
|
||||
location_text = " and ".join(delete_info) if delete_info else "nowhere (not found)"
|
||||
self.report({'INFO'}, f"🗑️ Deleted preset '{self.preset_name}' from {location_text}")
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.report({'ERROR'}, f"Failed to delete preset file: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Unexpected error deleting preset: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Add confirmation dialog for better UX
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text=f"Delete preset '{self.preset_name}'?")
|
||||
layout.label(text="This action cannot be undone.", icon='ERROR')
|
||||
|
||||
class TEXT_TEXTURE_OT_refresh_presets(Operator):
|
||||
"""Refresh presets from blend file and local storage"""
|
||||
bl_idname = "text_texture.refresh_presets"
|
||||
bl_label = "Refresh Presets"
|
||||
bl_description = "Reload presets from blend file and local storage"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
# Use the reliable synchronous refresh system
|
||||
success = refresh_presets_sync()
|
||||
|
||||
if success:
|
||||
props = context.scene.text_texture_props
|
||||
total_presets = len(props.presets)
|
||||
|
||||
# Count by source for detailed reporting
|
||||
blend_count = len([p for p in props.presets if p.source == 'BLEND_FILE'])
|
||||
persistent_count = len([p for p in props.presets if p.source == 'PERSISTENT_FILE'])
|
||||
local_count = len([p for p in props.presets if p.source == 'LOCAL_FILE'])
|
||||
|
||||
self.report({'INFO'}, f"✅ Refreshed {total_presets} presets ({blend_count} blend file, {persistent_count} persistent, {local_count} local)")
|
||||
else:
|
||||
self.report({'WARNING'}, "Preset refresh completed with issues - check console for details")
|
||||
|
||||
# Force UI redraw
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Failed to refresh presets: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'CANCELLED'}
|
||||
|
||||
class TEXT_TEXTURE_OT_export_presets(Operator):
|
||||
"""Export all presets to a file for backup"""
|
||||
bl_idname = "text_texture.export_presets"
|
||||
bl_label = "Export Presets"
|
||||
bl_description = "Export all presets to a file for backup"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
description="Path to save preset backup file",
|
||||
subtype='FILE_PATH',
|
||||
default="text_texture_presets_backup.json"
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
# Collect all presets from all sources
|
||||
all_presets = {}
|
||||
|
||||
# Get blend file presets
|
||||
blend_presets = get_blend_file_presets()
|
||||
for name, data in blend_presets.items():
|
||||
all_presets[name] = {
|
||||
"data": data,
|
||||
"source": "BLEND_FILE"
|
||||
}
|
||||
|
||||
# Get persistent presets
|
||||
persistent_dir = get_persistent_preset_path()
|
||||
if os.path.exists(persistent_dir):
|
||||
for filename in os.listdir(persistent_dir):
|
||||
if filename.endswith('.json'):
|
||||
preset_name = os.path.splitext(filename)[0]
|
||||
if preset_name not in all_presets: # Don't override blend file presets
|
||||
try:
|
||||
with open(os.path.join(persistent_dir, filename), 'r') as f:
|
||||
preset_data = json.load(f)
|
||||
all_presets[preset_name] = {
|
||||
"data": preset_data,
|
||||
"source": "PERSISTENT_FILE"
|
||||
}
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"[TTG] Error reading persistent preset {preset_name}: {e}")
|
||||
|
||||
# Get legacy presets
|
||||
legacy_dir = get_preset_path()
|
||||
if os.path.exists(legacy_dir) and legacy_dir != persistent_dir:
|
||||
for filename in os.listdir(legacy_dir):
|
||||
if filename.endswith('.json'):
|
||||
preset_name = os.path.splitext(filename)[0]
|
||||
if preset_name not in all_presets: # Don't override higher priority presets
|
||||
try:
|
||||
with open(os.path.join(legacy_dir, filename), 'r') as f:
|
||||
preset_data = json.load(f)
|
||||
all_presets[preset_name] = {
|
||||
"data": preset_data,
|
||||
"source": "LOCAL_FILE"
|
||||
}
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"[TTG] Error reading legacy preset {preset_name}: {e}")
|
||||
|
||||
if not all_presets:
|
||||
self.report({'WARNING'}, "No presets found to export")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create export data with metadata
|
||||
export_data = {
|
||||
"format_version": "1.0",
|
||||
"addon_version": bl_info["version"],
|
||||
"export_timestamp": time.time(),
|
||||
"presets": all_presets
|
||||
}
|
||||
|
||||
# Write to file
|
||||
with open(self.filepath, 'w') as f:
|
||||
json.dump(export_data, f, indent=2)
|
||||
|
||||
self.report({'INFO'}, f"✅ Exported {len(all_presets)} presets to {os.path.basename(self.filepath)}")
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Failed to export presets: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Set default filename with timestamp
|
||||
import datetime
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
self.filepath = f"text_texture_presets_backup_{timestamp}.json"
|
||||
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
class TEXT_TEXTURE_OT_import_presets(Operator):
|
||||
"""Import presets from a backup file"""
|
||||
bl_idname = "text_texture.import_presets"
|
||||
bl_label = "Import Presets"
|
||||
bl_description = "Import presets from a backup file"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
description="Path to preset backup file",
|
||||
subtype='FILE_PATH'
|
||||
)
|
||||
|
||||
import_mode: EnumProperty(
|
||||
name="Import Mode",
|
||||
description="How to handle existing presets with same names",
|
||||
items=[
|
||||
('SKIP', 'Skip Existing', 'Skip presets that already exist'),
|
||||
('OVERWRITE', 'Overwrite', 'Overwrite existing presets'),
|
||||
('RENAME', 'Rename New', 'Rename imported presets if they conflict'),
|
||||
],
|
||||
default='SKIP'
|
||||
)
|
||||
|
||||
import_to_blend: BoolProperty(
|
||||
name="Import to Blend File",
|
||||
description="Import presets to blend file (recommended for portability)",
|
||||
default=True
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
if not os.path.exists(self.filepath):
|
||||
self.report({'ERROR'}, "Backup file not found")
|
||||
return {'CANCELLED'}
|
||||
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
# Read backup file
|
||||
with open(self.filepath, 'r') as f:
|
||||
import_data = json.load(f)
|
||||
|
||||
# Validate format
|
||||
if "presets" not in import_data:
|
||||
self.report({'ERROR'}, "Invalid backup file format")
|
||||
return {'CANCELLED'}
|
||||
|
||||
imported_presets = import_data["presets"]
|
||||
if not imported_presets:
|
||||
self.report({'WARNING'}, "No presets found in backup file")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Get existing presets to check for conflicts
|
||||
existing_blend_presets = get_blend_file_presets()
|
||||
existing_preset_names = set(existing_blend_presets.keys())
|
||||
|
||||
# Also check persistent storage
|
||||
persistent_dir = get_persistent_preset_path()
|
||||
if os.path.exists(persistent_dir):
|
||||
for filename in os.listdir(persistent_dir):
|
||||
if filename.endswith('.json'):
|
||||
existing_preset_names.add(os.path.splitext(filename)[0])
|
||||
|
||||
imported_count = 0
|
||||
skipped_count = 0
|
||||
errors = []
|
||||
|
||||
for preset_name, preset_info in imported_presets.items():
|
||||
preset_data = preset_info.get("data", {})
|
||||
|
||||
# Handle conflicts
|
||||
final_name = preset_name
|
||||
if preset_name in existing_preset_names:
|
||||
if self.import_mode == 'SKIP':
|
||||
skipped_count += 1
|
||||
continue
|
||||
elif self.import_mode == 'RENAME':
|
||||
counter = 1
|
||||
base_name = preset_name
|
||||
while final_name in existing_preset_names:
|
||||
final_name = f"{base_name}_imported_{counter}"
|
||||
counter += 1
|
||||
# OVERWRITE mode uses original name
|
||||
|
||||
try:
|
||||
# Import to blend file or persistent storage based on setting
|
||||
if self.import_to_blend:
|
||||
success = save_blend_file_preset(final_name, preset_data)
|
||||
if not success:
|
||||
raise Exception("Failed to save to blend file")
|
||||
else:
|
||||
# Save to persistent storage
|
||||
persistent_file = os.path.join(get_persistent_preset_path(), f"{final_name}.json")
|
||||
with open(persistent_file, 'w') as f:
|
||||
json.dump(preset_data, f, indent=2)
|
||||
|
||||
imported_count += 1
|
||||
|
||||
# Add to UI list if not already present
|
||||
existing_preset = None
|
||||
for preset in props.presets:
|
||||
if preset.name == final_name:
|
||||
existing_preset = preset
|
||||
break
|
||||
|
||||
if not existing_preset:
|
||||
new_preset = props.presets.add()
|
||||
new_preset.name = final_name
|
||||
new_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE'
|
||||
else:
|
||||
existing_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE'
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"{preset_name}: {str(e)}")
|
||||
|
||||
# Report results
|
||||
if imported_count > 0:
|
||||
storage_location = "blend file" if self.import_to_blend else "persistent storage"
|
||||
self.report({'INFO'}, f"✅ Imported {imported_count} presets to {storage_location}")
|
||||
if skipped_count > 0:
|
||||
self.report({'INFO'}, f"⏭️ Skipped {skipped_count} existing presets")
|
||||
else:
|
||||
self.report({'WARNING'}, "No presets were imported")
|
||||
|
||||
if errors:
|
||||
self.report({'WARNING'}, f"Errors importing {len(errors)} presets - check console")
|
||||
for error in errors[:5]: # Show first 5 errors
|
||||
print(f"[TTG] Import error: {error}")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Failed to import presets: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "import_mode")
|
||||
layout.prop(self, "import_to_blend")
|
||||
|
||||
class TEXT_TEXTURE_OT_show_migration_report(Operator):
|
||||
"""Show migration report with backup and upgrade information"""
|
||||
bl_idname = "text_texture.show_migration_report"
|
||||
bl_label = "Show Migration Report"
|
||||
bl_description = "Display information about preset migration and backups"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Migration Report", icon='INFO')
|
||||
layout.separator()
|
||||
layout.label(text="This addon has been upgraded to the new preset system.")
|
||||
layout.label(text="Your existing presets have been preserved.")
|
||||
layout.separator()
|
||||
layout.label(text="Features:", icon='CHECKMARK')
|
||||
layout.label(text="• Presets saved in blend files")
|
||||
layout.label(text="• Cross-file preset sharing")
|
||||
layout.label(text="• Automatic backup system")
|
||||
layout.separator()
|
||||
|
||||
# Export all operator classes for wildcard imports
|
||||
__all__ = [
|
||||
'TEXT_TEXTURE_OT_save_preset',
|
||||
'TEXT_TEXTURE_OT_save_preset_enter',
|
||||
'TEXT_TEXTURE_OT_load_preset',
|
||||
'TEXT_TEXTURE_OT_delete_preset',
|
||||
'TEXT_TEXTURE_OT_refresh_presets',
|
||||
'TEXT_TEXTURE_OT_export_presets',
|
||||
'TEXT_TEXTURE_OT_import_presets',
|
||||
'TEXT_TEXTURE_OT_show_migration_report',
|
||||
]
|
||||
607
operators/utility_ops.py
Normal file
607
operators/utility_ops.py
Normal file
@@ -0,0 +1,607 @@
|
||||
"""
|
||||
Text Texture Generator - Utility Operators
|
||||
Handles preview refresh, font refresh, UI operations, overlay management, and positioning utilities.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty, IntProperty, EnumProperty
|
||||
import os
|
||||
|
||||
# Import functions from parent module
|
||||
try:
|
||||
print("[TTG DEBUG] IMPORT TRACE: Attempting to import from ui.preview...")
|
||||
print("[TTG DEBUG] IMPORT TRACE: Looking for generate_preview in ui.preview module")
|
||||
|
||||
# First, let's see what's actually available in ui.preview
|
||||
from .. import ui
|
||||
print(f"[TTG DEBUG] IMPORT TRACE: ui.preview module contents: {dir(ui.preview)}")
|
||||
|
||||
# Now try the actual import that's failing
|
||||
from ..core.generation_engine import generate_preview
|
||||
print("[TTG DEBUG] IMPORT TRACE: SUCCESS - generate_preview imported from ui.preview")
|
||||
from ..utils.system import get_system_fonts, get_font_enum_items
|
||||
from ..presets.storage_system import refresh_presets_sync, ensure_presets_available
|
||||
from ..utils.constants import sync_margin_values
|
||||
except ImportError as e:
|
||||
print(f"[TTG DEBUG] IMPORT ERROR: Failed to import from ui.preview: {e}")
|
||||
print("[TTG DEBUG] IMPORT TRACE: This confirms generate_preview is NOT in ui.preview")
|
||||
|
||||
# Let's check if it's available in core.generation_engine
|
||||
try:
|
||||
print("[TTG DEBUG] IMPORT TRACE: Attempting fallback import from core.generation_engine...")
|
||||
from ..core.generation_engine import generate_preview
|
||||
print("[TTG DEBUG] IMPORT TRACE: SUCCESS - generate_preview found in core.generation_engine!")
|
||||
from ..utils.system import get_system_fonts, get_font_enum_items
|
||||
from ..presets.storage_system import refresh_presets_sync, ensure_presets_available
|
||||
from ..utils.constants import sync_margin_values
|
||||
print("[TTG DEBUG] IMPORT TRACE: All imports successful with corrected path")
|
||||
except ImportError as fallback_error:
|
||||
print(f"[TTG DEBUG] IMPORT ERROR: Fallback import also failed: {fallback_error}")
|
||||
print(f"[TTG DEBUG] IMPORT ERROR: Original error was: {e}")
|
||||
# Fallback imports for development/testing
|
||||
print("[TTG DEBUG] IMPORT TRACE: Using fallback placeholder functions")
|
||||
def generate_preview(context):
|
||||
print("Fallback generate_preview called")
|
||||
return None
|
||||
def get_system_fonts():
|
||||
return {}
|
||||
def get_font_enum_items():
|
||||
return [("default", "Default Font", "Use system default font", 0)]
|
||||
def refresh_presets_sync():
|
||||
return True
|
||||
def ensure_presets_available():
|
||||
return True
|
||||
def sync_margin_values(props, changed_margin):
|
||||
pass
|
||||
|
||||
|
||||
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'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_view_preview_zoom(Operator):
|
||||
"""Open preview in new Image Editor window with zoom support"""
|
||||
bl_idname = "text_texture.view_preview_zoom"
|
||||
bl_label = "Enable Zoom"
|
||||
bl_description = "Open preview in new Image Editor window with zoom and pan controls"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
# Generate preview image when button is clicked
|
||||
print("[TTG] Generating preview on button click...")
|
||||
result = generate_preview(context)
|
||||
|
||||
if not result:
|
||||
self.report({'ERROR'}, "Failed to generate preview")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not props.preview_image:
|
||||
self.report({'ERROR'}, "No preview image to display")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Open a new window with Image Editor
|
||||
print(f"[ZOOM] Opening new window for preview image: {props.preview_image.name}")
|
||||
|
||||
try:
|
||||
# Create a new window
|
||||
bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
|
||||
|
||||
# Wait a moment and then change the newly opened window to Image Editor
|
||||
def setup_image_window():
|
||||
# Find the newest window (should be the one we just opened)
|
||||
newest_window = None
|
||||
for window in bpy.context.window_manager.windows:
|
||||
if window.screen.name.startswith("temp"):
|
||||
newest_window = window
|
||||
break
|
||||
|
||||
if not newest_window:
|
||||
# Fallback: use any non-main window
|
||||
for window in bpy.context.window_manager.windows:
|
||||
if window != bpy.context.window:
|
||||
newest_window = window
|
||||
break
|
||||
|
||||
if newest_window:
|
||||
# Set the area type to Image Editor
|
||||
for area in newest_window.screen.areas:
|
||||
if area.type == 'PREFERENCES':
|
||||
area.type = 'IMAGE_EDITOR'
|
||||
print("[ZOOM] Changed area to Image Editor")
|
||||
|
||||
# Set the preview image
|
||||
for space in area.spaces:
|
||||
if space.type == 'IMAGE_EDITOR':
|
||||
space.image = props.preview_image
|
||||
print(f"[ZOOM] Set preview image: {props.preview_image.name}")
|
||||
|
||||
# Set up proper viewing
|
||||
override = {
|
||||
'window': newest_window,
|
||||
'screen': newest_window.screen,
|
||||
'area': area,
|
||||
'region': area.regions[0] if area.regions else None
|
||||
}
|
||||
|
||||
with bpy.context.temp_override(**override):
|
||||
try:
|
||||
bpy.ops.image.view_all(fit_view=True)
|
||||
print("[ZOOM] Applied fit to view")
|
||||
except Exception as e:
|
||||
print(f"[ZOOM] Error applying fit to view: {e}")
|
||||
|
||||
break
|
||||
break
|
||||
|
||||
self.report({'INFO'}, "✅ NEW WINDOW OPENED! Use mouse wheel to zoom, middle-mouse to pan")
|
||||
print("[ZOOM] New window zoom feature activated successfully")
|
||||
else:
|
||||
print("[ZOOM] Could not find new window")
|
||||
self.report({'ERROR'}, "Failed to setup new window")
|
||||
|
||||
return None # Stop timer
|
||||
|
||||
# Use a timer to set up the window after it opens
|
||||
bpy.app.timers.register(setup_image_window, first_interval=0.1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ZOOM] Error creating new window: {e}")
|
||||
# Fallback to old behavior if new window creation fails
|
||||
self.report({'WARNING'}, "New window failed, trying area split instead")
|
||||
|
||||
# Find any area to split
|
||||
for area in context.screen.areas:
|
||||
if area.type in ['NODE_EDITOR', 'VIEW_3D', 'PROPERTIES']:
|
||||
print(f"[ZOOM] Fallback: Splitting {area.type} to create Image Editor")
|
||||
|
||||
# Split horizontally to create a new area
|
||||
with context.temp_override(area=area):
|
||||
bpy.ops.screen.area_split(direction='HORIZONTAL', factor=0.6)
|
||||
|
||||
# Find the newly created area
|
||||
for new_area in context.screen.areas:
|
||||
if new_area != area and new_area.y != area.y:
|
||||
new_area.type = 'IMAGE_EDITOR'
|
||||
|
||||
# Set the image in the Image Editor
|
||||
for space in new_area.spaces:
|
||||
if space.type == 'IMAGE_EDITOR':
|
||||
space.image = props.preview_image
|
||||
print("[ZOOM] Fallback: Image set in split Image Editor")
|
||||
break
|
||||
|
||||
# Tag for redraw and fit to view
|
||||
new_area.tag_redraw()
|
||||
override = context.copy()
|
||||
override['area'] = new_area
|
||||
override['region'] = new_area.regions[0]
|
||||
|
||||
with context.temp_override(**override):
|
||||
try:
|
||||
bpy.ops.image.view_all(fit_view=True)
|
||||
print("[ZOOM] Fallback: Fit to view applied")
|
||||
except Exception as e:
|
||||
print(f"[ZOOM] Fallback: Error applying fit to view: {e}")
|
||||
|
||||
self.report({'INFO'}, "✅ ZOOM ENABLED! Use mouse wheel to zoom, middle-mouse to pan")
|
||||
return {'FINISHED'}
|
||||
break
|
||||
|
||||
self.report({'ERROR'}, "Failed to create Image Editor")
|
||||
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'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_refresh_presets(Operator):
|
||||
"""Refresh presets from blend file and local storage"""
|
||||
bl_idname = "text_texture.refresh_presets"
|
||||
bl_label = "Refresh Presets"
|
||||
bl_description = "Reload presets from blend file and local storage"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
# Use the reliable synchronous refresh system
|
||||
success = refresh_presets_sync()
|
||||
|
||||
if success:
|
||||
props = context.scene.text_texture_props
|
||||
total_presets = len(props.presets)
|
||||
|
||||
# Count by source for detailed reporting
|
||||
blend_count = len([p for p in props.presets if p.source == 'BLEND_FILE'])
|
||||
persistent_count = len([p for p in props.presets if p.source == 'PERSISTENT_FILE'])
|
||||
local_count = len([p for p in props.presets if p.source == 'LOCAL_FILE'])
|
||||
|
||||
self.report({'INFO'}, f"✅ Refreshed {total_presets} presets ({blend_count} blend file, {persistent_count} persistent, {local_count} local)")
|
||||
else:
|
||||
self.report({'WARNING'}, "Preset refresh completed with issues - check console for details")
|
||||
|
||||
# Force UI redraw
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Failed to refresh presets: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_add_overlay(Operator):
|
||||
"""Add new image overlay"""
|
||||
bl_idname = "text_texture.add_overlay"
|
||||
bl_label = "Add Overlay"
|
||||
bl_description = "Add new image overlay"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
overlay = props.image_overlays.add()
|
||||
overlay.name = f"Overlay {len(props.image_overlays)}"
|
||||
props.active_overlay_index = len(props.image_overlays) - 1
|
||||
|
||||
# Force UI redraw
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
self.report({'INFO'}, f"Added overlay: {overlay.name}")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_remove_overlay(Operator):
|
||||
"""Remove active image overlay"""
|
||||
bl_idname = "text_texture.remove_overlay"
|
||||
bl_label = "Remove Overlay"
|
||||
bl_description = "Remove active image overlay"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
if props.image_overlays and props.active_overlay_index < len(props.image_overlays):
|
||||
overlay_name = props.image_overlays[props.active_overlay_index].name
|
||||
props.image_overlays.remove(props.active_overlay_index)
|
||||
|
||||
# Update active index
|
||||
if props.active_overlay_index >= len(props.image_overlays) and props.image_overlays:
|
||||
props.active_overlay_index = len(props.image_overlays) - 1
|
||||
|
||||
# Force UI redraw
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
self.report({'INFO'}, f"Removed overlay: {overlay_name}")
|
||||
else:
|
||||
self.report({'WARNING'}, "No overlay to remove")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_set_anchor_point(Operator):
|
||||
"""Set anchor point for text positioning"""
|
||||
bl_idname = "text_texture.set_anchor_point"
|
||||
bl_label = "Set Anchor Point"
|
||||
bl_description = "Set text anchor position"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
anchor_point: StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.text_texture_props
|
||||
props.anchor_point = self.anchor_point
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_sync_margins(Operator):
|
||||
"""Sync margin values when linked"""
|
||||
bl_idname = "text_texture.sync_margins"
|
||||
bl_label = "Sync Margins"
|
||||
bl_description = "Synchronize margin values"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
margin_type: StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.text_texture_props
|
||||
sync_margin_values(props, self.margin_type)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_duplicate_overlay(Operator):
|
||||
"""Duplicate image overlay"""
|
||||
bl_idname = "text_texture.duplicate_overlay"
|
||||
bl_label = "Duplicate Overlay"
|
||||
bl_description = "Duplicate this image overlay"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
overlay_index: IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
if 0 <= self.overlay_index < len(props.image_overlays):
|
||||
source_overlay = props.image_overlays[self.overlay_index]
|
||||
|
||||
# Debug logging
|
||||
print(f"[DUPLICATE DEBUG] Duplicating overlay '{source_overlay.name}'")
|
||||
print(f"[DUPLICATE DEBUG] Source properties:")
|
||||
print(f"[DUPLICATE DEBUG] image_path: '{source_overlay.image_path}'")
|
||||
print(f"[DUPLICATE DEBUG] enabled: {source_overlay.enabled}")
|
||||
print(f"[DUPLICATE DEBUG] positioning_mode: '{source_overlay.positioning_mode}'")
|
||||
print(f"[DUPLICATE DEBUG] scale: {source_overlay.scale}")
|
||||
print(f"[DUPLICATE DEBUG] z_index: {source_overlay.z_index}")
|
||||
|
||||
new_overlay = props.image_overlays.add()
|
||||
|
||||
# Copy all properties with explicit verification
|
||||
new_overlay.name = f"{source_overlay.name} Copy"
|
||||
new_overlay.image_path = source_overlay.image_path
|
||||
new_overlay.x_position = source_overlay.x_position
|
||||
new_overlay.y_position = source_overlay.y_position
|
||||
new_overlay.scale = source_overlay.scale
|
||||
new_overlay.rotation = source_overlay.rotation
|
||||
# Fix z-index assignment for duplicated overlays to ensure proper visibility
|
||||
# For PREPEND/APPEND overlays, increment z_index to ensure duplicate appears above original
|
||||
# This prevents the duplicate from being rendered behind the original overlay
|
||||
if source_overlay.positioning_mode in ['PREPEND', 'APPEND']:
|
||||
# Increment z_index to ensure proper layering order for duplicated overlay
|
||||
new_overlay.z_index = min(source_overlay.z_index + 1, 10) # Cap at max z_index value
|
||||
print(f"[DUPLICATE DEBUG] Fixed z_index for {source_overlay.positioning_mode} overlay: {source_overlay.z_index} -> {new_overlay.z_index}")
|
||||
else:
|
||||
# For ABSOLUTE overlays, keep same z_index (different positions so no overlap issue)
|
||||
new_overlay.z_index = source_overlay.z_index
|
||||
new_overlay.positioning_mode = source_overlay.positioning_mode
|
||||
new_overlay.text_spacing = source_overlay.text_spacing
|
||||
new_overlay.image_spacing = source_overlay.image_spacing
|
||||
new_overlay.enabled = source_overlay.enabled
|
||||
|
||||
# Verify the copy was successful
|
||||
print(f"[DUPLICATE DEBUG] New overlay properties:")
|
||||
print(f"[DUPLICATE DEBUG] name: '{new_overlay.name}'")
|
||||
print(f"[DUPLICATE DEBUG] image_path: '{new_overlay.image_path}'")
|
||||
print(f"[DUPLICATE DEBUG] enabled: {new_overlay.enabled}")
|
||||
print(f"[DUPLICATE DEBUG] positioning_mode: '{new_overlay.positioning_mode}'")
|
||||
print(f"[DUPLICATE DEBUG] scale: {new_overlay.scale}")
|
||||
print(f"[DUPLICATE DEBUG] z_index: {new_overlay.z_index}")
|
||||
|
||||
# Move to position after source
|
||||
new_index = len(props.image_overlays) - 1
|
||||
target_index = min(self.overlay_index + 1, new_index)
|
||||
|
||||
if new_index != target_index:
|
||||
props.image_overlays.move(new_index, target_index)
|
||||
print(f"[DUPLICATE DEBUG] Moved overlay from index {new_index} to {target_index}")
|
||||
|
||||
props.active_overlay_index = target_index
|
||||
|
||||
# Clear any cached images to force reload
|
||||
print(f"[DUPLICATE DEBUG] Clearing image cache and forcing preview update...")
|
||||
|
||||
# Set the updating flag to prevent recursive updates
|
||||
props.is_updating = True
|
||||
|
||||
try:
|
||||
# Force immediate preview regeneration
|
||||
print(f"[DUPLICATE DEBUG] Generating new preview...")
|
||||
preview_result = generate_preview(context)
|
||||
if preview_result:
|
||||
print(f"[DUPLICATE DEBUG] Preview generated successfully")
|
||||
else:
|
||||
print(f"[DUPLICATE DEBUG] Preview generation failed")
|
||||
|
||||
# Force UI redraw
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DUPLICATE DEBUG] Error updating preview: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
props.is_updating = False
|
||||
|
||||
print(f"[DUPLICATE DEBUG] Total overlays now: {len(props.image_overlays)}")
|
||||
print(f"[DUPLICATE DEBUG] Duplication operation completed")
|
||||
|
||||
self.report({'INFO'}, f"✅ Duplicated overlay: {source_overlay.name}")
|
||||
else:
|
||||
self.report({'ERROR'}, "Invalid overlay index")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_delete_overlay(Operator):
|
||||
"""Delete image overlay"""
|
||||
bl_idname = "text_texture.delete_overlay"
|
||||
bl_label = "Delete Overlay"
|
||||
bl_description = "Delete this image overlay"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
overlay_index: IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
print(f"[DELETE DEBUG] Deleting overlay at index {self.overlay_index}")
|
||||
|
||||
if 0 <= self.overlay_index < len(props.image_overlays):
|
||||
overlay_name = props.image_overlays[self.overlay_index].name
|
||||
print(f"[DELETE DEBUG] Deleting overlay: '{overlay_name}'")
|
||||
|
||||
props.image_overlays.remove(self.overlay_index)
|
||||
|
||||
# Update active index
|
||||
if props.active_overlay_index >= len(props.image_overlays) and props.image_overlays:
|
||||
props.active_overlay_index = len(props.image_overlays) - 1
|
||||
elif not props.image_overlays:
|
||||
props.active_overlay_index = 0
|
||||
|
||||
print(f"[DELETE DEBUG] Overlay deleted successfully. Total overlays now: {len(props.image_overlays)}")
|
||||
|
||||
# Force preview update after deletion
|
||||
try:
|
||||
generate_preview(context)
|
||||
print(f"[DELETE DEBUG] Preview updated after deletion")
|
||||
except Exception as e:
|
||||
print(f"[DELETE DEBUG] Error updating preview: {e}")
|
||||
|
||||
# Force UI redraw
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
self.report({'INFO'}, f"Deleted overlay: {overlay_name}")
|
||||
else:
|
||||
print(f"[DELETE DEBUG] Invalid overlay index: {self.overlay_index}")
|
||||
self.report({'ERROR'}, "Invalid overlay index")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class TEXT_TEXTURE_OT_move_overlay(Operator):
|
||||
"""Move image overlay up or down in the list"""
|
||||
bl_idname = "text_texture.move_overlay"
|
||||
bl_label = "Move Overlay"
|
||||
bl_description = "Move overlay up or down in the list"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
overlay_index: IntProperty()
|
||||
direction: EnumProperty(
|
||||
items=[
|
||||
('UP', 'Up', 'Move overlay up in list'),
|
||||
('DOWN', 'Down', 'Move overlay down in list')
|
||||
]
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
props = context.scene.text_texture_props
|
||||
|
||||
print(f"[MOVE DEBUG] Moving overlay at index {self.overlay_index} direction: {self.direction}")
|
||||
|
||||
if 0 <= self.overlay_index < len(props.image_overlays):
|
||||
current_index = self.overlay_index
|
||||
new_index = current_index
|
||||
|
||||
if self.direction == 'UP' and current_index > 0:
|
||||
new_index = current_index - 1
|
||||
elif self.direction == 'DOWN' and current_index < len(props.image_overlays) - 1:
|
||||
new_index = current_index + 1
|
||||
|
||||
if new_index != current_index:
|
||||
overlay_name = props.image_overlays[current_index].name
|
||||
props.image_overlays.move(current_index, new_index)
|
||||
props.active_overlay_index = new_index
|
||||
|
||||
print(f"[MOVE DEBUG] Successfully moved overlay '{overlay_name}' from {current_index} to {new_index}")
|
||||
|
||||
# Force preview update after moving (order affects rendering)
|
||||
try:
|
||||
generate_preview(context)
|
||||
print(f"[MOVE DEBUG] Preview updated after move operation")
|
||||
except Exception as e:
|
||||
print(f"[MOVE DEBUG] Error updating preview: {e}")
|
||||
|
||||
self.report({'INFO'}, f"Moved overlay {self.direction.lower()}")
|
||||
else:
|
||||
print(f"[MOVE DEBUG] No movement needed - overlay is already at edge")
|
||||
self.report({'INFO'}, f"Overlay is already at {self.direction.lower()} edge")
|
||||
else:
|
||||
print(f"[MOVE DEBUG] Invalid overlay index: {self.overlay_index}")
|
||||
self.report({'ERROR'}, "Invalid overlay index")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
# Export all operator classes for wildcard imports
|
||||
__all__ = [
|
||||
'TEXT_TEXTURE_OT_refresh_preview',
|
||||
'TEXT_TEXTURE_OT_view_preview_zoom',
|
||||
'TEXT_TEXTURE_OT_open_panel',
|
||||
'TEXT_TEXTURE_OT_refresh_fonts',
|
||||
'TEXT_TEXTURE_OT_refresh_presets',
|
||||
'TEXT_TEXTURE_OT_add_overlay',
|
||||
'TEXT_TEXTURE_OT_remove_overlay',
|
||||
'TEXT_TEXTURE_OT_set_anchor_point',
|
||||
'TEXT_TEXTURE_OT_sync_margins',
|
||||
'TEXT_TEXTURE_OT_duplicate_overlay',
|
||||
'TEXT_TEXTURE_OT_delete_overlay',
|
||||
'TEXT_TEXTURE_OT_move_overlay',
|
||||
]
|
||||
12
presets/__init__.py
Normal file
12
presets/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Preset System Module
|
||||
|
||||
This module contains the three-tier preset storage system for the Text Texture Generator:
|
||||
- Blend file storage (highest priority)
|
||||
- Persistent file storage (survives addon updates)
|
||||
- Legacy file storage (backward compatibility)
|
||||
"""
|
||||
|
||||
# Import all preset functionality
|
||||
from .storage_system import *
|
||||
from .migration import *
|
||||
125
presets/migration.py
Normal file
125
presets/migration.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# Migration functions for preset storage system
|
||||
import bpy
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
def migrate_presets_if_needed():
|
||||
"""
|
||||
Migrate presets from older versions if needed.
|
||||
This function checks if migration is required and performs it automatically.
|
||||
Returns: dict with 'migrated_count' key indicating number of presets migrated.
|
||||
"""
|
||||
print("[TTG MIGRATION DEBUG] Starting migrate_presets_if_needed()")
|
||||
preferences = bpy.context.preferences.addons.get("Text_Texture_Generator_Pro")
|
||||
if not preferences:
|
||||
print("[TTG MIGRATION DEBUG] No preferences found - returning proper dict format")
|
||||
return {'migrated_count': 0}
|
||||
|
||||
addon_version = preferences.preferences.get("addon_version", "0.0.0")
|
||||
current_version = "1.0.0" # This should match the addon's current version
|
||||
|
||||
if addon_version != current_version:
|
||||
print(f"Text Texture Generator: Migrating presets from version {addon_version} to {current_version}")
|
||||
|
||||
# Backup existing presets before migration
|
||||
backup_success = backup_presets()
|
||||
if not backup_success:
|
||||
print("Text Texture Generator: Warning - Failed to backup presets before migration")
|
||||
|
||||
# Perform migration steps based on version differences
|
||||
migration_success = True
|
||||
migrated_count = 0
|
||||
|
||||
try:
|
||||
# Version-specific migration logic would go here
|
||||
# For now, we'll just update the version number
|
||||
preferences.preferences["addon_version"] = current_version
|
||||
|
||||
# In a real migration, this would be the actual count of migrated presets
|
||||
# For now, we assume 1 migration operation (version update)
|
||||
migrated_count = 1
|
||||
|
||||
if migration_success:
|
||||
print(f"Text Texture Generator: Successfully migrated presets to version {current_version}")
|
||||
else:
|
||||
print("Text Texture Generator: Migration completed with warnings")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Text Texture Generator: Error during preset migration: {str(e)}")
|
||||
migration_success = False
|
||||
migrated_count = 0
|
||||
|
||||
print(f"[TTG MIGRATION DEBUG] Migration completed, returning proper dict format: migrated_count={migrated_count}")
|
||||
return {'migrated_count': migrated_count}
|
||||
|
||||
print("[TTG MIGRATION DEBUG] No migration needed, returning proper dict format: migrated_count=0")
|
||||
return {'migrated_count': 0} # No migration needed
|
||||
|
||||
def backup_presets():
|
||||
"""
|
||||
Create a backup of current presets before migration.
|
||||
Returns True if backup was successful, False otherwise.
|
||||
"""
|
||||
try:
|
||||
# Get the addon directory
|
||||
addon_dir = os.path.dirname(__file__)
|
||||
parent_dir = os.path.dirname(addon_dir)
|
||||
|
||||
# Create backup directory with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_dir = os.path.join(parent_dir, f"preset_backup_{timestamp}")
|
||||
|
||||
# Find preset files to backup
|
||||
preset_files = []
|
||||
|
||||
# Check for blend file presets
|
||||
if bpy.data.filepath:
|
||||
blend_file_dir = os.path.dirname(bpy.data.filepath)
|
||||
preset_file = os.path.join(blend_file_dir, ".text_texture_presets.json")
|
||||
if os.path.exists(preset_file):
|
||||
preset_files.append(preset_file)
|
||||
|
||||
# Check for persistent presets
|
||||
preferences = bpy.context.preferences.addons.get("Text_Texture_Generator_Pro")
|
||||
if preferences and hasattr(preferences.preferences, 'persistent_presets_path'):
|
||||
persistent_path = preferences.preferences.persistent_presets_path
|
||||
if persistent_path and os.path.exists(persistent_path):
|
||||
preset_files.append(persistent_path)
|
||||
|
||||
# Check for legacy presets in addon directory
|
||||
legacy_preset_file = os.path.join(parent_dir, "text_texture_presets.json")
|
||||
if os.path.exists(legacy_preset_file):
|
||||
preset_files.append(legacy_preset_file)
|
||||
|
||||
# Create backup if there are files to backup
|
||||
if preset_files:
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
for preset_file in preset_files:
|
||||
filename = os.path.basename(preset_file)
|
||||
backup_path = os.path.join(backup_dir, filename)
|
||||
shutil.copy2(preset_file, backup_path)
|
||||
print(f"Text Texture Generator: Backed up {filename} to {backup_dir}")
|
||||
|
||||
# Create a backup info file
|
||||
backup_info = {
|
||||
"backup_timestamp": timestamp,
|
||||
"addon_version_before_migration": bpy.context.preferences.addons.get("Text_Texture_Generator_Pro", {}).get("addon_version", "unknown"),
|
||||
"files_backed_up": [os.path.basename(f) for f in preset_files]
|
||||
}
|
||||
|
||||
info_file = os.path.join(backup_dir, "backup_info.json")
|
||||
with open(info_file, 'w') as f:
|
||||
json.dump(backup_info, f, indent=2)
|
||||
|
||||
print(f"Text Texture Generator: Created preset backup in {backup_dir}")
|
||||
return True
|
||||
else:
|
||||
print("Text Texture Generator: No preset files found to backup")
|
||||
return True # Not an error if there are no presets to backup
|
||||
|
||||
except Exception as e:
|
||||
print(f"Text Texture Generator: Error creating preset backup: {str(e)}")
|
||||
return False
|
||||
462
presets/storage_system.py
Normal file
462
presets/storage_system.py
Normal file
@@ -0,0 +1,462 @@
|
||||
import bpy
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import platform
|
||||
|
||||
# ============================================================================
|
||||
# PRESET STORAGE SYSTEM - V2.0 (PERSISTENT STORAGE)
|
||||
# ============================================================================
|
||||
#
|
||||
# The Text Texture Generator now uses a THREE-TIER preset storage system
|
||||
# that ensures preset persistence across addon updates:
|
||||
#
|
||||
# 1. BLEND FILE STORAGE (Highest Priority)
|
||||
# - Presets travel with the .blend file
|
||||
# - Perfect for project-specific presets
|
||||
# - Stored in scene custom properties
|
||||
#
|
||||
# 2. PERSISTENT FILE STORAGE (Medium Priority)
|
||||
# - Survives addon updates and Blender upgrades
|
||||
# - Stored in Blender's user config directory
|
||||
# - Shared across all blend files
|
||||
#
|
||||
# 3. LEGACY FILE STORAGE (Lowest Priority)
|
||||
# - Old system for backward compatibility
|
||||
# - Vulnerable to addon updates
|
||||
# - Will be automatically migrated to persistent storage
|
||||
#
|
||||
# MIGRATION SYSTEM:
|
||||
# - Automatically detects addon version changes
|
||||
# - Migrates legacy presets to persistent storage
|
||||
# - Creates backups before migration
|
||||
# - Provides detailed migration reports
|
||||
#
|
||||
# IMPORT/EXPORT SYSTEM:
|
||||
# - Export all presets for manual backup
|
||||
# - Import presets with conflict resolution
|
||||
# - Supports cross-machine preset sharing
|
||||
#
|
||||
# ============================================================================
|
||||
|
||||
def get_preset_path():
|
||||
"""Get path for storing presets (legacy local storage)
|
||||
|
||||
LEGACY SYSTEM: This storage location is vulnerable to addon updates.
|
||||
Presets stored here will be lost when the addon is updated through
|
||||
Blender's Add-ons preferences. This function is maintained for
|
||||
backward compatibility only.
|
||||
|
||||
For new installations, use get_persistent_preset_path() instead.
|
||||
"""
|
||||
addon_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
presets_dir = os.path.join(addon_dir, "presets")
|
||||
if not os.path.exists(presets_dir):
|
||||
try:
|
||||
os.makedirs(presets_dir)
|
||||
except OSError as e:
|
||||
print(f"Error creating presets directory: {e}")
|
||||
return presets_dir
|
||||
|
||||
def get_persistent_preset_path():
|
||||
"""Get persistent path for storing presets (survives addon updates)"""
|
||||
import bpy
|
||||
|
||||
# Get Blender's user config directory
|
||||
config_dir = bpy.utils.user_resource('CONFIG')
|
||||
if not config_dir:
|
||||
# Fallback to addon directory if config path unavailable
|
||||
print("[TTG] Warning: Could not get user config directory, falling back to addon directory")
|
||||
return get_preset_path()
|
||||
|
||||
# Create addon-specific subdirectory
|
||||
persistent_dir = os.path.join(config_dir, "text_texture_generator", "presets")
|
||||
|
||||
try:
|
||||
if not os.path.exists(persistent_dir):
|
||||
os.makedirs(persistent_dir, exist_ok=True)
|
||||
return persistent_dir
|
||||
except OSError as e:
|
||||
print(f"[TTG] Error creating persistent presets directory: {e}")
|
||||
# Fallback to addon directory
|
||||
return get_preset_path()
|
||||
|
||||
def get_addon_version():
|
||||
"""Get current addon version for migration tracking"""
|
||||
# Import bl_info from the main addon file
|
||||
import sys
|
||||
import importlib.util
|
||||
|
||||
# Get the main addon module
|
||||
main_module_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
init_file = os.path.join(main_module_path, "__init__.py")
|
||||
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("main_addon", init_file)
|
||||
main_addon = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(main_addon)
|
||||
return main_addon.bl_info["version"]
|
||||
except Exception as e:
|
||||
print(f"[TTG] Error getting addon version: {e}")
|
||||
return (2, 3, 2) # Fallback version
|
||||
|
||||
def get_version_file_path():
|
||||
"""Get path to version tracking file"""
|
||||
persistent_dir = os.path.dirname(get_persistent_preset_path())
|
||||
return os.path.join(persistent_dir, "addon_version.json")
|
||||
|
||||
def get_stored_version():
|
||||
"""Get previously stored addon version"""
|
||||
version_file = get_version_file_path()
|
||||
try:
|
||||
if os.path.exists(version_file):
|
||||
with open(version_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
return tuple(data.get('version', (0, 0, 0)))
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"[TTG] Error reading version file: {e}")
|
||||
return (0, 0, 0)
|
||||
|
||||
def save_current_version():
|
||||
"""Save current addon version to persistent storage"""
|
||||
version_file = get_version_file_path()
|
||||
try:
|
||||
version_data = {
|
||||
'version': get_addon_version(),
|
||||
'timestamp': time.time()
|
||||
}
|
||||
with open(version_file, 'w') as f:
|
||||
json.dump(version_data, f, indent=2)
|
||||
print(f"[TTG] Saved addon version {get_addon_version()} to persistent storage")
|
||||
except (OSError, json.JSONEncodeError) as e:
|
||||
print(f"[TTG] Error saving version file: {e}")
|
||||
|
||||
def get_blend_file_presets():
|
||||
"""Get presets stored in the current blend file"""
|
||||
try:
|
||||
# Get custom properties from the scene
|
||||
import bpy
|
||||
scene = bpy.context.scene
|
||||
|
||||
# DETAILED LOGGING: Function entry
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] ==================== LOADING FROM BLEND FILE ==================")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Function called to get presets from blend file")
|
||||
|
||||
# Store presets in scene custom properties
|
||||
if "text_texture_presets" not in scene:
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] No 'text_texture_presets' found in scene, creating empty")
|
||||
scene["text_texture_presets"] = "{}"
|
||||
else:
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Found 'text_texture_presets' in scene")
|
||||
|
||||
preset_data = scene.get("text_texture_presets", "{}")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data type: {type(preset_data)}")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data length: {len(str(preset_data))} characters")
|
||||
|
||||
if isinstance(preset_data, str):
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is string, attempting JSON parse...")
|
||||
try:
|
||||
import json
|
||||
parsed_presets = json.loads(preset_data)
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Successfully parsed JSON")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets type: {type(parsed_presets)}")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets count: {len(parsed_presets) if isinstance(parsed_presets, dict) else 'N/A'}")
|
||||
|
||||
if isinstance(parsed_presets, dict):
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset names: {list(parsed_presets.keys())}")
|
||||
|
||||
# DETAILED VERIFICATION: Check shader properties in each preset
|
||||
for preset_name, preset_content in parsed_presets.items():
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset '{preset_name}' analysis:")
|
||||
if isinstance(preset_content, dict):
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Keys: {list(preset_content.keys())}")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_shadows: {preset_content.get('disable_shadows', 'NOT FOUND')}")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_reflections: {preset_content.get('disable_reflections', 'NOT FOUND')}")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_backfacing: {preset_content.get('disable_backfacing', 'NOT FOUND')}")
|
||||
else:
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] ERROR: Preset content is not a dict: {type(preset_content)}")
|
||||
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Returning parsed presets")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================")
|
||||
return parsed_presets
|
||||
|
||||
except json.JSONDecodeError as json_err:
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] JSON decode error: {json_err}")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw data causing error: {preset_data[:200]}...") # First 200 chars
|
||||
print("Error parsing blend file presets, resetting to empty")
|
||||
scene["text_texture_presets"] = "{}"
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Reset to empty, returning empty dict")
|
||||
return {}
|
||||
else:
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is not string, returning empty dict")
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] EXCEPTION occurred: {e}")
|
||||
import traceback
|
||||
print(f"[GET_BLEND_FILE_PRESETS DEBUG] Full traceback: {traceback.format_exc()}")
|
||||
print(f"Error getting blend file presets: {e}")
|
||||
return {}
|
||||
|
||||
def save_blend_file_preset(preset_name, preset_data):
|
||||
"""Save a preset to the current blend file"""
|
||||
try:
|
||||
import bpy
|
||||
import json
|
||||
scene = bpy.context.scene
|
||||
|
||||
# DETAILED LOGGING: Function entry
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ==================== SAVING TO BLEND FILE ===================")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Function called with preset_name: '{preset_name}'")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data type: {type(preset_data)}")
|
||||
if isinstance(preset_data, dict):
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data keys: {list(preset_data.keys())}")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Shader properties in preset_data:")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
|
||||
else:
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: preset_data is not a dictionary!")
|
||||
|
||||
# Get existing presets
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Getting existing presets from blend file...")
|
||||
presets = get_blend_file_presets()
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing presets count: {len(presets)}")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing preset names: {list(presets.keys())}")
|
||||
|
||||
# Add or update the preset
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Adding/updating preset '{preset_name}'...")
|
||||
presets[preset_name] = preset_data
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Presets now contains {len(presets)} items")
|
||||
|
||||
# VERIFY: Check that shader properties are in the preset we're about to save
|
||||
if preset_name in presets and isinstance(presets[preset_name], dict):
|
||||
saved_preset = presets[preset_name]
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] VERIFICATION - Shader properties in final preset:")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {saved_preset.get('disable_shadows', 'NOT FOUND')}")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {saved_preset.get('disable_reflections', 'NOT FOUND')}")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {saved_preset.get('disable_backfacing', 'NOT FOUND')}")
|
||||
|
||||
# Save back to scene custom properties
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Converting presets to JSON...")
|
||||
json_data = json.dumps(presets, indent=2)
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] JSON data length: {len(json_data)} characters")
|
||||
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Saving to scene custom properties...")
|
||||
scene["text_texture_presets"] = json_data
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Successfully saved to scene['text_texture_presets']")
|
||||
|
||||
# FINAL VERIFICATION: Read back from scene to confirm save
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] FINAL VERIFICATION - Reading back from scene...")
|
||||
readback_data = scene.get("text_texture_presets", "{}")
|
||||
if readback_data:
|
||||
try:
|
||||
readback_presets = json.loads(readback_data)
|
||||
if preset_name in readback_presets:
|
||||
readback_preset = readback_presets[preset_name]
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] READBACK VERIFICATION - Shader properties:")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {readback_preset.get('disable_shadows', 'NOT FOUND')}")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {readback_preset.get('disable_reflections', 'NOT FOUND')}")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {readback_preset.get('disable_backfacing', 'NOT FOUND')}")
|
||||
else:
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Preset '{preset_name}' not found in readback data!")
|
||||
except json.JSONDecodeError as json_err:
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Could not parse readback JSON: {json_err}")
|
||||
else:
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: No data in scene['text_texture_presets'] after save!")
|
||||
|
||||
print(f"Saved preset '{preset_name}' to blend file")
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ================================================================")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] EXCEPTION occurred: {e}")
|
||||
import traceback
|
||||
print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Full traceback: {traceback.format_exc()}")
|
||||
print(f"Error saving preset to blend file: {e}")
|
||||
return False
|
||||
|
||||
def delete_blend_file_preset(preset_name):
|
||||
"""Delete a preset from the current blend file"""
|
||||
try:
|
||||
import bpy
|
||||
import json
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Get existing presets
|
||||
presets = get_blend_file_presets()
|
||||
|
||||
# Remove the preset if it exists
|
||||
if preset_name in presets:
|
||||
del presets[preset_name]
|
||||
scene["text_texture_presets"] = json.dumps(presets, indent=2)
|
||||
print(f"Deleted preset '{preset_name}' from blend file")
|
||||
return True
|
||||
else:
|
||||
print(f"Preset '{preset_name}' not found in blend file")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error deleting preset from blend file: {e}")
|
||||
return False
|
||||
|
||||
def initialize_presets():
|
||||
"""Initialize presets with simplified, reliable synchronization"""
|
||||
try:
|
||||
import bpy
|
||||
if not hasattr(bpy.context, 'scene') or not bpy.context.scene:
|
||||
return
|
||||
|
||||
props = bpy.context.scene.text_texture_props
|
||||
if not props:
|
||||
return
|
||||
|
||||
print(f"[TTG] Starting preset initialization...")
|
||||
|
||||
# Clear existing presets to avoid duplicates
|
||||
props.presets.clear()
|
||||
|
||||
# Collect all presets from all sources
|
||||
all_presets = {}
|
||||
|
||||
# 1. Load from blend file (highest priority - travels with file)
|
||||
try:
|
||||
blend_presets = get_blend_file_presets()
|
||||
for name, data in blend_presets.items():
|
||||
all_presets[name] = {
|
||||
'data': data,
|
||||
'source': 'BLEND_FILE'
|
||||
}
|
||||
print(f"[TTG] Found {len(blend_presets)} presets in blend file")
|
||||
except Exception as e:
|
||||
print(f"[TTG] Error loading blend file presets: {e}")
|
||||
|
||||
# 2. Load from persistent storage (survives addon updates)
|
||||
try:
|
||||
persistent_dir = get_persistent_preset_path()
|
||||
if os.path.exists(persistent_dir):
|
||||
for filename in os.listdir(persistent_dir):
|
||||
if filename.endswith('.json'):
|
||||
preset_name = os.path.splitext(filename)[0]
|
||||
# Only add if not already in blend file (blend file has priority)
|
||||
if preset_name not in all_presets:
|
||||
try:
|
||||
with open(os.path.join(persistent_dir, filename), 'r') as f:
|
||||
preset_data = json.load(f)
|
||||
all_presets[preset_name] = {
|
||||
'data': preset_data,
|
||||
'source': 'PERSISTENT_FILE'
|
||||
}
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"[TTG] Error reading persistent preset {preset_name}: {e}")
|
||||
print(f"[TTG] Found {len([p for p in all_presets.values() if p['source'] == 'PERSISTENT_FILE'])} persistent presets")
|
||||
except Exception as e:
|
||||
print(f"[TTG] Error loading persistent presets: {e}")
|
||||
|
||||
# 3. Load from legacy storage (backward compatibility)
|
||||
try:
|
||||
legacy_dir = get_preset_path()
|
||||
if os.path.exists(legacy_dir) and legacy_dir != get_persistent_preset_path():
|
||||
for filename in os.listdir(legacy_dir):
|
||||
if filename.endswith('.json'):
|
||||
preset_name = os.path.splitext(filename)[0]
|
||||
# Only add if not already found in higher priority sources
|
||||
if preset_name not in all_presets:
|
||||
try:
|
||||
with open(os.path.join(legacy_dir, filename), 'r') as f:
|
||||
preset_data = json.load(f)
|
||||
all_presets[preset_name] = {
|
||||
'data': preset_data,
|
||||
'source': 'LOCAL_FILE'
|
||||
}
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"[TTG] Error reading legacy preset {preset_name}: {e}")
|
||||
print(f"[TTG] Found {len([p for p in all_presets.values() if p['source'] == 'LOCAL_FILE'])} legacy presets")
|
||||
except Exception as e:
|
||||
print(f"[TTG] Error loading legacy presets: {e}")
|
||||
|
||||
# Add all presets to UI list
|
||||
for preset_name, preset_info in all_presets.items():
|
||||
preset = props.presets.add()
|
||||
preset.name = preset_name
|
||||
preset.source = preset_info['source']
|
||||
|
||||
# Report results
|
||||
blend_count = len([p for p in props.presets if p.source == 'BLEND_FILE'])
|
||||
persistent_count = len([p for p in props.presets if p.source == 'PERSISTENT_FILE'])
|
||||
legacy_count = len([p for p in props.presets if p.source == 'LOCAL_FILE'])
|
||||
|
||||
print(f"[TTG] ✅ Initialized {len(props.presets)} presets ({blend_count} blend file, {persistent_count} persistent, {legacy_count} legacy)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[TTG] Critical error in initialize_presets: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def refresh_presets():
|
||||
"""Refresh the preset list by rescanning files (legacy function)"""
|
||||
import bpy
|
||||
refresh_presets_sync()
|
||||
|
||||
# Force UI update
|
||||
try:
|
||||
if bpy.context.area:
|
||||
bpy.context.area.tag_redraw()
|
||||
except:
|
||||
pass
|
||||
|
||||
def refresh_presets_sync():
|
||||
"""Centralized, synchronous preset refresh - guarantees completion"""
|
||||
try:
|
||||
import bpy
|
||||
|
||||
print("[TTG] 🔄 Synchronizing presets...")
|
||||
|
||||
# Ensure we have valid context
|
||||
if not hasattr(bpy.context, 'scene') or not bpy.context.scene:
|
||||
print("[TTG] ⚠️ No valid scene context for preset refresh")
|
||||
return False
|
||||
|
||||
props = bpy.context.scene.text_texture_props
|
||||
if not props:
|
||||
print("[TTG] ⚠️ No text_texture_props found")
|
||||
return False
|
||||
|
||||
# Run the simplified initialization
|
||||
initialize_presets()
|
||||
|
||||
# Validate that presets are actually loaded
|
||||
preset_count = len(props.presets)
|
||||
if preset_count == 0:
|
||||
print("[TTG] ⚠️ Warning: No presets found after refresh")
|
||||
else:
|
||||
print(f"[TTG] ✅ Preset sync complete - {preset_count} presets available")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[TTG] ❌ Error in refresh_presets_sync: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def ensure_presets_available():
|
||||
"""Defensive programming - ensure presets are loaded before UI operations"""
|
||||
try:
|
||||
import bpy
|
||||
|
||||
if not hasattr(bpy.context, 'scene') or not bpy.context.scene:
|
||||
return False
|
||||
|
||||
props = bpy.context.scene.text_texture_props
|
||||
if not props:
|
||||
return False
|
||||
|
||||
# If no presets loaded, try to load them
|
||||
if len(props.presets) == 0:
|
||||
print("[TTG] 🛡️ No presets available - attempting to load...")
|
||||
return refresh_presets_sync()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[TTG] Error in ensure_presets_available: {e}")
|
||||
return False
|
||||
18
properties/__init__.py
Normal file
18
properties/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Text Texture Generator - Properties Module
|
||||
|
||||
This module contains all property group definitions and configuration systems
|
||||
for the text texture generator addon.
|
||||
"""
|
||||
|
||||
from .property_groups import (
|
||||
TEXT_TEXTURE_ImageOverlay,
|
||||
TEXT_TEXTURE_Preset,
|
||||
TEXT_TEXTURE_Properties
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'TEXT_TEXTURE_ImageOverlay',
|
||||
'TEXT_TEXTURE_Preset',
|
||||
'TEXT_TEXTURE_Properties',
|
||||
]
|
||||
BIN
properties/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
properties/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
properties/__pycache__/property_groups.cpython-311.pyc
Normal file
BIN
properties/__pycache__/property_groups.cpython-311.pyc
Normal file
Binary file not shown.
1056
properties/property_groups.py
Normal file
1056
properties/property_groups.py
Normal file
File diff suppressed because it is too large
Load Diff
30
ui/__init__.py
Normal file
30
ui/__init__.py
Normal 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'
|
||||
]
|
||||
BIN
ui/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
ui/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/panels.cpython-311.pyc
Normal file
BIN
ui/__pycache__/panels.cpython-311.pyc
Normal file
Binary file not shown.
940
ui/panels.py
Normal file
940
ui/panels.py
Normal 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
153
ui/preview.py
Normal 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
|
||||
13
utils/__init__.py
Normal file
13
utils/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Utility modules for the Text Texture Generator addon.
|
||||
"""
|
||||
|
||||
from .constants import bl_info
|
||||
from .system import install_pillow, get_system_fonts, get_font_enum_items
|
||||
|
||||
__all__ = [
|
||||
'bl_info',
|
||||
'install_pillow',
|
||||
'get_system_fonts',
|
||||
'get_font_enum_items'
|
||||
]
|
||||
13
utils/constants.py
Normal file
13
utils/constants.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Constants and metadata for the Text Texture Generator addon.
|
||||
"""
|
||||
|
||||
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",
|
||||
"description": "Generate image textures from text with accurate dimensions and instant live preview",
|
||||
"category": "Material",
|
||||
}
|
||||
84
utils/system.py
Normal file
84
utils/system.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
System utility functions for the Text Texture Generator addon.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import platform
|
||||
import os
|
||||
|
||||
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_anchor_point_matrix():
|
||||
"""Return a 3x3 matrix of anchor point identifiers for UI positioning grid"""
|
||||
return [
|
||||
['TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT'],
|
||||
['MIDDLE_LEFT', 'MIDDLE_CENTER', 'MIDDLE_RIGHT'],
|
||||
['BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT']
|
||||
]
|
||||
Reference in New Issue
Block a user