feat: enhance generation engine and preset management system
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"full": {
|
"full": {
|
||||||
"description": "Full version - all features included",
|
"description": "Full version - all features included",
|
||||||
"exclude_files": ["blender_manifest.toml"],
|
"exclude_files": [],
|
||||||
"exclude_patterns": [],
|
"exclude_patterns": [],
|
||||||
"max_concurrent_operations": 4
|
"max_concurrent_operations": 4
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
BIN
dist/text_texture_generator_v1.0.0.zip
vendored
Binary file not shown.
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
BIN
dist/text_texture_generator_v1.0.0_free.zip
vendored
Binary file not shown.
BIN
dist/text_texture_generator_v1.1.0.zip
vendored
BIN
dist/text_texture_generator_v1.1.0.zip
vendored
Binary file not shown.
BIN
dist/text_texture_generator_v1.1.0_free.zip
vendored
BIN
dist/text_texture_generator_v1.1.0_free.zip
vendored
Binary file not shown.
Binary file not shown.
@@ -4,7 +4,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Text Texture Generator Pro",
|
"name": "Text Texture Generator Pro",
|
||||||
"author": "Marc Mintel <marc@mintel.me>",
|
"author": "Marc Mintel <marc@mintel.me>",
|
||||||
"version": (1, 1, 0),
|
"version": (1, 2, 4),
|
||||||
"blender": (4, 0, 0),
|
"blender": (4, 0, 0),
|
||||||
"location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab",
|
"location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab",
|
||||||
"description": "Generate image textures from text with accurate dimensions and powerful styling options",
|
"description": "Generate image textures from text with accurate dimensions and powerful styling options",
|
||||||
@@ -14,8 +14,19 @@ bl_info = {
|
|||||||
import sys, os, platform
|
import sys, os, platform
|
||||||
_platform = platform.system().lower()
|
_platform = platform.system().lower()
|
||||||
_lib_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"lib-{_platform}")
|
_lib_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"lib-{_platform}")
|
||||||
|
|
||||||
|
# Only use bundled libs if they completely work for this Python version
|
||||||
if os.path.exists(_lib_dir) and _lib_dir not in sys.path:
|
if os.path.exists(_lib_dir) and _lib_dir not in sys.path:
|
||||||
sys.path.insert(0, _lib_dir)
|
sys.path.insert(0, _lib_dir)
|
||||||
|
try:
|
||||||
|
import PIL.Image
|
||||||
|
except ImportError:
|
||||||
|
# Bundled library doesn't match this python version (e.g. Blender 5)
|
||||||
|
sys.path.remove(_lib_dir)
|
||||||
|
# Clear any half-initialized PIL modules from the cache
|
||||||
|
keys_to_remove = [k for k in sys.modules if k == 'PIL' or k.startswith('PIL.')]
|
||||||
|
for k in keys_to_remove:
|
||||||
|
del sys.modules[k]
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import bmesh
|
import bmesh
|
||||||
@@ -27,7 +38,7 @@ import json
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
# Import utility functions
|
# Import utility functions
|
||||||
from .utils import install_pillow, get_system_fonts, get_font_enum_items
|
from .utils import install_pillow, get_system_fonts, get_font_enum_items, log_addon_configuration
|
||||||
|
|
||||||
# Import preset system functions (conditionally for free version)
|
# Import preset system functions (conditionally for free version)
|
||||||
try:
|
try:
|
||||||
@@ -100,6 +111,7 @@ try:
|
|||||||
TEXT_TEXTURE_OT_refresh_fonts,
|
TEXT_TEXTURE_OT_refresh_fonts,
|
||||||
TEXT_TEXTURE_OT_set_anchor_point,
|
TEXT_TEXTURE_OT_set_anchor_point,
|
||||||
TEXT_TEXTURE_OT_sync_margins,
|
TEXT_TEXTURE_OT_sync_margins,
|
||||||
|
TEXT_TEXTURE_OT_recover_data,
|
||||||
)
|
)
|
||||||
UTILITY_OPERATORS_AVAILABLE = True
|
UTILITY_OPERATORS_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -116,6 +128,7 @@ except ImportError:
|
|||||||
TEXT_TEXTURE_OT_refresh_fonts = MockOperator
|
TEXT_TEXTURE_OT_refresh_fonts = MockOperator
|
||||||
TEXT_TEXTURE_OT_set_anchor_point = MockOperator
|
TEXT_TEXTURE_OT_set_anchor_point = MockOperator
|
||||||
TEXT_TEXTURE_OT_sync_margins = MockOperator
|
TEXT_TEXTURE_OT_sync_margins = MockOperator
|
||||||
|
TEXT_TEXTURE_OT_recover_data = MockOperator
|
||||||
|
|
||||||
# Conditionally import preset-related operators
|
# Conditionally import preset-related operators
|
||||||
if PRESETS_AVAILABLE:
|
if PRESETS_AVAILABLE:
|
||||||
@@ -185,6 +198,7 @@ classes = (
|
|||||||
TEXT_TEXTURE_OT_refresh_presets,
|
TEXT_TEXTURE_OT_refresh_presets,
|
||||||
TEXT_TEXTURE_OT_export_presets,
|
TEXT_TEXTURE_OT_export_presets,
|
||||||
TEXT_TEXTURE_OT_import_presets,
|
TEXT_TEXTURE_OT_import_presets,
|
||||||
|
TEXT_TEXTURE_OT_recover_data,
|
||||||
TEXT_TEXTURE_PT_panel,
|
TEXT_TEXTURE_PT_panel,
|
||||||
TEXT_TEXTURE_PT_panel_3d,
|
TEXT_TEXTURE_PT_panel_3d,
|
||||||
TEXT_TEXTURE_PT_panel_properties,
|
TEXT_TEXTURE_PT_panel_properties,
|
||||||
@@ -202,6 +216,15 @@ def _is_mock_class(cls):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
# Ensure Pillow is installed
|
||||||
|
try:
|
||||||
|
import PIL
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
install_pillow()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Text Texture Generator: Failed to install Pillow: {e}")
|
||||||
|
|
||||||
# Clear the registered classes list
|
# Clear the registered classes list
|
||||||
global _registered_classes
|
global _registered_classes
|
||||||
_registered_classes.clear()
|
_registered_classes.clear()
|
||||||
@@ -276,10 +299,32 @@ def register():
|
|||||||
print(f"Error reloading presets: {e}")
|
print(f"Error reloading presets: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# AUTO-MIGRATION: Scan for orphaned text automatically on file load!
|
||||||
|
def _trigger_scan():
|
||||||
|
if hasattr(bpy.context, 'scene'):
|
||||||
|
try:
|
||||||
|
# Execute quietly without user interaction
|
||||||
|
bpy.ops.text_texture.recover_data('EXEC_DEFAULT', quiet=True)
|
||||||
|
except Exception as op_err:
|
||||||
|
print(f"DEBUG: Auto-scan failed/skipped: {op_err}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print("Registering auto-migration scan timer...")
|
||||||
|
if hasattr(bpy.app.timers, 'register'):
|
||||||
|
bpy.app.timers.register(_trigger_scan, first_interval=1.5)
|
||||||
|
else:
|
||||||
|
_trigger_scan()
|
||||||
|
|
||||||
# Register the file load handler
|
# Register the file load handler
|
||||||
if on_file_load not in bpy.app.handlers.load_post:
|
if on_file_load not in bpy.app.handlers.load_post:
|
||||||
bpy.app.handlers.load_post.append(on_file_load)
|
bpy.app.handlers.load_post.append(on_file_load)
|
||||||
|
|
||||||
|
# Log the addon configuration at the end of registration
|
||||||
|
try:
|
||||||
|
log_addon_configuration()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Logging configuration failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -331,8 +331,8 @@ def generate_texture_image(props, width, height):
|
|||||||
entry_height = entry.get('height', entry['image'].height if entry['image'] else 0)
|
entry_height = entry.get('height', entry['image'].height if entry['image'] else 0)
|
||||||
spacing_factor = float(getattr(component, 'text_spacing', 0.0) or 0.0) / 100.0
|
spacing_factor = float(getattr(component, 'text_spacing', 0.0) or 0.0) / 100.0
|
||||||
inter_spacing_factor = float(getattr(component, 'image_spacing', 0.0) or 0.0) / 100.0
|
inter_spacing_factor = float(getattr(component, 'image_spacing', 0.0) or 0.0) / 100.0
|
||||||
spacing_pixels = spacing_factor * width
|
spacing_pixels = spacing_factor * base_font_size
|
||||||
inter_pixels = inter_spacing_factor * width
|
inter_pixels = inter_spacing_factor * base_font_size
|
||||||
|
|
||||||
if placement == 'PREPEND':
|
if placement == 'PREPEND':
|
||||||
cursor -= spacing_pixels
|
cursor -= spacing_pixels
|
||||||
@@ -353,8 +353,11 @@ def generate_texture_image(props, width, height):
|
|||||||
center_y = base_center_y
|
center_y = base_center_y
|
||||||
y_pos = int(round(center_y - entry_height / 2.0))
|
y_pos = int(round(center_y - entry_height / 2.0))
|
||||||
x_pos = int(round(x_pos))
|
x_pos = int(round(x_pos))
|
||||||
y_pos = max(0, min(height - entry_height, y_pos))
|
# For sequential layouts (PREPEND/APPEND), we don't strictly clamp.
|
||||||
x_pos = max(0, min(width - entry_width, x_pos))
|
# If the spacing is huge, it should render off-screen, not clump together at x=0
|
||||||
|
if placement == 'ABSOLUTE':
|
||||||
|
y_pos = max(0, min(height - entry_height, y_pos))
|
||||||
|
x_pos = max(0, min(width - entry_width, x_pos))
|
||||||
entry['position'] = (x_pos, y_pos)
|
entry['position'] = (x_pos, y_pos)
|
||||||
assigned.append(entry)
|
assigned.append(entry)
|
||||||
return assigned
|
return assigned
|
||||||
@@ -423,6 +426,26 @@ def generate_texture_image(props, width, height):
|
|||||||
pass
|
pass
|
||||||
measure_draw = ImageDraw.Draw(pil_img)
|
measure_draw = ImageDraw.Draw(pil_img)
|
||||||
|
|
||||||
|
# Gather static widths (Images) and prepare dynamic components for fitting loop
|
||||||
|
static_overlay_width = 0.0
|
||||||
|
dynamic_components = []
|
||||||
|
|
||||||
|
if hasattr(props, "image_overlays"):
|
||||||
|
for component in props.image_overlays:
|
||||||
|
if component.enabled and getattr(component, 'placement_mode', 'ABSOLUTE') in ['PREPEND', 'APPEND']:
|
||||||
|
c_type = getattr(component, 'component_type', 'IMAGE')
|
||||||
|
dynamic_components.append(component)
|
||||||
|
|
||||||
|
if c_type == 'IMAGE':
|
||||||
|
# Image width is static to font scaling
|
||||||
|
resolved_path = getattr(component, 'image_path', '')
|
||||||
|
if resolved_path:
|
||||||
|
try:
|
||||||
|
with Image.open(resolved_path) as raw:
|
||||||
|
static_overlay_width += raw.width * float(getattr(component, 'scale', 1.0) or 1.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Combine text based on layout mode
|
# Combine text based on layout mode
|
||||||
if props.prepend_append_layout == 'HORIZONTAL':
|
if props.prepend_append_layout == 'HORIZONTAL':
|
||||||
pass
|
pass
|
||||||
@@ -452,11 +475,12 @@ def generate_texture_image(props, width, height):
|
|||||||
text_parts_with_spacing.append(part)
|
text_parts_with_spacing.append(part)
|
||||||
|
|
||||||
full_text = " ".join(text_parts_with_spacing)
|
full_text = " ".join(text_parts_with_spacing)
|
||||||
|
|
||||||
layout_mode = "horizontal"
|
layout_mode = "horizontal"
|
||||||
else:
|
else:
|
||||||
pass
|
|
||||||
# Vertical layout - keep parts separate for individual positioning
|
# Vertical layout - keep parts separate for individual positioning
|
||||||
full_text = "\n".join(full_text_parts)
|
full_text = "\n".join(full_text_parts)
|
||||||
|
|
||||||
layout_mode = "vertical"
|
layout_mode = "vertical"
|
||||||
|
|
||||||
|
|
||||||
@@ -497,6 +521,11 @@ def generate_texture_image(props, width, height):
|
|||||||
|
|
||||||
# Determine font size (with text fitting if enabled)
|
# Determine font size (with text fitting if enabled)
|
||||||
font_size = props.font_size
|
font_size = props.font_size
|
||||||
|
stroke_width = getattr(props, 'stroke_width', 0) if getattr(props, 'enable_stroke', False) else 0
|
||||||
|
stroke_color = tuple(int(c * 255) for c in getattr(props, 'stroke_color', (0, 0, 0, 1))[:4])
|
||||||
|
stroke_kwargs = {'stroke_width': stroke_width, 'stroke_fill': stroke_color} if stroke_width > 0 else {}
|
||||||
|
bbox_stroke_kwargs = {'stroke_width': stroke_width} if stroke_width > 0 else {}
|
||||||
|
|
||||||
if props.enable_text_fitting and has_text_fitting():
|
if props.enable_text_fitting and has_text_fitting():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -509,22 +538,34 @@ def generate_texture_image(props, width, height):
|
|||||||
|
|
||||||
optimal_size = min_size
|
optimal_size = min_size
|
||||||
for test_size in range(min_size, max_size + 1, 2): # Step by 2 for performance
|
for test_size in range(min_size, max_size + 1, 2): # Step by 2 for performance
|
||||||
pass
|
|
||||||
test_font = load_font_for_size(test_size)
|
test_font = load_font_for_size(test_size)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bbox = measure_draw.textbbox((0, 0), full_text, font=test_font)
|
bbox = measure_draw.textbbox((0, 0), full_text, font=test_font, **bbox_stroke_kwargs)
|
||||||
test_width = bbox[2] - bbox[0]
|
test_width = bbox[2] - bbox[0]
|
||||||
test_height = bbox[3] - bbox[1]
|
test_height = bbox[3] - bbox[1]
|
||||||
except (ValueError, AttributeError):
|
except Exception:
|
||||||
# Fallback for bitmap fonts
|
# Fallback gracefully if font measurement fails
|
||||||
test_width, test_height = measure_draw.textsize(full_text, font=test_font)
|
test_width = test_size * len(full_text) * 0.6 + stroke_width * 2
|
||||||
|
test_height = test_size + stroke_width * 2
|
||||||
|
|
||||||
if test_width <= target_width and test_height <= target_height:
|
# Add dynamic components width + spacing mathematically
|
||||||
pass
|
for component in dynamic_components:
|
||||||
|
c_type = getattr(component, 'component_type', 'IMAGE')
|
||||||
|
c_spacing = (float(getattr(component, 'text_spacing', 0.0) or 0.0) / 100.0) * test_size
|
||||||
|
c_inter_spacing = (float(getattr(component, 'image_spacing', 0.0) or 0.0) / 100.0) * test_size
|
||||||
|
test_width += c_spacing + c_inter_spacing
|
||||||
|
|
||||||
|
if c_type == 'TEXT' and component.text_content.strip():
|
||||||
|
try:
|
||||||
|
c_bbox = measure_draw.textbbox((0, 0), component.text_content.strip(), font=test_font, **bbox_stroke_kwargs)
|
||||||
|
test_width += (c_bbox[2] - c_bbox[0]) * float(getattr(component, 'scale', 1.0) or 1.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if test_width + static_overlay_width <= target_width and test_height <= target_height:
|
||||||
optimal_size = test_size
|
optimal_size = test_size
|
||||||
else:
|
else:
|
||||||
pass
|
|
||||||
break
|
break
|
||||||
|
|
||||||
font_size = optimal_size
|
font_size = optimal_size
|
||||||
@@ -553,13 +594,14 @@ def generate_texture_image(props, width, height):
|
|||||||
for part in full_text_parts:
|
for part in full_text_parts:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
bbox = measure_draw.textbbox((0, 0), part, font=font)
|
bbox = measure_draw.textbbox((0, 0), part, font=font, **bbox_stroke_kwargs)
|
||||||
part_height = bbox[3] - bbox[1]
|
part_height = bbox[3] - bbox[1]
|
||||||
part_width = bbox[2] - bbox[0]
|
part_width = bbox[2] - bbox[0]
|
||||||
baseline = base_ascent
|
baseline = base_ascent
|
||||||
except (ValueError, AttributeError):
|
except Exception:
|
||||||
# Fallback for bitmap fonts
|
# Fallback if font measurement fails
|
||||||
part_width, part_height = measure_draw.textsize(part, font=font)
|
part_width = font_size * len(part) * 0.6 + stroke_width * 2
|
||||||
|
part_height = font_size + stroke_width * 2
|
||||||
baseline = base_ascent
|
baseline = base_ascent
|
||||||
part_heights.append(part_height)
|
part_heights.append(part_height)
|
||||||
part_widths.append(part_width)
|
part_widths.append(part_width)
|
||||||
@@ -590,22 +632,61 @@ def generate_texture_image(props, width, height):
|
|||||||
}
|
}
|
||||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size)
|
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size)
|
||||||
before_layers_applied = True
|
before_layers_applied = True
|
||||||
draw = ImageDraw.Draw(pil_img)
|
from PIL import ImageFilter
|
||||||
|
|
||||||
# Draw each part
|
|
||||||
current_y = start_y
|
|
||||||
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
||||||
|
|
||||||
|
# Create effect layers
|
||||||
|
has_shadow = getattr(props, 'enable_shadow', False)
|
||||||
|
has_glow = getattr(props, 'enable_glow', False)
|
||||||
|
has_blur = getattr(props, 'enable_blur', False)
|
||||||
|
|
||||||
|
shadow_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_shadow else None
|
||||||
|
glow_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_glow else None
|
||||||
|
text_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_blur else None
|
||||||
|
|
||||||
|
main_draw = ImageDraw.Draw(pil_img) if not has_blur else ImageDraw.Draw(text_layer)
|
||||||
|
s_draw = ImageDraw.Draw(shadow_layer) if has_shadow else None
|
||||||
|
g_draw = ImageDraw.Draw(glow_layer) if has_glow else None
|
||||||
|
|
||||||
|
s_color = tuple(int(c * 255) for c in getattr(props, 'shadow_color', (0,0,0,1))[:4]) if has_shadow else None
|
||||||
|
g_color = tuple(int(c * 255) for c in getattr(props, 'glow_color', (1,1,1,0.8))[:4]) if has_glow else None
|
||||||
|
|
||||||
|
s_dx = getattr(props, 'shadow_offset_x', 8.0) if has_shadow else 0
|
||||||
|
s_dy = getattr(props, 'shadow_offset_y', 8.0) if has_shadow else 0
|
||||||
|
|
||||||
for i, part in enumerate(full_text_parts):
|
for i, part in enumerate(full_text_parts):
|
||||||
pass
|
|
||||||
part_width = part_widths[i]
|
part_width = part_widths[i]
|
||||||
x = (width - part_width) // 2 # Center horizontally
|
x = (width - part_width) // 2 # Center horizontally
|
||||||
|
|
||||||
draw.text((x, current_y), part, font=font, fill=text_color)
|
if has_shadow:
|
||||||
|
s_draw.text((x + s_dx, current_y + s_dy), part, font=font, fill=s_color, **stroke_kwargs)
|
||||||
|
if has_glow:
|
||||||
|
g_draw.text((x, current_y), part, font=font, fill=g_color, **stroke_kwargs)
|
||||||
|
|
||||||
|
main_draw.text((x, current_y), part, font=font, fill=text_color, **stroke_kwargs)
|
||||||
|
|
||||||
current_y += part_heights[i]
|
current_y += part_heights[i]
|
||||||
if i < len(full_text_parts) - 1: # Add spacing except after last part
|
if i < len(full_text_parts) - 1: # Add spacing except after last part
|
||||||
current_y += font_size // 4
|
current_y += font_size // 4
|
||||||
|
|
||||||
|
# Apply effects and composite
|
||||||
|
if has_shadow:
|
||||||
|
s_blur = getattr(props, 'shadow_blur', 6.0)
|
||||||
|
if s_blur > 0:
|
||||||
|
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(radius=s_blur))
|
||||||
|
pil_img = Image.alpha_composite(pil_img, shadow_layer)
|
||||||
|
|
||||||
|
if has_glow:
|
||||||
|
g_blur = getattr(props, 'glow_blur', 10.0)
|
||||||
|
if g_blur > 0:
|
||||||
|
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(radius=g_blur))
|
||||||
|
pil_img = Image.alpha_composite(pil_img, glow_layer)
|
||||||
|
|
||||||
|
if has_blur:
|
||||||
|
t_blur = getattr(props, 'blur_radius', 2.0)
|
||||||
|
if t_blur > 0:
|
||||||
|
text_layer = text_layer.filter(ImageFilter.GaussianBlur(radius=t_blur))
|
||||||
|
pil_img = Image.alpha_composite(pil_img, text_layer)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
@@ -616,22 +697,60 @@ def generate_texture_image(props, width, height):
|
|||||||
bottom_offset = 0
|
bottom_offset = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
text_bbox = measure_draw.textbbox((0, 0), full_text, font=font)
|
text_bbox = measure_draw.textbbox((0, 0), full_text, font=font, **bbox_stroke_kwargs)
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
text_height = text_bbox[3] - text_bbox[1]
|
text_height = text_bbox[3] - text_bbox[1]
|
||||||
left_offset = text_bbox[0]
|
left_offset = text_bbox[0]
|
||||||
right_offset = text_bbox[2]
|
right_offset = text_bbox[2]
|
||||||
top_offset = text_bbox[1]
|
top_offset = text_bbox[1]
|
||||||
bottom_offset = text_bbox[3]
|
bottom_offset = text_bbox[3]
|
||||||
except (ValueError, AttributeError):
|
except Exception:
|
||||||
# Fallback for bitmap fonts that don't support textbbox
|
# Fallback for bitmap fonts that don't support textbbox
|
||||||
text_width, text_height = measure_draw.textsize(full_text, font=font)
|
text_width = font_size * len(full_text) * 0.6 + stroke_width * 2
|
||||||
|
text_height = font_size + stroke_width * 2
|
||||||
right_offset = text_width
|
right_offset = text_width
|
||||||
bottom_offset = text_height
|
bottom_offset = text_height
|
||||||
|
|
||||||
# Center text
|
# Calculate total widths for PREPEND and APPEND components
|
||||||
x = (width - text_width) // 2
|
# to properly center the entire logical sequence.
|
||||||
|
total_prepend_width = 0.0
|
||||||
|
total_append_width = 0.0
|
||||||
|
|
||||||
|
if hasattr(props, "image_overlays"):
|
||||||
|
for component in props.image_overlays:
|
||||||
|
if component.enabled and getattr(component, 'placement_mode', 'ABSOLUTE') in ['PREPEND', 'APPEND']:
|
||||||
|
c_type = getattr(component, 'component_type', 'IMAGE')
|
||||||
|
c_spacing = float(getattr(component, 'text_spacing', 0.0) or 0.0) / 100.0 * final_font_size
|
||||||
|
c_inter_spacing = float(getattr(component, 'image_spacing', 0.0) or 0.0) / 100.0 * final_font_size
|
||||||
|
|
||||||
|
c_width = 0
|
||||||
|
if c_type == 'TEXT' and component.text_content.strip():
|
||||||
|
try:
|
||||||
|
c_bbox = measure_draw.textbbox((0, 0), component.text_content.strip(), font=font, **bbox_stroke_kwargs)
|
||||||
|
c_width = c_bbox[2] - c_bbox[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif c_type == 'IMAGE':
|
||||||
|
resolved_path = getattr(component, 'image_path', '')
|
||||||
|
if resolved_path:
|
||||||
|
try:
|
||||||
|
with Image.open(resolved_path) as raw:
|
||||||
|
c_width = raw.width * float(getattr(component, 'scale', 1.0) or 1.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if getattr(component, 'placement_mode', 'ABSOLUTE') == 'PREPEND':
|
||||||
|
total_prepend_width += (c_width + c_spacing + c_inter_spacing)
|
||||||
|
else:
|
||||||
|
total_append_width += (c_width + c_spacing + c_inter_spacing)
|
||||||
|
|
||||||
|
# Center the entire logical block
|
||||||
|
total_logical_width = total_prepend_width + text_width + total_append_width
|
||||||
|
logical_start_x = (width - total_logical_width) // 2
|
||||||
|
|
||||||
|
x = logical_start_x + int(total_prepend_width)
|
||||||
y = (height - text_height) // 2
|
y = (height - text_height) // 2
|
||||||
|
|
||||||
left = x + left_offset
|
left = x + left_offset
|
||||||
right = x + right_offset
|
right = x + right_offset
|
||||||
top = y + top_offset
|
top = y + top_offset
|
||||||
@@ -650,11 +769,52 @@ def generate_texture_image(props, width, height):
|
|||||||
}
|
}
|
||||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size)
|
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size)
|
||||||
before_layers_applied = True
|
before_layers_applied = True
|
||||||
draw = ImageDraw.Draw(pil_img)
|
from PIL import ImageFilter
|
||||||
|
|
||||||
# Draw text
|
|
||||||
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
||||||
draw.text((x, y), full_text, font=font, fill=text_color)
|
|
||||||
|
# Create effect layers
|
||||||
|
has_shadow = getattr(props, 'enable_shadow', False)
|
||||||
|
has_glow = getattr(props, 'enable_glow', False)
|
||||||
|
has_blur = getattr(props, 'enable_blur', False)
|
||||||
|
|
||||||
|
shadow_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_shadow else None
|
||||||
|
glow_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_glow else None
|
||||||
|
text_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_blur else None
|
||||||
|
|
||||||
|
main_draw = ImageDraw.Draw(pil_img) if not has_blur else ImageDraw.Draw(text_layer)
|
||||||
|
|
||||||
|
if has_shadow:
|
||||||
|
s_draw = ImageDraw.Draw(shadow_layer)
|
||||||
|
s_color = tuple(int(c * 255) for c in getattr(props, 'shadow_color', (0,0,0,1))[:4])
|
||||||
|
s_dx = getattr(props, 'shadow_offset_x', 8.0)
|
||||||
|
s_dy = getattr(props, 'shadow_offset_y', 8.0)
|
||||||
|
s_draw.text((x + s_dx, y + s_dy), full_text, font=font, fill=s_color, **stroke_kwargs)
|
||||||
|
|
||||||
|
if has_glow:
|
||||||
|
g_draw = ImageDraw.Draw(glow_layer)
|
||||||
|
g_color = tuple(int(c * 255) for c in getattr(props, 'glow_color', (1,1,1,0.8))[:4])
|
||||||
|
g_draw.text((x, y), full_text, font=font, fill=g_color, **stroke_kwargs)
|
||||||
|
|
||||||
|
main_draw.text((x, y), full_text, font=font, fill=text_color, **stroke_kwargs)
|
||||||
|
|
||||||
|
# Apply effects and composite
|
||||||
|
if has_shadow:
|
||||||
|
s_blur = getattr(props, 'shadow_blur', 6.0)
|
||||||
|
if s_blur > 0:
|
||||||
|
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(radius=s_blur))
|
||||||
|
pil_img = Image.alpha_composite(pil_img, shadow_layer)
|
||||||
|
|
||||||
|
if has_glow:
|
||||||
|
g_blur = getattr(props, 'glow_blur', 10.0)
|
||||||
|
if g_blur > 0:
|
||||||
|
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(radius=g_blur))
|
||||||
|
pil_img = Image.alpha_composite(pil_img, glow_layer)
|
||||||
|
|
||||||
|
if has_blur:
|
||||||
|
t_blur = getattr(props, 'blur_radius', 2.0)
|
||||||
|
if t_blur > 0:
|
||||||
|
text_layer = text_layer.filter(ImageFilter.GaussianBlur(radius=t_blur))
|
||||||
|
pil_img = Image.alpha_composite(pil_img, text_layer)
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ try:
|
|||||||
TEXT_TEXTURE_OT_duplicate_overlay,
|
TEXT_TEXTURE_OT_duplicate_overlay,
|
||||||
TEXT_TEXTURE_OT_delete_overlay,
|
TEXT_TEXTURE_OT_delete_overlay,
|
||||||
TEXT_TEXTURE_OT_move_overlay,
|
TEXT_TEXTURE_OT_move_overlay,
|
||||||
|
TEXT_TEXTURE_OT_recover_data,
|
||||||
)
|
)
|
||||||
UTILITY_OPERATORS = [
|
UTILITY_OPERATORS = [
|
||||||
'TEXT_TEXTURE_OT_refresh_preview',
|
'TEXT_TEXTURE_OT_refresh_preview',
|
||||||
@@ -105,6 +106,7 @@ try:
|
|||||||
'TEXT_TEXTURE_OT_duplicate_overlay',
|
'TEXT_TEXTURE_OT_duplicate_overlay',
|
||||||
'TEXT_TEXTURE_OT_delete_overlay',
|
'TEXT_TEXTURE_OT_delete_overlay',
|
||||||
'TEXT_TEXTURE_OT_move_overlay',
|
'TEXT_TEXTURE_OT_move_overlay',
|
||||||
|
'TEXT_TEXTURE_OT_recover_data',
|
||||||
]
|
]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Binary file not shown.
@@ -723,7 +723,18 @@ class TEXT_TEXTURE_OT_generate_shader(Operator):
|
|||||||
if space.type == 'NODE_EDITOR':
|
if space.type == 'NODE_EDITOR':
|
||||||
pass
|
pass
|
||||||
space.shader_type = 'OBJECT'
|
space.shader_type = 'OBJECT'
|
||||||
space.id = mat
|
# In Blender 5+, space.id is read-only.
|
||||||
|
# Instead, point the node_tree directly or
|
||||||
|
# set the active material on the object so
|
||||||
|
# the editor follows it automatically.
|
||||||
|
try:
|
||||||
|
space.node_tree = mat.node_tree
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
pass
|
||||||
|
# Also ensure the active object shows this
|
||||||
|
# material so the editor stays in sync.
|
||||||
|
if context.active_object and hasattr(context.active_object, 'active_material'):
|
||||||
|
context.active_object.active_material = mat
|
||||||
area.tag_redraw()
|
area.tag_redraw()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -35,13 +35,11 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pass
|
|
||||||
props = context.scene.text_texture_props
|
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()
|
preset_name = self.preset_name.strip() if self.preset_name else props.new_preset_name.strip()
|
||||||
|
print(f"DEBUG: TEXT_TEXTURE_OT_save_preset.execute - Name: '{preset_name}', Overwrite: {self.overwrite}, SaveWithBlend: {props.save_with_blend_file}")
|
||||||
|
|
||||||
if not preset_name:
|
if not preset_name:
|
||||||
pass
|
|
||||||
self.report({'ERROR'}, "Please enter a preset name")
|
self.report({'ERROR'}, "Please enter a preset name")
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
@@ -67,15 +65,13 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
|
|||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
# Check for duplicate names
|
# Check for duplicate names
|
||||||
preset_file = os.path.join(get_preset_path(), f"{preset_name}.json")
|
preset_file = os.path.join(get_persistent_preset_path(), f"{preset_name}.json")
|
||||||
if os.path.exists(preset_file) and not self.overwrite:
|
if os.path.exists(preset_file) and not self.overwrite:
|
||||||
pass
|
|
||||||
# Suggest alternative names
|
# Suggest alternative names
|
||||||
base_name = preset_name
|
base_name = preset_name
|
||||||
counter = 1
|
counter = 1
|
||||||
suggested_name = f"{base_name}_{counter}"
|
suggested_name = f"{base_name}_{counter}"
|
||||||
while os.path.exists(os.path.join(get_preset_path(), f"{suggested_name}.json")):
|
while os.path.exists(os.path.join(get_persistent_preset_path(), f"{suggested_name}.json")):
|
||||||
pass
|
|
||||||
counter += 1
|
counter += 1
|
||||||
suggested_name = f"{base_name}_{counter}"
|
suggested_name = f"{base_name}_{counter}"
|
||||||
|
|
||||||
@@ -107,6 +103,7 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
|
|||||||
'use_alpha': props.use_alpha,
|
'use_alpha': props.use_alpha,
|
||||||
'emission_strength': props.emission_strength,
|
'emission_strength': props.emission_strength,
|
||||||
'auto_assign_material': props.auto_assign_material,
|
'auto_assign_material': props.auto_assign_material,
|
||||||
|
'custom_material_name': getattr(props, 'custom_material_name', ''),
|
||||||
'disable_shadows': props.disable_shadows,
|
'disable_shadows': props.disable_shadows,
|
||||||
'disable_reflections': props.disable_reflections,
|
'disable_reflections': props.disable_reflections,
|
||||||
'disable_backfacing': props.disable_backfacing,
|
'disable_backfacing': props.disable_backfacing,
|
||||||
@@ -129,14 +126,32 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
|
|||||||
'glow_color': list(props.glow_color),
|
'glow_color': list(props.glow_color),
|
||||||
'enable_blur': props.enable_blur,
|
'enable_blur': props.enable_blur,
|
||||||
'blur_radius': props.blur_radius,
|
'blur_radius': props.blur_radius,
|
||||||
|
# Text fitting properties
|
||||||
|
'enable_text_fitting': props.enable_text_fitting,
|
||||||
|
'text_fitting_mode': props.text_fitting_mode,
|
||||||
|
'text_fitting_margin': props.text_fitting_margin,
|
||||||
|
'min_font_size': props.min_font_size,
|
||||||
|
'max_font_size': props.max_font_size,
|
||||||
|
# Enhanced positioning properties
|
||||||
|
'anchor_point': props.anchor_point,
|
||||||
|
'position_mode': props.position_mode,
|
||||||
|
'margin_x': props.margin_x,
|
||||||
|
'margin_y': props.margin_y,
|
||||||
|
'margins_linked': props.margins_linked,
|
||||||
|
'offset_x': props.offset_x,
|
||||||
|
'offset_y': props.offset_y,
|
||||||
|
'manual_position_x': props.manual_position_x,
|
||||||
|
'manual_position_y': props.manual_position_y,
|
||||||
|
'manual_position_unit': props.manual_position_unit,
|
||||||
|
'constrain_to_canvas': props.constrain_to_canvas,
|
||||||
'image_overlays': []
|
'image_overlays': []
|
||||||
}
|
}
|
||||||
|
|
||||||
# DETAILED LOGGING: Verify shader properties in preset_data
|
# DETAILED LOGGING: Verify shader properties in preset_data
|
||||||
|
|
||||||
# Save overlay data
|
# Save overlay data
|
||||||
for overlay in props.image_overlays:
|
print(f"DEBUG: TEXT_TEXTURE_OT_save_preset - Found {len(props.image_overlays)} overlays in Scene")
|
||||||
pass
|
for i, overlay in enumerate(props.image_overlays):
|
||||||
overlay_data = {
|
overlay_data = {
|
||||||
'name': overlay.name,
|
'name': overlay.name,
|
||||||
'component_type': overlay.component_type,
|
'component_type': overlay.component_type,
|
||||||
@@ -152,6 +167,7 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
|
|||||||
'z_index': overlay.z_index,
|
'z_index': overlay.z_index,
|
||||||
'enabled': overlay.enabled
|
'enabled': overlay.enabled
|
||||||
}
|
}
|
||||||
|
print(f"DEBUG: Extracted overlay {i}: '{overlay.name}' (Type: {overlay.component_type}, Mode: {overlay.placement_mode})")
|
||||||
preset_data['image_overlays'].append(overlay_data)
|
preset_data['image_overlays'].append(overlay_data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -227,10 +243,8 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
|
|||||||
preset_name = props.new_preset_name.strip()
|
preset_name = props.new_preset_name.strip()
|
||||||
|
|
||||||
if preset_name and preset_name != "New Preset":
|
if preset_name and preset_name != "New Preset":
|
||||||
pass
|
preset_file = os.path.join(get_persistent_preset_path(), f"{preset_name}.json")
|
||||||
preset_file = os.path.join(get_preset_path(), f"{preset_name}.json")
|
|
||||||
if os.path.exists(preset_file):
|
if os.path.exists(preset_file):
|
||||||
pass
|
|
||||||
return context.window_manager.invoke_confirm(self, event)
|
return context.window_manager.invoke_confirm(self, event)
|
||||||
|
|
||||||
return self.execute(context)
|
return self.execute(context)
|
||||||
@@ -276,8 +290,8 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
|
|||||||
"""Load preset"""
|
"""Load preset"""
|
||||||
bl_idname = "text_texture.load_preset"
|
bl_idname = "text_texture.load_preset"
|
||||||
bl_label = "Load Preset"
|
bl_label = "Load Preset"
|
||||||
bl_description = "Load selected preset"
|
bl_description = "Load the selected preset and optionally recreate materials. Non-undoable to preserve volatile properties."
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER'}
|
||||||
|
|
||||||
preset_name: StringProperty()
|
preset_name: StringProperty()
|
||||||
|
|
||||||
@@ -305,26 +319,51 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
|
|||||||
|
|
||||||
preset_data, source = all_presets[self.preset_name]
|
preset_data, source = all_presets[self.preset_name]
|
||||||
|
|
||||||
|
# Update the UI checkbox to match the preset's source location
|
||||||
|
props.save_with_blend_file = (source == 'BLEND_FILE')
|
||||||
|
|
||||||
props.is_updating = True
|
props.is_updating = True
|
||||||
|
|
||||||
# Skip loading text to prevent overwriting current text
|
def _safe_assign_color(prop_array, new_values):
|
||||||
# props.text = preset_data.get('text', props.text)
|
for i, v in enumerate(new_values):
|
||||||
|
if i < len(prop_array):
|
||||||
|
prop_array[i] = v
|
||||||
|
|
||||||
|
# Load text (previously skipped, this might be why users think it's broken)
|
||||||
|
props.text = preset_data.get('text', props.text)
|
||||||
props.texture_width = preset_data.get('texture_width', props.texture_width)
|
props.texture_width = preset_data.get('texture_width', props.texture_width)
|
||||||
props.texture_height = preset_data.get('texture_height', props.texture_height)
|
props.texture_height = preset_data.get('texture_height', props.texture_height)
|
||||||
props.font_size = preset_data.get('font_size', props.font_size)
|
props.font_size = preset_data.get('font_size', props.font_size)
|
||||||
props.padding = preset_data.get('padding', props.padding)
|
props.padding = preset_data.get('padding', props.padding)
|
||||||
props.text_color = preset_data.get('text_color', props.text_color)
|
if 'text_color' in preset_data:
|
||||||
|
_safe_assign_color(props.text_color, preset_data['text_color'])
|
||||||
props.background_transparent = preset_data.get('background_transparent', props.background_transparent)
|
props.background_transparent = preset_data.get('background_transparent', props.background_transparent)
|
||||||
props.background_color = preset_data.get('background_color', props.background_color)
|
if 'background_color' in preset_data:
|
||||||
|
_safe_assign_color(props.background_color, preset_data['background_color'])
|
||||||
props.font_path = preset_data.get('font_path', props.font_path)
|
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.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.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.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)
|
# --- MIGRATION: LEGACY PREPEND/APPEND TEXT ---
|
||||||
props.prepend_margin = preset_data.get('prepend_margin', props.prepend_margin)
|
# Instead of loading into hidden legacy fields, process them immediately
|
||||||
props.append_margin = preset_data.get('append_margin', props.append_margin)
|
# and append to the 'image_overlays' if valid payload exists.
|
||||||
|
# Then ALWAYS clear the legacy properties so generation_engine ignores them.
|
||||||
|
legacy_prependRaw = preset_data.get('prepend_text', '')
|
||||||
|
legacy_appendRaw = preset_data.get('append_text', '')
|
||||||
|
legacy_prepend = legacy_prependRaw.strip() if legacy_prependRaw else ''
|
||||||
|
legacy_append = legacy_appendRaw.strip() if legacy_appendRaw else ''
|
||||||
|
|
||||||
|
# Save for later processing when we handle image_overlays
|
||||||
|
_has_legacy_migration = bool(legacy_prepend or legacy_append)
|
||||||
|
_migrated_legacy_prepend = legacy_prepend
|
||||||
|
_migrated_legacy_append = legacy_append
|
||||||
|
|
||||||
|
# Force them clear on the property group
|
||||||
|
props.prepend_text = ""
|
||||||
|
props.append_text = ""
|
||||||
|
props.prepend_margin = preset_data.get('prepend_margin', 0)
|
||||||
|
props.append_margin = preset_data.get('append_margin', 0)
|
||||||
props.prepend_append_layout = preset_data.get('prepend_append_layout', props.prepend_append_layout)
|
props.prepend_append_layout = preset_data.get('prepend_append_layout', props.prepend_append_layout)
|
||||||
|
|
||||||
# DETAILED LOGGING: Shader property loading with before/after values
|
# DETAILED LOGGING: Shader property loading with before/after values
|
||||||
@@ -344,6 +383,9 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
|
|||||||
old_auto_assign = props.auto_assign_material
|
old_auto_assign = props.auto_assign_material
|
||||||
props.auto_assign_material = preset_data.get('auto_assign_material', props.auto_assign_material)
|
props.auto_assign_material = preset_data.get('auto_assign_material', props.auto_assign_material)
|
||||||
|
|
||||||
|
if hasattr(props, 'custom_material_name'):
|
||||||
|
props.custom_material_name = preset_data.get('custom_material_name', getattr(props, 'custom_material_name', ''))
|
||||||
|
|
||||||
# CRITICAL SHADER PROPERTIES - Most detailed logging
|
# CRITICAL SHADER PROPERTIES - Most detailed logging
|
||||||
|
|
||||||
old_disable_shadows = props.disable_shadows
|
old_disable_shadows = props.disable_shadows
|
||||||
@@ -368,21 +410,24 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
|
|||||||
props.enable_stroke = preset_data.get('enable_stroke', props.enable_stroke)
|
props.enable_stroke = preset_data.get('enable_stroke', props.enable_stroke)
|
||||||
props.stroke_width = preset_data.get('stroke_width', props.stroke_width)
|
props.stroke_width = preset_data.get('stroke_width', props.stroke_width)
|
||||||
props.stroke_position = preset_data.get('stroke_position', props.stroke_position)
|
props.stroke_position = preset_data.get('stroke_position', props.stroke_position)
|
||||||
props.stroke_color = preset_data.get('stroke_color', props.stroke_color)
|
if 'stroke_color' in preset_data:
|
||||||
|
_safe_assign_color(props.stroke_color, preset_data['stroke_color'])
|
||||||
|
|
||||||
# Load shadow/glow settings
|
# Load shadow/glow settings
|
||||||
props.enable_shadow = preset_data.get('enable_shadow', props.enable_shadow)
|
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_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_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_blur = preset_data.get('shadow_blur', props.shadow_blur)
|
||||||
props.shadow_color = preset_data.get('shadow_color', props.shadow_color)
|
if 'shadow_color' in preset_data:
|
||||||
|
_safe_assign_color(props.shadow_color, preset_data['shadow_color'])
|
||||||
# Handle new shadow_spread property with backward compatibility
|
# Handle new shadow_spread property with backward compatibility
|
||||||
if hasattr(props, 'shadow_spread'):
|
if hasattr(props, 'shadow_spread'):
|
||||||
pass
|
pass
|
||||||
props.shadow_spread = preset_data.get('shadow_spread', 1.0)
|
props.shadow_spread = preset_data.get('shadow_spread', 1.0)
|
||||||
props.enable_glow = preset_data.get('enable_glow', props.enable_glow)
|
props.enable_glow = preset_data.get('enable_glow', props.enable_glow)
|
||||||
props.glow_blur = preset_data.get('glow_blur', props.glow_blur)
|
props.glow_blur = preset_data.get('glow_blur', props.glow_blur)
|
||||||
props.glow_color = preset_data.get('glow_color', props.glow_color)
|
if 'glow_color' in preset_data:
|
||||||
|
_safe_assign_color(props.glow_color, preset_data['glow_color'])
|
||||||
|
|
||||||
# Load blur settings
|
# Load blur settings
|
||||||
props.enable_blur = preset_data.get('enable_blur', props.enable_blur)
|
props.enable_blur = preset_data.get('enable_blur', props.enable_blur)
|
||||||
@@ -395,25 +440,81 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
|
|||||||
props.min_font_size = preset_data.get('min_font_size', props.min_font_size)
|
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)
|
props.max_font_size = preset_data.get('max_font_size', props.max_font_size)
|
||||||
|
|
||||||
# Load overlay data
|
# Load enhanced positioning settings
|
||||||
props.image_overlays.clear()
|
props.anchor_point = preset_data.get('anchor_point', props.anchor_point)
|
||||||
overlay_data_list = preset_data.get('image_overlays', [])
|
props.position_mode = preset_data.get('position_mode', props.position_mode)
|
||||||
for overlay_data in overlay_data_list:
|
props.margin_x = preset_data.get('margin_x', props.margin_x)
|
||||||
pass
|
props.margin_y = preset_data.get('margin_y', props.margin_y)
|
||||||
overlay = props.image_overlays.add()
|
props.margins_linked = preset_data.get('margins_linked', props.margins_linked)
|
||||||
overlay.name = overlay_data.get('name', 'Overlay')
|
props.offset_x = preset_data.get('offset_x', props.offset_x)
|
||||||
overlay.component_type = overlay_data.get('component_type', overlay.component_type)
|
props.offset_y = preset_data.get('offset_y', props.offset_y)
|
||||||
overlay.placement_mode = overlay_data.get('placement_mode', 'ABSOLUTE')
|
props.manual_position_x = preset_data.get('manual_position_x', props.manual_position_x)
|
||||||
overlay.text_content = overlay_data.get('text_content', '')
|
props.manual_position_y = preset_data.get('manual_position_y', props.manual_position_y)
|
||||||
overlay.text_spacing = overlay_data.get('text_spacing', overlay.text_spacing)
|
props.manual_position_unit = preset_data.get('manual_position_unit', props.manual_position_unit)
|
||||||
overlay.image_spacing = overlay_data.get('image_spacing', overlay.image_spacing)
|
props.constrain_to_canvas = preset_data.get('constrain_to_canvas', props.constrain_to_canvas)
|
||||||
overlay.image_path = overlay_data.get('image_path', '')
|
|
||||||
overlay.x_position = overlay_data.get('x_position', 0.5)
|
# Deep data log for loading
|
||||||
overlay.y_position = overlay_data.get('y_position', 0.5)
|
import json
|
||||||
overlay.scale = overlay_data.get('scale', 1.0)
|
try:
|
||||||
overlay.rotation = overlay_data.get('rotation', 0.0)
|
data_dump = json.dumps(preset_data, indent=2)
|
||||||
overlay.z_index = overlay_data.get('z_index', 1)
|
print(f"DEBUG: TEXT_TEXTURE_OT_load_preset - LOADING PAYLOAD:\n{data_dump}")
|
||||||
overlay.enabled = overlay_data.get('enabled', True)
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Error dumping preset_data during load: {e}")
|
||||||
|
|
||||||
|
# Load overlay data - ALWAYS check for data (bulletproof structure)
|
||||||
|
if 'image_overlays' in preset_data:
|
||||||
|
props.image_overlays.clear()
|
||||||
|
overlay_data_list = preset_data.get('image_overlays', [])
|
||||||
|
print(f"DEBUG: TEXT_TEXTURE_OT_load_preset - Found {len(overlay_data_list)} overlays in payload - REPLACING")
|
||||||
|
for i, overlay_data in enumerate(overlay_data_list):
|
||||||
|
overlay = props.image_overlays.add()
|
||||||
|
overlay.name = overlay_data.get('name', 'Overlay')
|
||||||
|
overlay.component_type = overlay_data.get('component_type', overlay.component_type)
|
||||||
|
overlay.placement_mode = overlay_data.get('placement_mode', 'ABSOLUTE')
|
||||||
|
overlay.text_content = overlay_data.get('text_content', '')
|
||||||
|
overlay.text_spacing = overlay_data.get('text_spacing', overlay.text_spacing)
|
||||||
|
overlay.image_spacing = overlay_data.get('image_spacing', overlay.image_spacing)
|
||||||
|
overlay.image_path = overlay_data.get('image_path', '')
|
||||||
|
overlay.x_position = overlay_data.get('x_position', 0.0)
|
||||||
|
overlay.y_position = overlay_data.get('y_position', 0.0)
|
||||||
|
overlay.scale = overlay_data.get('scale', 1.0)
|
||||||
|
overlay.rotation = overlay_data.get('rotation', 0.0)
|
||||||
|
overlay.z_index = overlay_data.get('z_index', 0)
|
||||||
|
overlay.enabled = overlay_data.get('enabled', True)
|
||||||
|
print(f"DEBUG: Added overlay {i} to Scene: '{overlay.name}' (Type: {overlay.component_type}, Mode: {overlay.placement_mode})")
|
||||||
|
else:
|
||||||
|
print("DEBUG: TEXT_TEXTURE_OT_load_preset - 'image_overlays' key missing in payload - RETAINING current session data")
|
||||||
|
# Wait, if image_overlays is missing we retain session data, but we might
|
||||||
|
# be migrating legacy fields from this preset. Let's make sure we actually
|
||||||
|
# apply the migration even if no 'image_overlays' key exists.
|
||||||
|
|
||||||
|
# --- APPLY MIGRATED PREPEND/APPEND OVERLAYS ---
|
||||||
|
# Ensure legacy text gets added to the current image_overlays list
|
||||||
|
# (either the fully replaced one or the retained one).
|
||||||
|
if _has_legacy_migration:
|
||||||
|
if _migrated_legacy_prepend:
|
||||||
|
overlay = props.image_overlays.add()
|
||||||
|
overlay.name = "Migrated Prefix"
|
||||||
|
overlay.component_type = 'TEXT'
|
||||||
|
overlay.placement_mode = 'PREPEND'
|
||||||
|
overlay.text_content = _migrated_legacy_prepend
|
||||||
|
# Handle margin which could be None in the preset, and map from old 0-100 logic
|
||||||
|
margin_val = preset_data.get('prepend_margin', 0)
|
||||||
|
overlay.text_spacing = min(30.0, float(margin_val) / 5.0) if margin_val else 0.0
|
||||||
|
overlay.enabled = True
|
||||||
|
print(f"DEBUG: Migrated legacy prepend_text to PREPEND layer")
|
||||||
|
|
||||||
|
if _migrated_legacy_append:
|
||||||
|
overlay = props.image_overlays.add()
|
||||||
|
overlay.name = "Migrated Suffix"
|
||||||
|
overlay.component_type = 'TEXT'
|
||||||
|
overlay.placement_mode = 'APPEND'
|
||||||
|
overlay.text_content = _migrated_legacy_append
|
||||||
|
# Handle margin which could be None in the preset, and map from old 0-100 logic
|
||||||
|
margin_val = preset_data.get('append_margin', 0)
|
||||||
|
overlay.text_spacing = min(30.0, float(margin_val) / 5.0) if margin_val else 0.0
|
||||||
|
overlay.enabled = True
|
||||||
|
print(f"DEBUG: Migrated legacy append_text to APPEND layer")
|
||||||
|
|
||||||
props.is_updating = False
|
props.is_updating = False
|
||||||
|
|
||||||
@@ -424,6 +525,12 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
|
|||||||
from ..core.generation_engine import generate_preview
|
from ..core.generation_engine import generate_preview
|
||||||
generate_preview(props, props.preview_size if hasattr(props, 'preview_size') else 512, props.preview_size if hasattr(props, 'preview_size') else 512)
|
generate_preview(props, props.preview_size if hasattr(props, 'preview_size') else 512, props.preview_size if hasattr(props, 'preview_size') else 512)
|
||||||
|
|
||||||
|
# Also regenerate the main texture to update the 3D view immediately
|
||||||
|
try:
|
||||||
|
bpy.ops.text_texture.generate('EXEC_DEFAULT')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to auto-generate texture: {e}")
|
||||||
|
|
||||||
if hasattr(props, "last_loaded_preset"):
|
if hasattr(props, "last_loaded_preset"):
|
||||||
pass
|
pass
|
||||||
props.last_loaded_preset = self.preset_name
|
props.last_loaded_preset = self.preset_name
|
||||||
@@ -477,10 +584,10 @@ class TEXT_TEXTURE_OT_delete_preset(Operator):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
pass
|
pass
|
||||||
# Delete from both blend file and local storage
|
# Delete from both blend file and persistent storage
|
||||||
blend_deleted = delete_blend_file_preset(self.preset_name)
|
blend_deleted = delete_blend_file_preset(self.preset_name)
|
||||||
|
|
||||||
preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json")
|
preset_file = os.path.join(get_persistent_preset_path(), f"{self.preset_name}.json")
|
||||||
local_deleted = False
|
local_deleted = False
|
||||||
if os.path.exists(preset_file):
|
if os.path.exists(preset_file):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ class TEXT_TEXTURE_OT_add_overlay(Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pass
|
print(f"DEBUG ENTRY: TEXT_TEXTURE_OT_add_overlay.execute (mode: {self.placement_mode})")
|
||||||
props = context.scene.text_texture_props
|
props = context.scene.text_texture_props
|
||||||
|
|
||||||
overlay = props.image_overlays.add()
|
overlay = props.image_overlays.add()
|
||||||
@@ -367,9 +367,10 @@ class TEXT_TEXTURE_OT_add_overlay(Operator):
|
|||||||
overlay.component_type = 'TEXT' if self.placement_mode in {'PREPEND', 'APPEND'} else 'IMAGE'
|
overlay.component_type = 'TEXT' if self.placement_mode in {'PREPEND', 'APPEND'} else 'IMAGE'
|
||||||
props.active_overlay_index = len(props.image_overlays) - 1
|
props.active_overlay_index = len(props.image_overlays) - 1
|
||||||
|
|
||||||
|
print(f"DEBUG EXIT: TEXT_TEXTURE_OT_add_overlay.execute - Created: '{overlay.name}', New Total: {len(props.image_overlays)}")
|
||||||
|
|
||||||
# Force UI redraw
|
# Force UI redraw
|
||||||
for area in context.screen.areas:
|
for area in context.screen.areas:
|
||||||
pass
|
|
||||||
area.tag_redraw()
|
area.tag_redraw()
|
||||||
|
|
||||||
self.report({'INFO'}, f"Added component: {overlay.name}")
|
self.report({'INFO'}, f"Added component: {overlay.name}")
|
||||||
@@ -649,6 +650,83 @@ class TEXT_TEXTURE_OT_move_overlay(Operator):
|
|||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class TEXT_TEXTURE_OT_recover_data(Operator):
|
||||||
|
"""Scan and recover orphaned composition data"""
|
||||||
|
bl_idname = "text_texture.recover_data"
|
||||||
|
bl_label = "Scan for Lost Components"
|
||||||
|
bl_description = "Scan current scene for orphaned composition data and attempt to re-link it"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
quiet: bpy.props.BoolProperty(
|
||||||
|
name="Quiet Mode",
|
||||||
|
description="Suppress info messages when nothing is found",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
props = context.scene.text_texture_props
|
||||||
|
print(f"DEBUG: TEXT_TEXTURE_OT_recover_data - STARTING SCAN (Quiet: {self.quiet})")
|
||||||
|
|
||||||
|
# Check if the underlying collection is just empty or actually missing
|
||||||
|
if not hasattr(props, "image_overlays"):
|
||||||
|
if not self.quiet:
|
||||||
|
self.report({'ERROR'}, "Critical: image_overlays property missing from scene data")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# Run Migration for Legacy Text Fields!
|
||||||
|
# If the user opened an old .blend file, the prepend_text is still in the Scene props,
|
||||||
|
# but won't be seen by the new Component UI.
|
||||||
|
legacy_prepend = props.prepend_text.strip() if props.prepend_text else ""
|
||||||
|
legacy_append = props.append_text.strip() if props.append_text else ""
|
||||||
|
|
||||||
|
migrated_count = 0
|
||||||
|
|
||||||
|
if legacy_prepend:
|
||||||
|
overlay = props.image_overlays.add()
|
||||||
|
overlay.name = "Recovered Prefix"
|
||||||
|
overlay.component_type = 'TEXT'
|
||||||
|
overlay.placement_mode = 'PREPEND'
|
||||||
|
overlay.text_content = legacy_prepend
|
||||||
|
# Legacy margin was 0-100 where 10 = 1 space character.
|
||||||
|
# New text_spacing is canvas percentage (0-100%).
|
||||||
|
# We map: legacy 10 -> 2%, legacy 100 -> 20%
|
||||||
|
margin_val = props.prepend_margin
|
||||||
|
overlay.text_spacing = min(30.0, float(margin_val) / 5.0) if margin_val else 0.0
|
||||||
|
overlay.enabled = True
|
||||||
|
props.prepend_text = ""
|
||||||
|
migrated_count += 1
|
||||||
|
|
||||||
|
if legacy_append:
|
||||||
|
overlay = props.image_overlays.add()
|
||||||
|
overlay.name = "Recovered Suffix"
|
||||||
|
overlay.component_type = 'TEXT'
|
||||||
|
overlay.placement_mode = 'APPEND'
|
||||||
|
overlay.text_content = legacy_append
|
||||||
|
margin_val = props.append_margin
|
||||||
|
overlay.text_spacing = min(30.0, float(margin_val) / 5.0) if margin_val else 0.0
|
||||||
|
overlay.enabled = True
|
||||||
|
props.append_text = ""
|
||||||
|
migrated_count += 1
|
||||||
|
|
||||||
|
total = len(props.image_overlays)
|
||||||
|
print(f"DEBUG: Current count after recovery: {total}")
|
||||||
|
|
||||||
|
# Force a tag redraw and a collection refresh
|
||||||
|
for area in context.screen.areas:
|
||||||
|
area.tag_redraw()
|
||||||
|
|
||||||
|
if total == 0 and migrated_count == 0:
|
||||||
|
if not self.quiet:
|
||||||
|
self.report({'INFO'}, "Scan complete. No orphaned data or legacy properties found.")
|
||||||
|
elif migrated_count > 0:
|
||||||
|
self.report({'INFO'}, f"Recovery complete. Migrated {migrated_count} legacy text block(s).")
|
||||||
|
else:
|
||||||
|
if not self.quiet:
|
||||||
|
self.report({'INFO'}, f"Scan complete. Found {total} valid components.")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
# Export all operator classes for wildcard imports
|
# Export all operator classes for wildcard imports
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -664,4 +742,5 @@ __all__ = [
|
|||||||
'TEXT_TEXTURE_OT_duplicate_overlay',
|
'TEXT_TEXTURE_OT_duplicate_overlay',
|
||||||
'TEXT_TEXTURE_OT_delete_overlay',
|
'TEXT_TEXTURE_OT_delete_overlay',
|
||||||
'TEXT_TEXTURE_OT_move_overlay',
|
'TEXT_TEXTURE_OT_move_overlay',
|
||||||
|
'TEXT_TEXTURE_OT_recover_data',
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
@@ -200,21 +200,18 @@ def get_blend_file_presets():
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def save_blend_file_preset(preset_name, preset_data):
|
def save_blend_file_preset(preset_name, preset_data):
|
||||||
pass
|
|
||||||
"""Save a preset to the current blend file"""
|
"""Save a preset to the current blend file"""
|
||||||
try:
|
try:
|
||||||
pass
|
print(f"DEBUG: save_blend_file_preset - Name: '{preset_name}'")
|
||||||
import bpy
|
import bpy
|
||||||
import json
|
import json
|
||||||
scene = bpy.context.scene
|
scene = bpy.context.scene
|
||||||
|
|
||||||
# DETAILED LOGGING: Function entry
|
# DETAILED LOGGING: Function entry
|
||||||
if isinstance(preset_data, dict):
|
if isinstance(preset_data, dict):
|
||||||
pass
|
print(f"DEBUG: Preset keys to save: {list(preset_data.keys())}")
|
||||||
print(f"Preset data keys: {list(preset_data.keys())}")
|
|
||||||
else:
|
else:
|
||||||
pass
|
print("DEBUG ERROR: preset_data is not a dictionary!")
|
||||||
print("ERROR: preset_data is not a dictionary!")
|
|
||||||
|
|
||||||
# Get existing presets
|
# Get existing presets
|
||||||
presets = get_blend_file_presets()
|
presets = get_blend_file_presets()
|
||||||
@@ -263,10 +260,9 @@ def save_blend_file_preset(preset_name, preset_data):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_blend_file_preset(preset_name):
|
def delete_blend_file_preset(preset_name):
|
||||||
pass
|
|
||||||
"""Delete a preset from the current blend file"""
|
"""Delete a preset from the current blend file"""
|
||||||
try:
|
try:
|
||||||
pass
|
print(f"DEBUG: delete_blend_file_preset - Name: '{preset_name}'")
|
||||||
import bpy
|
import bpy
|
||||||
import json
|
import json
|
||||||
scene = bpy.context.scene
|
scene = bpy.context.scene
|
||||||
@@ -276,18 +272,15 @@ def delete_blend_file_preset(preset_name):
|
|||||||
|
|
||||||
# Remove the preset if it exists
|
# Remove the preset if it exists
|
||||||
if preset_name in presets:
|
if preset_name in presets:
|
||||||
pass
|
|
||||||
del presets[preset_name]
|
del presets[preset_name]
|
||||||
scene["text_texture_presets"] = json.dumps(presets, indent=2)
|
scene["text_texture_presets"] = json.dumps(presets, indent=2)
|
||||||
print(f"Deleted preset '{preset_name}' from blend file")
|
print(f"DEBUG: Successfully deleted preset '{preset_name}' from blend file")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
pass
|
print(f"DEBUG: Preset '{preset_name}' not found in blend file for deletion")
|
||||||
print(f"Preset '{preset_name}' not found in blend file")
|
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
print(f"DEBUG ERROR deleting preset from blend file: {e}")
|
||||||
print(f"Error deleting preset from blend file: {e}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def migrate_from_legacy_system():
|
def migrate_from_legacy_system():
|
||||||
@@ -429,22 +422,51 @@ def get_unified_preset_list():
|
|||||||
all_presets[name] = (data, 'BLEND_FILE')
|
all_presets[name] = (data, 'BLEND_FILE')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
print(f"DEBUG: Error in get_unified_preset_list: {e}")
|
||||||
print(f"Error in get_unified_preset_list: {e}")
|
|
||||||
|
|
||||||
|
print(f"DEBUG: get_unified_preset_list returning {len(all_presets)} presets")
|
||||||
return all_presets
|
return all_presets
|
||||||
|
|
||||||
def save_preset_unified(preset_data, name, save_to_blend_file=False):
|
def save_preset_unified(preset_data, name, save_to_blend_file=False):
|
||||||
pass
|
|
||||||
"""
|
"""
|
||||||
Save a preset to either persistent file or blend file based on user choice.
|
Save a preset to either persistent file or blend file based on user choice.
|
||||||
Default behavior saves to persistent file.
|
Default behavior saves to persistent file.
|
||||||
|
Always cleans up the alternative location to prevent shadowing/duplication.
|
||||||
"""
|
"""
|
||||||
|
print(f"DEBUG: save_preset_unified - Name: '{name}', SaveToBlend: {save_to_blend_file}")
|
||||||
|
|
||||||
|
# Deep data log
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
data_dump = json.dumps(preset_data, indent=2)
|
||||||
|
print(f"DEBUG: FULL PRESET DATA PAYLOAD:\n{data_dump}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Error dumping preset_data: {e}")
|
||||||
|
|
||||||
if save_to_blend_file:
|
if save_to_blend_file:
|
||||||
pass
|
print(f"DEBUG: Saving to Blend File: {name}")
|
||||||
return save_blend_file_preset(name, preset_data)
|
success = save_blend_file_preset(name, preset_data)
|
||||||
|
|
||||||
|
# Clean up from persistent storage to avoid duplication
|
||||||
|
try:
|
||||||
|
persistent_dir = get_persistent_preset_path()
|
||||||
|
persistent_file = os.path.join(persistent_dir, f"{name}.json")
|
||||||
|
if os.path.exists(persistent_file):
|
||||||
|
print(f"DEBUG: Removing shadowed persistent file: {persistent_file}")
|
||||||
|
os.remove(persistent_file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Error removing shadowed persistent preset: {e}")
|
||||||
|
|
||||||
|
return success
|
||||||
else:
|
else:
|
||||||
pass
|
print(f"DEBUG: Saving to Persistent Storage: {name}")
|
||||||
|
# Clean up from blend file to avoid it shadowing the new persistent one
|
||||||
|
try:
|
||||||
|
print(f"DEBUG: Attempting to delete from blend file if exists: {name}")
|
||||||
|
delete_blend_file_preset(name)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Error removing shadowed blend preset: {e}")
|
||||||
|
|
||||||
# Save to persistent file
|
# Save to persistent file
|
||||||
try:
|
try:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -261,9 +261,32 @@ def _iter_components_by_placement(props, placement_mode):
|
|||||||
overlays = list(props.image_overlays)
|
overlays = list(props.image_overlays)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
overlays = props.image_overlays
|
overlays = props.image_overlays
|
||||||
|
|
||||||
|
scene_name = getattr(bpy.context.scene, "name", "UNKNOWN")
|
||||||
|
print(f"DEBUG: _iter_components_by_placement [Scene: {scene_name}] - TOTAL overlays in collection: {len(overlays)} (Checking for mode: {placement_mode})")
|
||||||
|
|
||||||
|
# Detailed dumping of EVERY component regardless of filter
|
||||||
|
for i, comp in enumerate(overlays):
|
||||||
|
name = getattr(comp, "name", "unnamed")
|
||||||
|
pmode = getattr(comp, "placement_mode", "NONE")
|
||||||
|
ctype = getattr(comp, "component_type", "NONE")
|
||||||
|
enabled = getattr(comp, "enabled", False)
|
||||||
|
print(f" -> Collection Item {i}: Name='{name}', Mode='{pmode}', Type='{ctype}', Enabled={enabled}")
|
||||||
|
|
||||||
for index, component in enumerate(overlays):
|
for index, component in enumerate(overlays):
|
||||||
if getattr(component, "placement_mode", 'ABSOLUTE') == placement_mode:
|
comp_name = getattr(component, "name", f"Overlay {index}")
|
||||||
|
comp_placement = getattr(component, "placement_mode", 'ABSOLUTE')
|
||||||
|
|
||||||
|
if not comp_placement or comp_placement not in ('ABSOLUTE', 'PREPEND', 'APPEND'):
|
||||||
|
print(f"DEBUG: Component '{comp_name}' has invalid placement '{comp_placement}'. Forcing to ABSOLUTE.")
|
||||||
|
comp_placement = 'ABSOLUTE'
|
||||||
|
|
||||||
|
if comp_placement == placement_mode:
|
||||||
|
print(f"DEBUG: MATCH FOUND for '{comp_name}' (Index: {index}, Type: {getattr(component, 'component_type', 'UNKNOWN')})")
|
||||||
yield index, component
|
yield index, component
|
||||||
|
else:
|
||||||
|
# Only log skips if there were actually components to skip
|
||||||
|
print(f"DEBUG: Skipping '{comp_name}' (Placement '{comp_placement}' != requested '{placement_mode}')")
|
||||||
|
|
||||||
|
|
||||||
def _draw_component_header(row, component):
|
def _draw_component_header(row, component):
|
||||||
@@ -346,7 +369,9 @@ def draw_component_section(layout, props, placement_mode, title, icon):
|
|||||||
add_op.placement_mode = placement_mode
|
add_op.placement_mode = placement_mode
|
||||||
|
|
||||||
components = list(_iter_components_by_placement(props, placement_mode))
|
components = list(_iter_components_by_placement(props, placement_mode))
|
||||||
|
print(f"DEBUG: draw_component_section for {placement_mode} - found {len(components)} components")
|
||||||
if not components:
|
if not components:
|
||||||
|
section.label(text="No components added", icon='INFO')
|
||||||
return
|
return
|
||||||
|
|
||||||
for index, component in components:
|
for index, component in components:
|
||||||
@@ -357,20 +382,42 @@ def draw_component_section(layout, props, placement_mode, title, icon):
|
|||||||
_draw_component_details(box, component)
|
_draw_component_details(box, component)
|
||||||
|
|
||||||
def draw_composition_section(layout, props):
|
def draw_composition_section(layout, props):
|
||||||
pass
|
|
||||||
"""Draw combined composition controls for text and image components."""
|
"""Draw combined composition controls for text and image components."""
|
||||||
if not has_image_overlays():
|
# Bulletproof Visibility: Even if locked, show header with count if data exists
|
||||||
pass
|
has_overlays = has_image_overlays()
|
||||||
draw_feature_lock_indicator(layout, 'image_overlays')
|
total_count = len(props.image_overlays)
|
||||||
|
if not has_overlays:
|
||||||
|
if total_count > 0:
|
||||||
|
layout.label(text=f"Composition ({total_count}) 🔒 PRO", icon='LOCKED')
|
||||||
|
layout.label(text="Upgrade to edit existing components")
|
||||||
|
else:
|
||||||
|
draw_feature_lock_indicator(layout, 'image_overlays')
|
||||||
return
|
return
|
||||||
|
|
||||||
content = layout.column(align=True)
|
content = layout.column(align=True)
|
||||||
content.use_property_split = False
|
content.use_property_split = False
|
||||||
content.use_property_decorate = False
|
content.use_property_decorate = False
|
||||||
|
|
||||||
draw_component_section(content, props, 'ABSOLUTE', "Absolute Components", 'CURSOR')
|
# Enhanced Header with Counter
|
||||||
draw_component_section(content, props, 'PREPEND', "Prepended Components", 'TRIA_LEFT_BAR')
|
count_abs = len(list(_iter_components_by_placement(props, 'ABSOLUTE')))
|
||||||
draw_component_section(content, props, 'APPEND', "Appended Components", 'TRIA_RIGHT_BAR')
|
count_pre = len(list(_iter_components_by_placement(props, 'PREPEND')))
|
||||||
|
count_app = len(list(_iter_components_by_placement(props, 'APPEND')))
|
||||||
|
|
||||||
|
draw_component_section(content, props, 'ABSOLUTE', f"Absolute Components ({count_abs})", 'CURSOR')
|
||||||
|
draw_component_section(content, props, 'PREPEND', f"Prepended Components ({count_pre})", 'TRIA_LEFT_BAR')
|
||||||
|
draw_component_section(content, props, 'APPEND', f"Appended Components ({count_app})", 'TRIA_RIGHT_BAR')
|
||||||
|
|
||||||
|
# Recovery Utility (Safety Net)
|
||||||
|
content.separator(factor=1.5)
|
||||||
|
diag_box = content.box()
|
||||||
|
diag_row = diag_box.row(align=True)
|
||||||
|
diag_row.label(text="Data Status:", icon='INFO')
|
||||||
|
if total_count > 0:
|
||||||
|
diag_row.label(text="✓ OK", icon='CHECKMARK')
|
||||||
|
else:
|
||||||
|
diag_row.label(text="⚠ EMPTY", icon='ERROR')
|
||||||
|
|
||||||
|
diag_box.operator("text_texture.recover_data", text="Scan for Lost Components", icon='FILE_REFRESH')
|
||||||
|
|
||||||
def draw_positioning_section(layout, props):
|
def draw_positioning_section(layout, props):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ from .system import (
|
|||||||
install_cairosvg_from_bundle,
|
install_cairosvg_from_bundle,
|
||||||
get_system_fonts,
|
get_system_fonts,
|
||||||
get_font_enum_items,
|
get_font_enum_items,
|
||||||
find_default_truetype_font
|
find_default_truetype_font,
|
||||||
|
log_addon_configuration
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -45,5 +46,6 @@ __all__ = [
|
|||||||
'has_shadow_glow_effects',
|
'has_shadow_glow_effects',
|
||||||
'has_shader_generation',
|
'has_shader_generation',
|
||||||
'is_free_version',
|
'is_free_version',
|
||||||
'is_full_version'
|
'is_full_version',
|
||||||
|
'log_addon_configuration'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ VERSION_TYPE = "full" # "free" or "full"
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Text Texture Generator" + (" Free" if VERSION_TYPE == "free" else " Full"),
|
"name": "Text Texture Generator" + (" Free" if VERSION_TYPE == "free" else " Full"),
|
||||||
"author": "Marc Mintel <marc@mintel.me>",
|
"author": "Marc Mintel <marc@mintel.me>",
|
||||||
"version": (1, 1, 0),
|
"version": (1, 2, 4),
|
||||||
"blender": (4, 0, 0),
|
"blender": (4, 0, 0),
|
||||||
"location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab",
|
"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" + (" - Free Version (Limited Features)" if VERSION_TYPE == "free" else " - Full Version"),
|
"description": "Generate image textures from text with accurate dimensions and instant live preview" + (" - Free Version (Limited Features)" if VERSION_TYPE == "free" else " - Full Version"),
|
||||||
|
|||||||
@@ -265,3 +265,39 @@ def find_default_truetype_font():
|
|||||||
|
|
||||||
_DEFAULT_FONT_CACHE = None
|
_DEFAULT_FONT_CACHE = None
|
||||||
return _DEFAULT_FONT_CACHE
|
return _DEFAULT_FONT_CACHE
|
||||||
|
|
||||||
|
def log_addon_configuration():
|
||||||
|
"""Log the entire addon configuration and system info to the console."""
|
||||||
|
from .constants import bl_info, VERSION_TYPE, ENABLED_FEATURES, get_version_limits
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Header
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(f"TEXT TEXTURE GENERATOR - INITIALIZATION")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Basic Info
|
||||||
|
version_str = ".".join(map(str, bl_info['version']))
|
||||||
|
print(f"Version: {version_str} ({VERSION_TYPE.upper()})")
|
||||||
|
print(f"Platform: {platform.system()} ({platform.release()})")
|
||||||
|
print(f"Python: {sys.version.split(' ')[0]}")
|
||||||
|
print(f"OS Node: {platform.node()}")
|
||||||
|
|
||||||
|
# Features
|
||||||
|
print("-" * 60)
|
||||||
|
print("ENABLED FEATURES:")
|
||||||
|
# Wrap features for better readability
|
||||||
|
features = sorted(ENABLED_FEATURES)
|
||||||
|
for i in range(0, len(features), 3):
|
||||||
|
print(f" - {', '.join(features[i:i+3])}")
|
||||||
|
|
||||||
|
# Limits
|
||||||
|
print("-" * 60)
|
||||||
|
limits = get_version_limits()
|
||||||
|
print("ADDON LIMITS:")
|
||||||
|
for key, value in limits.items():
|
||||||
|
name = key.replace('_', ' ').title()
|
||||||
|
print(f" - {name}: {value}")
|
||||||
|
|
||||||
|
print("="*60 + "\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user