diff --git a/build/file_exclusions.json b/build/file_exclusions.json index 172542e..36dc5d4 100644 --- a/build/file_exclusions.json +++ b/build/file_exclusions.json @@ -16,7 +16,7 @@ }, "full": { "description": "Full version - all features included", - "exclude_files": ["blender_manifest.toml"], + "exclude_files": [], "exclude_patterns": [], "max_concurrent_operations": 4 } diff --git a/dist/text_texture_generator_v1.0.0.zip b/dist/text_texture_generator_v1.0.0.zip index f761834..71f1f86 100644 Binary files a/dist/text_texture_generator_v1.0.0.zip and b/dist/text_texture_generator_v1.0.0.zip differ diff --git a/dist/text_texture_generator_v1.0.0_free.zip b/dist/text_texture_generator_v1.0.0_free.zip index 19d4181..2d0f733 100644 Binary files a/dist/text_texture_generator_v1.0.0_free.zip and b/dist/text_texture_generator_v1.0.0_free.zip differ diff --git a/dist/text_texture_generator_v1.1.0.zip b/dist/text_texture_generator_v1.1.0.zip index abfd64f..ab74e9e 100644 Binary files a/dist/text_texture_generator_v1.1.0.zip and b/dist/text_texture_generator_v1.1.0.zip differ diff --git a/dist/text_texture_generator_v1.1.0_free.zip b/dist/text_texture_generator_v1.1.0_free.zip index 7e14aab..0f500ff 100644 Binary files a/dist/text_texture_generator_v1.1.0_free.zip and b/dist/text_texture_generator_v1.1.0_free.zip differ diff --git a/marketing/showcase.blend b/marketing/showcase.blend index aa9db4b..ad68b3c 100644 Binary files a/marketing/showcase.blend and b/marketing/showcase.blend differ diff --git a/src/__init__.py b/src/__init__.py index a893631..5f0aea7 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "Text Texture Generator Pro", "author": "Marc Mintel ", - "version": (1, 1, 0), + "version": (1, 2, 4), "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 powerful styling options", @@ -14,8 +14,19 @@ bl_info = { import sys, os, platform _platform = platform.system().lower() _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: 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 bmesh @@ -27,7 +38,7 @@ import json import time # 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) try: @@ -100,6 +111,7 @@ try: TEXT_TEXTURE_OT_refresh_fonts, TEXT_TEXTURE_OT_set_anchor_point, TEXT_TEXTURE_OT_sync_margins, + TEXT_TEXTURE_OT_recover_data, ) UTILITY_OPERATORS_AVAILABLE = True except ImportError: @@ -116,6 +128,7 @@ except ImportError: TEXT_TEXTURE_OT_refresh_fonts = MockOperator TEXT_TEXTURE_OT_set_anchor_point = MockOperator TEXT_TEXTURE_OT_sync_margins = MockOperator + TEXT_TEXTURE_OT_recover_data = MockOperator # Conditionally import preset-related operators if PRESETS_AVAILABLE: @@ -185,6 +198,7 @@ classes = ( TEXT_TEXTURE_OT_refresh_presets, TEXT_TEXTURE_OT_export_presets, TEXT_TEXTURE_OT_import_presets, + TEXT_TEXTURE_OT_recover_data, TEXT_TEXTURE_PT_panel, TEXT_TEXTURE_PT_panel_3d, TEXT_TEXTURE_PT_panel_properties, @@ -202,6 +216,15 @@ def _is_mock_class(cls): ) 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 global _registered_classes _registered_classes.clear() @@ -276,10 +299,32 @@ def register(): print(f"Error reloading presets: {e}") import traceback 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 if on_file_load not in bpy.app.handlers.load_post: 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(): diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 1a9454d..0000000 Binary files a/src/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/src/core/__pycache__/generation_engine.cpython-311.pyc b/src/core/__pycache__/generation_engine.cpython-311.pyc deleted file mode 100644 index 5fe96b0..0000000 Binary files a/src/core/__pycache__/generation_engine.cpython-311.pyc and /dev/null differ diff --git a/src/core/generation_engine.py b/src/core/generation_engine.py index 8229f4a..3a8ef04 100644 --- a/src/core/generation_engine.py +++ b/src/core/generation_engine.py @@ -331,8 +331,8 @@ def generate_texture_image(props, width, height): 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 inter_spacing_factor = float(getattr(component, 'image_spacing', 0.0) or 0.0) / 100.0 - spacing_pixels = spacing_factor * width - inter_pixels = inter_spacing_factor * width + spacing_pixels = spacing_factor * base_font_size + inter_pixels = inter_spacing_factor * base_font_size if placement == 'PREPEND': cursor -= spacing_pixels @@ -353,8 +353,11 @@ def generate_texture_image(props, width, height): center_y = base_center_y y_pos = int(round(center_y - entry_height / 2.0)) x_pos = int(round(x_pos)) - y_pos = max(0, min(height - entry_height, y_pos)) - x_pos = max(0, min(width - entry_width, x_pos)) + # For sequential layouts (PREPEND/APPEND), we don't strictly clamp. + # 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) assigned.append(entry) return assigned @@ -423,6 +426,26 @@ def generate_texture_image(props, width, height): pass 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 if props.prepend_append_layout == 'HORIZONTAL': pass @@ -452,11 +475,12 @@ def generate_texture_image(props, width, height): text_parts_with_spacing.append(part) full_text = " ".join(text_parts_with_spacing) + layout_mode = "horizontal" else: - pass # Vertical layout - keep parts separate for individual positioning full_text = "\n".join(full_text_parts) + layout_mode = "vertical" @@ -497,6 +521,11 @@ def generate_texture_image(props, width, height): # Determine font size (with text fitting if enabled) 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(): pass @@ -509,22 +538,34 @@ def generate_texture_image(props, width, height): optimal_size = min_size 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) 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_height = bbox[3] - bbox[1] - except (ValueError, AttributeError): - # Fallback for bitmap fonts - test_width, test_height = measure_draw.textsize(full_text, font=test_font) + except Exception: + # Fallback gracefully if font measurement fails + 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: - pass + # Add dynamic components width + spacing mathematically + 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 else: - pass break font_size = optimal_size @@ -553,13 +594,14 @@ def generate_texture_image(props, width, height): for part in full_text_parts: pass 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_width = bbox[2] - bbox[0] baseline = base_ascent - except (ValueError, AttributeError): - # Fallback for bitmap fonts - part_width, part_height = measure_draw.textsize(part, font=font) + except Exception: + # Fallback if font measurement fails + part_width = font_size * len(part) * 0.6 + stroke_width * 2 + part_height = font_size + stroke_width * 2 baseline = base_ascent part_heights.append(part_height) 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) before_layers_applied = True - draw = ImageDraw.Draw(pil_img) - - # Draw each part - current_y = start_y + from PIL import ImageFilter 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): - pass part_width = part_widths[i] 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] if i < len(full_text_parts) - 1: # Add spacing except after last part 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: pass @@ -616,22 +697,60 @@ def generate_texture_image(props, width, height): bottom_offset = 0 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_height = text_bbox[3] - text_bbox[1] left_offset = text_bbox[0] right_offset = text_bbox[2] top_offset = text_bbox[1] bottom_offset = text_bbox[3] - except (ValueError, AttributeError): + except Exception: # 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 bottom_offset = text_height - # Center text - x = (width - text_width) // 2 + # Calculate total widths for PREPEND and APPEND components + # 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 + left = x + left_offset right = x + right_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) before_layers_applied = True - draw = ImageDraw.Draw(pil_img) - - # Draw text + from PIL import ImageFilter 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: pass diff --git a/src/operators/__init__.py b/src/operators/__init__.py index bca6060..cd94dd9 100644 --- a/src/operators/__init__.py +++ b/src/operators/__init__.py @@ -92,6 +92,7 @@ try: TEXT_TEXTURE_OT_duplicate_overlay, TEXT_TEXTURE_OT_delete_overlay, TEXT_TEXTURE_OT_move_overlay, + TEXT_TEXTURE_OT_recover_data, ) UTILITY_OPERATORS = [ 'TEXT_TEXTURE_OT_refresh_preview', @@ -105,6 +106,7 @@ try: 'TEXT_TEXTURE_OT_duplicate_overlay', 'TEXT_TEXTURE_OT_delete_overlay', 'TEXT_TEXTURE_OT_move_overlay', + 'TEXT_TEXTURE_OT_recover_data', ] except ImportError: pass diff --git a/src/operators/__pycache__/preset_ops.cpython-311.pyc b/src/operators/__pycache__/preset_ops.cpython-311.pyc deleted file mode 100644 index 20f884c..0000000 Binary files a/src/operators/__pycache__/preset_ops.cpython-311.pyc and /dev/null differ diff --git a/src/operators/generation_ops.py b/src/operators/generation_ops.py index 8287528..b1b4ded 100644 --- a/src/operators/generation_ops.py +++ b/src/operators/generation_ops.py @@ -723,7 +723,18 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): if space.type == 'NODE_EDITOR': pass 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() break diff --git a/src/operators/preset_ops.py b/src/operators/preset_ops.py index 13dace5..8f57d45 100644 --- a/src/operators/preset_ops.py +++ b/src/operators/preset_ops.py @@ -35,13 +35,11 @@ class TEXT_TEXTURE_OT_save_preset(Operator): ) def execute(self, context): - pass 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() + 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: - pass self.report({'ERROR'}, "Please enter a preset name") return {'CANCELLED'} @@ -67,15 +65,13 @@ class TEXT_TEXTURE_OT_save_preset(Operator): return {'CANCELLED'} # 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: - pass # 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")): - pass + while os.path.exists(os.path.join(get_persistent_preset_path(), f"{suggested_name}.json")): counter += 1 suggested_name = f"{base_name}_{counter}" @@ -107,6 +103,7 @@ class TEXT_TEXTURE_OT_save_preset(Operator): 'use_alpha': props.use_alpha, 'emission_strength': props.emission_strength, 'auto_assign_material': props.auto_assign_material, + 'custom_material_name': getattr(props, 'custom_material_name', ''), 'disable_shadows': props.disable_shadows, 'disable_reflections': props.disable_reflections, 'disable_backfacing': props.disable_backfacing, @@ -129,14 +126,32 @@ class TEXT_TEXTURE_OT_save_preset(Operator): 'glow_color': list(props.glow_color), 'enable_blur': props.enable_blur, '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': [] } # DETAILED LOGGING: Verify shader properties in preset_data # Save overlay data - for overlay in props.image_overlays: - pass + print(f"DEBUG: TEXT_TEXTURE_OT_save_preset - Found {len(props.image_overlays)} overlays in Scene") + for i, overlay in enumerate(props.image_overlays): overlay_data = { 'name': overlay.name, 'component_type': overlay.component_type, @@ -152,6 +167,7 @@ class TEXT_TEXTURE_OT_save_preset(Operator): 'z_index': overlay.z_index, '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) try: @@ -227,10 +243,8 @@ class TEXT_TEXTURE_OT_save_preset(Operator): preset_name = props.new_preset_name.strip() if preset_name and preset_name != "New Preset": - pass - 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): - pass return context.window_manager.invoke_confirm(self, event) return self.execute(context) @@ -276,8 +290,8 @@ 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'} + bl_description = "Load the selected preset and optionally recreate materials. Non-undoable to preserve volatile properties." + bl_options = {'REGISTER'} preset_name: StringProperty() @@ -305,26 +319,51 @@ class TEXT_TEXTURE_OT_load_preset(Operator): 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 - # Skip loading text to prevent overwriting current text - # props.text = preset_data.get('text', props.text) + def _safe_assign_color(prop_array, new_values): + 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_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) + 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_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.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) + + # --- MIGRATION: LEGACY PREPEND/APPEND TEXT --- + # Instead of loading into hidden legacy fields, process them immediately + # 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) # 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 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 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.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) + if 'stroke_color' in preset_data: + _safe_assign_color(props.stroke_color, preset_data['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) + if 'shadow_color' in preset_data: + _safe_assign_color(props.shadow_color, preset_data['shadow_color']) # Handle new shadow_spread property with backward compatibility if hasattr(props, 'shadow_spread'): pass 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) + if 'glow_color' in preset_data: + _safe_assign_color(props.glow_color, preset_data['glow_color']) # Load blur settings 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.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: - pass - 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.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) + # Load enhanced positioning settings + props.anchor_point = preset_data.get('anchor_point', props.anchor_point) + props.position_mode = preset_data.get('position_mode', props.position_mode) + props.margin_x = preset_data.get('margin_x', props.margin_x) + props.margin_y = preset_data.get('margin_y', props.margin_y) + props.margins_linked = preset_data.get('margins_linked', props.margins_linked) + props.offset_x = preset_data.get('offset_x', props.offset_x) + props.offset_y = preset_data.get('offset_y', props.offset_y) + props.manual_position_x = preset_data.get('manual_position_x', props.manual_position_x) + props.manual_position_y = preset_data.get('manual_position_y', props.manual_position_y) + props.manual_position_unit = preset_data.get('manual_position_unit', props.manual_position_unit) + props.constrain_to_canvas = preset_data.get('constrain_to_canvas', props.constrain_to_canvas) + + # Deep data log for loading + import json + try: + data_dump = json.dumps(preset_data, indent=2) + print(f"DEBUG: TEXT_TEXTURE_OT_load_preset - LOADING PAYLOAD:\n{data_dump}") + 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 @@ -424,6 +525,12 @@ class TEXT_TEXTURE_OT_load_preset(Operator): 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) + # 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"): pass props.last_loaded_preset = self.preset_name @@ -477,10 +584,10 @@ class TEXT_TEXTURE_OT_delete_preset(Operator): try: 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) - 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 if os.path.exists(preset_file): pass diff --git a/src/operators/utility_ops.py b/src/operators/utility_ops.py index 49e954c..15be7fa 100644 --- a/src/operators/utility_ops.py +++ b/src/operators/utility_ops.py @@ -358,7 +358,7 @@ class TEXT_TEXTURE_OT_add_overlay(Operator): ) 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 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' 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 for area in context.screen.areas: - pass area.tag_redraw() self.report({'INFO'}, f"Added component: {overlay.name}") @@ -649,6 +650,83 @@ class TEXT_TEXTURE_OT_move_overlay(Operator): return {'CANCELLED'} 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 __all__ = [ @@ -664,4 +742,5 @@ __all__ = [ 'TEXT_TEXTURE_OT_duplicate_overlay', 'TEXT_TEXTURE_OT_delete_overlay', 'TEXT_TEXTURE_OT_move_overlay', + 'TEXT_TEXTURE_OT_recover_data', ] diff --git a/src/presets/__pycache__/storage_system.cpython-311.pyc b/src/presets/__pycache__/storage_system.cpython-311.pyc deleted file mode 100644 index 7e5d003..0000000 Binary files a/src/presets/__pycache__/storage_system.cpython-311.pyc and /dev/null differ diff --git a/src/presets/storage_system.py b/src/presets/storage_system.py index 42a1bf4..e26139d 100644 --- a/src/presets/storage_system.py +++ b/src/presets/storage_system.py @@ -200,21 +200,18 @@ def get_blend_file_presets(): return {} def save_blend_file_preset(preset_name, preset_data): - pass """Save a preset to the current blend file""" try: - pass + print(f"DEBUG: save_blend_file_preset - Name: '{preset_name}'") import bpy import json scene = bpy.context.scene # DETAILED LOGGING: Function entry if isinstance(preset_data, dict): - pass - print(f"Preset data keys: {list(preset_data.keys())}") + print(f"DEBUG: Preset keys to save: {list(preset_data.keys())}") else: - pass - print("ERROR: preset_data is not a dictionary!") + print("DEBUG ERROR: preset_data is not a dictionary!") # Get existing presets presets = get_blend_file_presets() @@ -263,10 +260,9 @@ def save_blend_file_preset(preset_name, preset_data): return False def delete_blend_file_preset(preset_name): - pass """Delete a preset from the current blend file""" try: - pass + print(f"DEBUG: delete_blend_file_preset - Name: '{preset_name}'") import bpy import json scene = bpy.context.scene @@ -276,18 +272,15 @@ def delete_blend_file_preset(preset_name): # Remove the preset if it exists if preset_name in presets: - pass del presets[preset_name] 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 else: - pass - print(f"Preset '{preset_name}' not found in blend file") + print(f"DEBUG: Preset '{preset_name}' not found in blend file for deletion") return False except Exception as e: - pass - print(f"Error deleting preset from blend file: {e}") + print(f"DEBUG ERROR deleting preset from blend file: {e}") return False def migrate_from_legacy_system(): @@ -429,22 +422,51 @@ def get_unified_preset_list(): all_presets[name] = (data, 'BLEND_FILE') except Exception as e: - pass - print(f"Error in get_unified_preset_list: {e}") + print(f"DEBUG: Error in get_unified_preset_list: {e}") + print(f"DEBUG: get_unified_preset_list returning {len(all_presets)} presets") return all_presets 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. 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: - pass - return save_blend_file_preset(name, preset_data) + print(f"DEBUG: Saving to Blend File: {name}") + 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: - 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 try: pass diff --git a/src/ui/panels.py b/src/ui/panels.py index d712017..497baf0 100644 --- a/src/ui/panels.py +++ b/src/ui/panels.py @@ -261,9 +261,32 @@ def _iter_components_by_placement(props, placement_mode): overlays = list(props.image_overlays) except TypeError: 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): - 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 + 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): @@ -346,7 +369,9 @@ def draw_component_section(layout, props, placement_mode, title, icon): add_op.placement_mode = 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: + section.label(text="No components added", icon='INFO') return for index, component in components: @@ -357,20 +382,42 @@ def draw_component_section(layout, props, placement_mode, title, icon): _draw_component_details(box, component) def draw_composition_section(layout, props): - pass """Draw combined composition controls for text and image components.""" - if not has_image_overlays(): - pass - draw_feature_lock_indicator(layout, 'image_overlays') + # Bulletproof Visibility: Even if locked, show header with count if data exists + has_overlays = has_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 content = layout.column(align=True) content.use_property_split = False content.use_property_decorate = False - draw_component_section(content, props, 'ABSOLUTE', "Absolute Components", 'CURSOR') - draw_component_section(content, props, 'PREPEND', "Prepended Components", 'TRIA_LEFT_BAR') - draw_component_section(content, props, 'APPEND', "Appended Components", 'TRIA_RIGHT_BAR') + # Enhanced Header with Counter + count_abs = len(list(_iter_components_by_placement(props, 'ABSOLUTE'))) + 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): pass diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 8518c48..c842fbb 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -23,7 +23,8 @@ from .system import ( install_cairosvg_from_bundle, get_system_fonts, get_font_enum_items, - find_default_truetype_font + find_default_truetype_font, + log_addon_configuration ) __all__ = [ @@ -45,5 +46,6 @@ __all__ = [ 'has_shadow_glow_effects', 'has_shader_generation', 'is_free_version', - 'is_full_version' + 'is_full_version', + 'log_addon_configuration' ] diff --git a/src/utils/constants.py b/src/utils/constants.py index 9ec49e9..f78d7e9 100644 --- a/src/utils/constants.py +++ b/src/utils/constants.py @@ -8,7 +8,7 @@ VERSION_TYPE = "full" # "free" or "full" bl_info = { "name": "Text Texture Generator" + (" Free" if VERSION_TYPE == "free" else " Full"), "author": "Marc Mintel ", - "version": (1, 1, 0), + "version": (1, 2, 4), "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" + (" - Free Version (Limited Features)" if VERSION_TYPE == "free" else " - Full Version"), diff --git a/src/utils/system.py b/src/utils/system.py index c62cb97..db69425 100644 --- a/src/utils/system.py +++ b/src/utils/system.py @@ -265,3 +265,39 @@ def find_default_truetype_font(): _DEFAULT_FONT_CACHE = None 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")