diff --git a/Archive.zip b/Archive.zip index 82128ce..73e6992 100644 Binary files a/Archive.zip and b/Archive.zip differ diff --git a/BLENDERMARKET_LISTING.md b/BLENDERMARKET_LISTING.md index 8e14d6f..cdde33c 100644 --- a/BLENDERMARKET_LISTING.md +++ b/BLENDERMARKET_LISTING.md @@ -45,10 +45,12 @@ Cross-platform compatibility across Windows, macOS, and Linux environments, comb ### Smart Preset Management -- **User-Defined Presets**: Save and organize custom configurations +- **Three-Tier Preset System**: Blend File, Persistent, and Local preset storage with proper UI display +- **User-Defined Presets**: Save and organize custom configurations with improved interface - **Project-Specific Presets**: Maintain consistent styling across project assets - **Preset Import/Export**: Share configurations between team members - **Version Control**: Preset versioning for workflow tracking +- **Enhanced UI Display**: Recent improvements ensure all preset types display correctly in the interface ### Material Integration diff --git a/GUMROAD_LISTING.md b/GUMROAD_LISTING.md index 4f63eb9..f645f0b 100644 --- a/GUMROAD_LISTING.md +++ b/GUMROAD_LISTING.md @@ -36,7 +36,7 @@ From indie game developers to 3D artists and creative hobbyists, this tool saves - Professional text effects: outlines, drop shadows, glowing effects - Normal map generation for realistic 3D depth - Complete color customization with transparency support -- Smart preset management - save and share your favorite styles +- Smart preset management with improved display - save and share your favorite styles across three storage types (Blend File, Persistent, Local) - Instant Blender material creation and assignment - Batch processing capabilities for multiple textures diff --git a/MARKETING_REVIEW_AND_SOCIAL_GUIDE.md b/MARKETING_REVIEW_AND_SOCIAL_GUIDE.md index 0b8ce1e..4fbd3af 100644 --- a/MARKETING_REVIEW_AND_SOCIAL_GUIDE.md +++ b/MARKETING_REVIEW_AND_SOCIAL_GUIDE.md @@ -281,7 +281,7 @@ Key Features: • 9-point positioning system for precise placement • Professional effects: shadows, glows, strokes, normal maps • Batch processing for multiple texts -• Three-tier preset system (travels with .blend files!) +• Improved three-tier preset system (Blend File, Persistent, Local) with enhanced UI display • Multiline text support with auto-wrapping • Complete shader generation with material assignment • Cross-platform: Windows, macOS, Linux diff --git a/README.md b/README.md index 91d4eb9..9c02491 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,9 @@ A modern Blender addon that generates image textures from text with extensive cu - **Preset Management System**: - - Three-tier storage: Blend file, Persistent, Legacy - - Presets travel with .blend files + - Three-tier storage: Blend File, Persistent, Local presets + - All preset types now display properly in the interface + - Presets travel with .blend files (Blend File type) - Import/export functionality for backup and sharing - Automatic migration system for addon updates - Conflict resolution for preset imports @@ -269,12 +270,13 @@ The addon features a sophisticated three-tier preset system: - Stored in Blender's user config directory - Shared across all blend files -3. **Legacy File Storage** (Lowest Priority) - - Backward compatibility with old addon versions - - Automatically migrated to persistent storage +3. **Local File Storage** (Lowest Priority) + - Local addon-specific presets + - Automatically migrated to persistent storage when needed ### Preset Features +- **Improved UI Display**: Recent bug fix ensures all three preset types (Blend File, Persistent, Local) display properly in the interface - **Auto-Migration**: Seamlessly upgrades presets during addon updates - **Import/Export**: Backup and share presets across installations - **Conflict Resolution**: Smart handling of duplicate preset names diff --git a/__init__.py b/__init__.py index 0c84cc0..75ff392 100644 --- a/__init__.py +++ b/__init__.py @@ -1251,89 +1251,6 @@ def draw_multiline_text_with_stroke(draw, lines, font, text_color, props, canvas if lines and lines[0]: draw.text((10, 10), lines[0], fill=text_color, font=font) -def apply_tiling(image, props): - """Apply tiling effects to the image.""" - try: - if not props.enable_tiling: - return image - - from PIL import Image - - # Get original image dimensions - orig_width, orig_height = image.size - - # Determine tile counts based on mode - if props.tiling_mode == 'TILE' or props.tiling_mode == 'MIRROR': - tile_x = props.tile_count_x - tile_y = props.tile_count_y - elif props.tiling_mode == 'REPEAT_X': - tile_x = props.tile_count_x - tile_y = 1 - elif props.tiling_mode == 'REPEAT_Y': - tile_x = 1 - tile_y = props.tile_count_y - else: - return image - - # Calculate spacing - spacing = props.tile_spacing - - # Calculate tile dimensions - if props.expand_tiling_canvas: - # Expand canvas to fit all tiles - tile_width = orig_width - tile_height = orig_height - canvas_width = tile_x * tile_width + (tile_x - 1) * spacing - canvas_height = tile_y * tile_height + (tile_y - 1) * spacing - else: - # Scale down tiles to fit in original canvas - available_width = orig_width - (tile_x - 1) * spacing - available_height = orig_height - (tile_y - 1) * spacing - tile_width = max(1, available_width // tile_x) - tile_height = max(1, available_height // tile_y) - canvas_width = orig_width - canvas_height = orig_height - - # Resize the original image if needed - if tile_width != orig_width or tile_height != orig_height: - image = image.resize((tile_width, tile_height), Image.LANCZOS) - - # Create the tiled canvas - tiled_image = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0)) - - # Generate tiles - for row in range(tile_y): - for col in range(tile_x): - # Calculate position - x = col * (tile_width + spacing) - y = row * (tile_height + spacing) - - # Get the tile image based on tiling mode - if props.tiling_mode == 'MIRROR': - # Mirror alternating tiles for checkerboard pattern - tile_image = image.copy() - if (row + col) % 2 == 1: - # Alternate between horizontal and vertical mirroring - if row % 2 == 1: - tile_image = tile_image.transpose(Image.FLIP_TOP_BOTTOM) - if col % 2 == 1: - tile_image = tile_image.transpose(Image.FLIP_LEFT_RIGHT) - else: - # Regular tile (TILE, REPEAT_X, REPEAT_Y) - tile_image = image - - # Paste the tile - if tile_image.mode == 'RGBA': - tiled_image.paste(tile_image, (x, y), tile_image) - else: - tiled_image.paste(tile_image, (x, y)) - - return tiled_image - - except Exception as e: - print(f"Error applying tiling: {e}") - return image - def generate_texture_image(props, width, height): """Generate the actual texture image with overlays - width and height are REQUIRED @@ -2014,12 +1931,23 @@ def generate_texture_image(props, width, height): # MULTILINE TEXT PROCESSING with stroke support elif props.enable_multiline: print("DEBUG MULTILINE: Processing multiline text with stroke support") + print(f"DEBUG MULTILINE: Text image mode: {text_img.mode}, size: {text_img.size}") # Process multiline text lines = process_multiline_text(text, font, props) print(f"DEBUG MULTILINE: Processed into {len(lines)} lines: {lines}") + # DEBUG: Check text_img before multiline drawing + pre_draw_pixels = list(text_img.getdata()) + pre_draw_non_transparent = sum(1 for pixel in pre_draw_pixels if len(pixel) >= 4 and pixel[3] > 0) + print(f"DEBUG MULTILINE: Text image has {pre_draw_non_transparent} non-transparent pixels BEFORE multiline drawing") + # Draw multiline text with stroke support draw_multiline_text_with_stroke(draw, lines, font, text_color, props, width, height) + + # DEBUG: Check text_img after multiline drawing + post_draw_pixels = list(text_img.getdata()) + post_draw_non_transparent = sum(1 for pixel in post_draw_pixels if len(pixel) >= 4 and pixel[3] > 0) + print(f"DEBUG MULTILINE: Text image has {post_draw_non_transparent} non-transparent pixels AFTER multiline drawing") print("DEBUG MULTILINE: Multiline text rendering with stroke completed") else: @@ -2030,14 +1958,35 @@ def generate_texture_image(props, width, height): print(f"Text drawn at ({x}, {y}) with stroke: {props.enable_stroke}") print(f"DEBUG UPPERCASE ISSUE - Text drawing with stroke completed") - # Add shadow/glow effects if enabled + # CRITICAL FIX: Move shadow/glow generation AFTER text rendering + # The issue was that shadow/glow effects were being created before text was fully rendered + # This caused invisible shadows because they were created from empty text images + + # Apply blur to main text if enabled (before shadow/glow generation) + if props.enable_blur and props.blur_radius > 0: + print(f"DEBUG BLUR: Applying blur effect - radius: {props.blur_radius}") + text_img = text_img.filter(ImageFilter.GaussianBlur(radius=props.blur_radius)) + print(f"DEBUG BLUR: Blur effect applied successfully") + + # Now generate shadow/glow effects AFTER text is fully rendered shadow_glow_img = None if props.enable_shadow or props.enable_glow: print(f"DEBUG SHADOW/GLOW: Generating shadow/glow effects - shadow: {props.enable_shadow}, glow: {props.enable_glow}") shadow_glow_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) + # DEBUG: Check source text_img after full rendering + source_pixels = list(text_img.getdata()) + source_non_transparent = sum(1 for pixel in source_pixels if len(pixel) >= 4 and pixel[3] > 0) + print(f"DEBUG SHADOW: Source text_img has {source_non_transparent} non-transparent pixels (after full text rendering)") + + if source_non_transparent == 0: + print(f"WARNING: Source text image is still empty after full rendering! Skipping shadow/glow.") + shadow_glow_img = None + else: + print(f"SUCCESS: Source text image has content, proceeding with shadow/glow generation") + # Apply effects in rendering order: shadow first, then glow - if props.enable_shadow: + if shadow_glow_img and props.enable_shadow: print(f"DEBUG SHADOW: Creating drop shadow - offset: ({props.shadow_offset_x}, {props.shadow_offset_y}), blur: {props.shadow_blur}") # Create shadow by offsetting and blurring the text @@ -2046,21 +1995,44 @@ def generate_texture_image(props, width, height): # Apply shadow color to the text (keep alpha channel from original) shadow_pixels = list(shadow_img.getdata()) shadow_color_rgb = tuple(int(c * 255) for c in props.shadow_color[:3]) - shadow_alpha_multiplier = props.shadow_color[3] if len(props.shadow_color) > 3 else 1.0 + + # Enhanced shadow intensity calculation + base_alpha_multiplier = props.shadow_color[3] if len(props.shadow_color) > 3 else 1.0 + shadow_spread = getattr(props, 'shadow_spread', 1.0) # Fallback for compatibility + # Apply shadow spread to the offset distances + shadow_offset_x_spread = int(shadow_offset_x * shadow_spread) + shadow_offset_y_spread = int(shadow_offset_y * shadow_spread) + + # Keep the original alpha multiplier (no intensity-based changes) + final_alpha_multiplier = base_alpha_multiplier + + # DEBUG: Log shadow color and alpha values + print(f"DEBUG SHADOW: Shadow color RGB: {shadow_color_rgb}") + print(f"DEBUG SHADOW: Base alpha multiplier: {base_alpha_multiplier}") + print(f"DEBUG SHADOW: Shadow spread: {shadow_spread}") + print(f"DEBUG SHADOW: Shadow offset X spread: {shadow_offset_x_spread}") + print(f"DEBUG SHADOW: Shadow offset Y spread: {shadow_offset_y_spread}") + print(f"DEBUG SHADOW: Final alpha multiplier: {final_alpha_multiplier}") + print(f"DEBUG SHADOW: Original shadow_img size: {shadow_img.size}") + print(f"DEBUG SHADOW: Original shadow_img mode: {shadow_img.mode}") new_shadow_pixels = [] + modified_pixel_count = 0 for pixel in shadow_pixels: if len(pixel) >= 4: # RGBA r, g, b, a = pixel if a > 0: # Only modify non-transparent pixels - # Apply shadow color and alpha - new_a = int(a * shadow_alpha_multiplier) + # Apply shadow color and enhanced alpha with intensity + new_a = int(min(255, a * final_alpha_multiplier)) new_shadow_pixels.append((*shadow_color_rgb, new_a)) + modified_pixel_count += 1 else: new_shadow_pixels.append(pixel) else: # RGB new_shadow_pixels.append((*shadow_color_rgb, 255)) + modified_pixel_count += 1 + print(f"DEBUG SHADOW: Modified {modified_pixel_count} pixels out of {len(shadow_pixels)}") shadow_img.putdata(new_shadow_pixels) # Apply blur if specified @@ -2070,20 +2042,36 @@ def generate_texture_image(props, width, height): # Create offset shadow image offset_shadow_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) - # Calculate offset positions - offset_x = int(props.shadow_offset_x) - offset_y = int(props.shadow_offset_y) + # Calculate offset positions with spread applied + offset_x = shadow_offset_x_spread + offset_y = shadow_offset_y_spread + + print(f"DEBUG SHADOW: Offset position: ({offset_x}, {offset_y})") + print(f"DEBUG SHADOW: Shadow image dimensions: {shadow_img.size}") + print(f"DEBUG SHADOW: Canvas dimensions: {width}x{height}") # Paste shadow at offset position try: offset_shadow_img.paste(shadow_img, (offset_x, offset_y), shadow_img) + print(f"DEBUG SHADOW: Successfully pasted shadow at offset ({offset_x}, {offset_y})") except Exception as e: print(f"DEBUG SHADOW: Error pasting shadow: {e}") # Fallback to center if offset fails offset_shadow_img.paste(shadow_img, (0, 0), shadow_img) + print(f"DEBUG SHADOW: Used fallback position (0, 0)") + + # Check if offset_shadow_img has any non-transparent pixels + offset_shadow_pixels = list(offset_shadow_img.getdata()) + non_transparent_count = sum(1 for pixel in offset_shadow_pixels if len(pixel) >= 4 and pixel[3] > 0) + print(f"DEBUG SHADOW: Offset shadow has {non_transparent_count} non-transparent pixels out of {len(offset_shadow_pixels)}") # Composite shadow with shadow_glow_img shadow_glow_img = Image.alpha_composite(shadow_glow_img, offset_shadow_img) + + # Check final shadow_glow_img + final_shadow_pixels = list(shadow_glow_img.getdata()) + final_non_transparent_count = sum(1 for pixel in final_shadow_pixels if len(pixel) >= 4 and pixel[3] > 0) + print(f"DEBUG SHADOW: Final shadow_glow_img has {final_non_transparent_count} non-transparent pixels") print(f"DEBUG SHADOW: Drop shadow applied successfully") if props.enable_glow: @@ -2125,7 +2113,7 @@ def generate_texture_image(props, width, height): elements.append((-1, 'shadow_glow', shadow_glow_img, 0, 0)) print(f"DEBUG SHADOW/GLOW: Added shadow/glow to elements list at z-index -1") - # Add text to elements list + # Add text to elements list (blur was already applied before shadow/glow generation) elements.append((0, 'text', text_img, 0, 0)) # Sort elements by z-index @@ -2134,13 +2122,22 @@ def generate_texture_image(props, width, height): # Composite all elements final_img = base_img.copy() for z_index, element_type, element_img, pos_x, pos_y in elements: + print(f"DEBUG COMPOSITING: Processing element type '{element_type}' at z-index {z_index}") if element_type == 'text': final_img = Image.alpha_composite(final_img, element_img) + print(f"DEBUG COMPOSITING: Text element composited successfully") elif element_type == 'image': # Create a temporary image to paste the overlay temp_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) temp_img.paste(element_img, (pos_x, pos_y), element_img) final_img = Image.alpha_composite(final_img, temp_img) + print(f"DEBUG COMPOSITING: Image element composited successfully") + elif element_type == 'shadow_glow': + # BUG FIX: Add missing handler for shadow/glow effects + final_img = Image.alpha_composite(final_img, element_img) + print(f"DEBUG COMPOSITING: Shadow/glow element composited successfully") + else: + print(f"DEBUG COMPOSITING: WARNING - Unknown element type '{element_type}' skipped!") print(f"Final composite image created with {len(elements)} elements") @@ -2159,9 +2156,6 @@ def generate_texture_image(props, width, height): else: print(f"[Normal Map] Failed to generate normal map") - # Apply tiling if enabled - if props.enable_tiling: - final_img = apply_tiling(final_img, props) return final_img, normal_map_img @@ -2956,6 +2950,13 @@ class TEXT_TEXTURE_Properties(PropertyGroup): default=True ) + custom_material_name: StringProperty( + name="Custom Material Name", + description="Custom name for the generated material (leave empty to use text-based naming)", + default="", + maxlen=128 + ) + disable_shadows: BoolProperty( name="Disable Shadows", description="Make material not cast shadows using light path", @@ -3114,7 +3115,7 @@ class TEXT_TEXTURE_Properties(PropertyGroup): auto_wrap: BoolProperty( name="Auto Wrap", description="Automatically wrap text to fit texture width", - default=False, + default=True, update=update_live_preview ) @@ -3214,7 +3215,7 @@ class TEXT_TEXTURE_Properties(PropertyGroup): shadow_offset_x: FloatProperty( name="Shadow Offset X", description="Horizontal offset for drop shadow", - default=5.0, + default=8.0, min=-100.0, max=100.0, step=0.1, @@ -3225,7 +3226,7 @@ class TEXT_TEXTURE_Properties(PropertyGroup): shadow_offset_y: FloatProperty( name="Shadow Offset Y", description="Vertical offset for drop shadow", - default=5.0, + default=8.0, min=-100.0, max=100.0, step=0.1, @@ -3236,7 +3237,7 @@ class TEXT_TEXTURE_Properties(PropertyGroup): shadow_blur: FloatProperty( name="Shadow Blur", description="Blur radius for drop shadow", - default=5.0, + default=6.0, min=0.0, max=50.0, step=0.1, @@ -3248,13 +3249,24 @@ class TEXT_TEXTURE_Properties(PropertyGroup): name="Shadow Color", description="Color of drop shadow", subtype='COLOR', - default=(0.0, 0.0, 0.0, 0.7), + default=(0.0, 0.0, 0.0, 1.0), size=4, min=0.0, max=1.0, update=update_live_preview ) + shadow_spread: FloatProperty( + name="Shadow Spread", + description="Controls how far the shadow extends from the text (multiplies shadow offset)", + default=1.0, + min=0.0, + max=3.0, + precision=2, + step=0.1, + update=update_live_preview + ) + enable_glow: BoolProperty( name="Enable Glow", description="Enable glow effect", @@ -3284,120 +3296,35 @@ class TEXT_TEXTURE_Properties(PropertyGroup): update=update_live_preview ) + # ============================================================================ - # TILING SYSTEM PROPERTIES + # BLUR PROPERTIES # ============================================================================ - expand_tiling: BoolProperty( - name="Tiling System", - description="Show/hide Tiling System section", + expand_blur: BoolProperty( + name="Blur Effects", + description="Show/hide Blur Effects section", default=False ) - enable_tiling: BoolProperty( - name="Enable Tiling", - description="Enable tiling to repeat text patterns across texture space", + enable_blur: BoolProperty( + name="Enable Blur", + description="Enable blur effect on main text content", default=False, update=update_live_preview ) - tiling_mode: EnumProperty( - name="Tiling Mode", - description="How the texture should be tiled/repeated", - items=[ - ('TILE', "Tile", "Simple repetition of the texture"), - ('MIRROR', "Mirror", "Alternating normal/mirrored tiles"), - ('REPEAT_X', "Repeat X", "Tile horizontally only"), - ('REPEAT_Y', "Repeat Y", "Tile vertically only") - ], - default='TILE', + blur_radius: FloatProperty( + name="Blur Radius", + description="Blur radius for text content (0-50 pixels)", + default=2.0, + min=0.0, + max=50.0, + step=0.1, + precision=1, update=update_live_preview ) - tile_count_x: IntProperty( - name="Tile Count X", - description="Number of horizontal tiles", - default=2, - min=1, - max=10, - update=update_live_preview - ) - - tile_count_y: IntProperty( - name="Tile Count Y", - description="Number of vertical tiles", - default=2, - min=1, - max=10, - update=update_live_preview - ) - - tile_spacing: IntProperty( - name="Tile Spacing", - description="Spacing between tiles in pixels", - default=0, - min=0, - max=100, - update=update_live_preview - ) - - expand_tiling_canvas: BoolProperty( - name="Expand Canvas", - description="Expand canvas to fit all tiles instead of scaling down", - default=True, - update=update_live_preview - ) - - # ============================================================================ - # BATCH PROCESSING PROPERTIES - # ============================================================================ - - expand_batch_processing: BoolProperty( - name="Batch Processing", - description="Show/hide Batch Processing section", - default=False - ) - - batch_word_list: StringProperty( - name="Word List", - description="Comma-separated list of words/phrases for batch processing", - default="", - maxlen=8192 - ) - - batch_output_folder: StringProperty( - name="Output Folder", - description="Folder path for batch output files", - default="//batch_output/", - subtype='DIR_PATH' - ) - - batch_filename_prefix: StringProperty( - name="Filename Prefix", - description="Prefix for batch output filenames", - default="text_texture_" - ) - - batch_auto_save: BoolProperty( - name="Auto Save", - description="Automatically save each generated texture", - default=True - ) - - batch_progress: IntProperty( - name="Progress", - description="Current batch processing progress", - default=0, - min=0, - max=100, - subtype='PERCENTAGE' - ) - - batch_template_name: StringProperty( - name="Template Name", - description="Name for saving/loading batch templates", - default="batch_template" - ) # ============================================================================ # OPERATORS @@ -3593,9 +3520,14 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): normal_map_blender_img.pack() print(f"[Normal Map] Created normal map texture for shader: {normal_map_name}") - # Create or get material - material_name = f"TextTexture_Mat_{props.text[:15].replace(' ', '_')}" - + # Create or get material using custom name or fallback + if props.custom_material_name.strip(): + material_name = props.custom_material_name.strip() + else: + # Fallback to text-based naming + clean_text = props.text[:15].replace(' ', '_').replace('\n', '_') + material_name = f"TextTexture_Mat_{clean_text}" + if material_name in bpy.data.materials: mat = bpy.data.materials[material_name] # Clear existing nodes @@ -3938,8 +3870,18 @@ class TEXT_TEXTURE_OT_generate_shader(Operator): # Assign material to active object if enabled and possible if props.auto_assign_material and context.active_object and context.active_object.type in ['MESH', 'CURVE']: obj = context.active_object - # Always create a new material slot - obj.data.materials.append(mat) + # Check if material slot exists, reuse if found, otherwise create new one + slot_found = False + for i, slot in enumerate(obj.material_slots): + if slot.material and slot.material.name == material_name: + # Material already exists in a slot, update it + obj.material_slots[i].material = mat + slot_found = True + break + + if not slot_found: + # Create new material slot if not found + obj.data.materials.append(mat) self.report({'INFO'}, f"Generated shader '{material_name}' and assigned to {obj.name}") else: @@ -4244,15 +4186,12 @@ class TEXT_TEXTURE_OT_save_preset(Operator): 'shadow_offset_y': props.shadow_offset_y, 'shadow_blur': props.shadow_blur, 'shadow_color': list(props.shadow_color), + 'shadow_spread': getattr(props, 'shadow_spread', 1.0), 'enable_glow': props.enable_glow, 'glow_blur': props.glow_blur, 'glow_color': list(props.glow_color), - 'enable_tiling': props.enable_tiling, - 'tiling_mode': props.tiling_mode, - 'tile_count_x': props.tile_count_x, - 'tile_count_y': props.tile_count_y, - 'tile_spacing': props.tile_spacing, - 'expand_tiling_canvas': props.expand_tiling_canvas, + 'enable_blur': props.enable_blur, + 'blur_radius': props.blur_radius, 'image_overlays': [] } @@ -4562,17 +4501,16 @@ class TEXT_TEXTURE_OT_load_preset(Operator): props.shadow_offset_y = preset_data.get('shadow_offset_y', props.shadow_offset_y) props.shadow_blur = preset_data.get('shadow_blur', props.shadow_blur) props.shadow_color = preset_data.get('shadow_color', props.shadow_color) + # Handle new shadow_spread property with backward compatibility + if hasattr(props, 'shadow_spread'): + props.shadow_spread = preset_data.get('shadow_spread', 1.0) props.enable_glow = preset_data.get('enable_glow', props.enable_glow) props.glow_blur = preset_data.get('glow_blur', props.glow_blur) props.glow_color = preset_data.get('glow_color', props.glow_color) - # Load tiling settings - props.enable_tiling = preset_data.get('enable_tiling', props.enable_tiling) - props.tiling_mode = preset_data.get('tiling_mode', props.tiling_mode) - props.tile_count_x = preset_data.get('tile_count_x', props.tile_count_x) - props.tile_count_y = preset_data.get('tile_count_y', props.tile_count_y) - props.tile_spacing = preset_data.get('tile_spacing', props.tile_spacing) - props.expand_tiling_canvas = preset_data.get('expand_tiling_canvas', props.expand_tiling_canvas) + # Load blur settings + props.enable_blur = preset_data.get('enable_blur', props.enable_blur) + props.blur_radius = preset_data.get('blur_radius', props.blur_radius) # Load text fitting settings props.enable_text_fitting = preset_data.get('enable_text_fitting', props.enable_text_fitting) @@ -5122,371 +5060,6 @@ class TEXT_TEXTURE_OT_show_migration_report(Operator): return {'CANCELLED'} -# ============================================================================ -# BATCH PROCESSING OPERATORS -# ============================================================================ - -class TEXT_TEXTURE_OT_batch_generate(Operator): - """Generate textures for multiple words/phrases in batch""" - bl_idname = "text_texture.batch_generate" - bl_label = "Generate Batch" - bl_description = "Generate textures for all words in the batch list" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.text_texture_props - - # Parse word list - if not props.batch_word_list.strip(): - self.report({'ERROR'}, "Word list is empty") - return {'CANCELLED'} - - # Split by commas and clean up words - words = [word.strip() for word in props.batch_word_list.split(',')] - words = [word for word in words if word] # Remove empty strings - - if not words: - self.report({'ERROR'}, "No valid words found in list") - return {'CANCELLED'} - - # Store original text - original_text = props.text - - # Create output directory if it doesn't exist and auto-save is enabled - if props.batch_auto_save: - output_path = bpy.path.abspath(props.batch_output_folder) - import os - try: - os.makedirs(output_path, exist_ok=True) - except OSError as e: - self.report({'ERROR'}, f"Failed to create output directory: {e}") - return {'CANCELLED'} - - # Process each word - total_words = len(words) - successful_generations = 0 - - for i, word in enumerate(words): - # Update progress - props.batch_progress = int((i / total_words) * 100) - - # Set current word as text input - props.text = word - - try: - # Generate texture using existing generation operator - result = bpy.ops.text_texture.generate() - if result == {'FINISHED'}: - successful_generations += 1 - - # Save if auto-save is enabled - if props.batch_auto_save and context.scene.text_texture_image: - # Create safe filename - safe_word = "".join(c for c in word if c.isalnum() or c in (' ', '-', '_')).rstrip() - safe_word = safe_word.replace(' ', '_') - filename = f"{props.batch_filename_prefix}{safe_word}.png" - filepath = os.path.join(output_path, filename) - - # Save the image - try: - context.scene.text_texture_image.filepath_raw = filepath - context.scene.text_texture_image.file_format = 'PNG' - context.scene.text_texture_image.save() - except Exception as save_error: - self.report({'WARNING'}, f"Failed to save '{word}': {save_error}") - - except Exception as gen_error: - self.report({'WARNING'}, f"Failed to generate texture for '{word}': {gen_error}") - - # Update UI to show progress - for area in context.screen.areas: - area.tag_redraw() - - # Restore original text - props.text = original_text - props.batch_progress = 100 - - # Final report - if successful_generations == total_words: - self.report({'INFO'}, f"✅ Successfully generated {successful_generations} textures") - elif successful_generations > 0: - self.report({'WARNING'}, f"Generated {successful_generations}/{total_words} textures (some failed)") - else: - self.report({'ERROR'}, "Failed to generate any textures") - - return {'FINISHED'} - - -class TEXT_TEXTURE_OT_import_word_list(Operator): - """Import word list from text file""" - bl_idname = "text_texture.import_word_list" - bl_label = "Import Word List" - bl_description = "Import word list from a text file" - bl_options = {'REGISTER', 'UNDO'} - - filepath: StringProperty( - name="File Path", - description="Path to text file containing word list", - subtype='FILE_PATH' - ) - - filter_glob: StringProperty( - default="*.txt", - options={'HIDDEN'} - ) - - def execute(self, context): - props = context.scene.text_texture_props - - try: - with open(self.filepath, 'r', encoding='utf-8') as file: - content = file.read() - - # Support both comma-separated and line-separated words - if ',' in content: - words = [word.strip() for word in content.split(',')] - else: - words = [line.strip() for line in content.splitlines()] - - # Filter out empty words - words = [word for word in words if word] - - if words: - props.batch_word_list = ', '.join(words) - self.report({'INFO'}, f"✅ Imported {len(words)} words") - else: - self.report({'WARNING'}, "No words found in file") - - except Exception as e: - self.report({'ERROR'}, f"Failed to import file: {str(e)}") - return {'CANCELLED'} - - return {'FINISHED'} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - - -class TEXT_TEXTURE_OT_export_word_list(Operator): - """Export word list to text file""" - bl_idname = "text_texture.export_word_list" - bl_label = "Export Word List" - bl_description = "Export current word list to a text file" - bl_options = {'REGISTER', 'UNDO'} - - filepath: StringProperty( - name="File Path", - description="Path to save text file", - subtype='FILE_PATH', - default="word_list.txt" - ) - - filter_glob: StringProperty( - default="*.txt", - options={'HIDDEN'} - ) - - def execute(self, context): - props = context.scene.text_texture_props - - if not props.batch_word_list.strip(): - self.report({'ERROR'}, "Word list is empty") - return {'CANCELLED'} - - try: - words = [word.strip() for word in props.batch_word_list.split(',')] - words = [word for word in words if word] # Remove empty strings - - with open(self.filepath, 'w', encoding='utf-8') as file: - for word in words: - file.write(word + '\n') - - self.report({'INFO'}, f"✅ Exported {len(words)} words to {os.path.basename(self.filepath)}") - - except Exception as e: - self.report({'ERROR'}, f"Failed to export file: {str(e)}") - return {'CANCELLED'} - - return {'FINISHED'} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - - -class TEXT_TEXTURE_OT_save_batch_template(Operator): - """Save current settings as batch template""" - bl_idname = "text_texture.save_batch_template" - bl_label = "Save Batch Template" - bl_description = "Save current settings as a reusable batch template" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.text_texture_props - - if not props.batch_template_name.strip(): - self.report({'ERROR'}, "Template name is required") - return {'CANCELLED'} - - # Create template data including all relevant settings - template_data = { - # Core text settings - 'font_size': props.font_size, - 'font_color': list(props.font_color), - 'background_transparent': props.background_transparent, - 'background_color': list(props.background_color), - 'font_path': props.font_path, - 'custom_font_path': props.custom_font_path, - 'use_custom_font': props.use_custom_font, - 'text_align': props.text_align, - 'padding': props.padding, - - # Texture settings - 'texture_width': props.texture_width, - 'texture_height': props.texture_height, - - # Multiline settings - 'enable_multiline': props.enable_multiline, - 'line_height': props.line_height, - 'text_alignment': props.text_alignment, - 'auto_wrap': props.auto_wrap, - 'wrap_width': props.wrap_width, - 'max_lines': props.max_lines, - 'line_spacing_pixels': props.line_spacing_pixels, - - # Stroke settings - 'enable_stroke': props.enable_stroke, - 'stroke_width': props.stroke_width, - 'stroke_position': props.stroke_position, - 'stroke_color': list(props.stroke_color), - - # Shadow/Glow settings - 'enable_shadow': props.enable_shadow, - 'shadow_offset_x': props.shadow_offset_x, - 'shadow_offset_y': props.shadow_offset_y, - 'shadow_blur': props.shadow_blur, - 'shadow_color': list(props.shadow_color), - 'enable_glow': props.enable_glow, - 'glow_blur': props.glow_blur, - 'glow_color': list(props.glow_color), - - # Tiling settings - 'enable_tiling': props.enable_tiling, - 'tiling_mode': props.tiling_mode, - 'tile_count_x': props.tile_count_x, - 'tile_count_y': props.tile_count_y, - 'tile_spacing': props.tile_spacing, - 'expand_tiling_canvas': props.expand_tiling_canvas, - - # Batch specific settings - 'batch_output_folder': props.batch_output_folder, - 'batch_filename_prefix': props.batch_filename_prefix, - 'batch_auto_save': props.batch_auto_save, - } - - # Save to presets with special prefix for batch templates - template_key = f"batch_{props.batch_template_name}" - - # Save to blend file (highest priority) - blend_success = save_blend_file_preset(template_key, template_data) - - # Also save to persistent storage for cross-file sharing - persistent_success = False - try: - persistent_file = os.path.join(get_persistent_preset_path(), f"{template_key}.json") - with open(persistent_file, 'w') as f: - json.dump(template_data, f, indent=2) - persistent_success = True - except Exception as e: - print(f"Warning: Failed to save batch template to persistent storage: {e}") - - if blend_success or persistent_success: - storage_info = "" - if blend_success and persistent_success: - storage_info = " (saved to blend file + persistent storage)" - elif blend_success: - storage_info = " (saved to blend file)" - elif persistent_success: - storage_info = " (saved to persistent storage)" - - self.report({'INFO'}, f"✅ Saved batch template: {props.batch_template_name}{storage_info}") - else: - self.report({'ERROR'}, "Failed to save batch template") - return {'CANCELLED'} - - return {'FINISHED'} - - -class TEXT_TEXTURE_OT_load_batch_template(Operator): - """Load batch template settings""" - bl_idname = "text_texture.load_batch_template" - bl_label = "Load Batch Template" - bl_description = "Load previously saved batch template" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.text_texture_props - - if not props.batch_template_name.strip(): - self.report({'ERROR'}, "Template name is required") - return {'CANCELLED'} - - template_key = f"batch_{props.batch_template_name}" - - # Three-tier loading: Blend File > Persistent File > Legacy File - template_data = None - source = 'UNKNOWN' - - # Try blend file first - blend_presets = get_blend_file_presets() - if template_key in blend_presets: - template_data = blend_presets[template_key] - source = 'BLEND_FILE' - else: - # Try persistent storage - persistent_file = os.path.join(get_persistent_preset_path(), f"{template_key}.json") - if os.path.exists(persistent_file): - try: - with open(persistent_file, 'r') as f: - template_data = json.load(f) - source = 'PERSISTENT_FILE' - except Exception as e: - self.report({'ERROR'}, f"Failed to read template: {e}") - return {'CANCELLED'} - - if not template_data: - self.report({'ERROR'}, f"Batch template not found: {props.batch_template_name}") - return {'CANCELLED'} - - # Apply template settings - props.is_updating = True - - try: - # Apply all settings from template - for key, value in template_data.items(): - if hasattr(props, key): - setattr(props, key, value) - - props.is_updating = False - - # Update live preview - generate_preview(context) - - source_info = { - 'BLEND_FILE': "from blend file", - 'PERSISTENT_FILE': "from persistent storage", - 'LOCAL_FILE': "from legacy storage" - }.get(source, "from unknown source") - - self.report({'INFO'}, f"✅ Loaded batch template: {props.batch_template_name} {source_info}") - - except Exception as e: - props.is_updating = False - self.report({'ERROR'}, f"Failed to apply template: {e}") - return {'CANCELLED'} - - return {'FINISHED'} # ============================================================================ @@ -5707,6 +5280,7 @@ def draw_presets_section(layout, props): # Separate presets by source blend_presets = [p for p in props.presets if p.source == 'BLEND_FILE'] + persistent_presets = [p for p in props.presets if p.source == 'PERSISTENT_FILE'] local_presets = [p for p in props.presets if p.source == 'LOCAL_FILE'] # Show blend file presets first @@ -5726,10 +5300,29 @@ def draw_presets_section(layout, props): delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') delete_btn.preset_name = preset.name - # Show local presets second - if local_presets: + # Show persistent presets second + if persistent_presets: if blend_presets: layout.separator() + layout.label(text="🛡️ Persistent Presets (survive addon updates):", icon='PREFERENCES') + for preset in persistent_presets: + row = layout.row() + load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}") + load_btn.preset_name = preset.name + + # Quick overwrite button + overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH') + overwrite_btn.overwrite = True + # Set the preset name so it can be overwritten + overwrite_btn.preset_name = preset.name + + delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') + delete_btn.preset_name = preset.name + + # Show local presets third + if local_presets: + if blend_presets or persistent_presets: + layout.separator() layout.label(text="💻 Local Presets (this machine only):", icon='DISK_DRIVE') for preset in local_presets: row = layout.row() @@ -5978,6 +5571,7 @@ def draw_shadow_glow_section(layout, props): # Shadow properties col = layout.column(align=True) col.prop(props, "shadow_blur", text="Shadow Blur", slider=True) + col.prop(props, "shadow_spread", text="Shadow Spread", slider=True) col.prop(props, "shadow_color", text="Shadow Color") layout.separator() @@ -6008,116 +5602,27 @@ def draw_shadow_glow_section(layout, props): info_box.label(text="• Higher blur = softer effect") info_box.label(text="• Alpha controls effect intensity") -def draw_tiling_section(layout, props): - """Draw tiling system controls""" - # Main toggle for tiling - layout.prop(props, "enable_tiling", text="Enable Tiling", icon='GRID') +def draw_blur_section(layout, props): + """Draw blur effects controls""" + # Main toggle for blur + layout.prop(props, "enable_blur", text="Enable Blur", icon='MOD_SMOOTH') - # Show controls only if tiling is enabled - if props.enable_tiling: + # Show controls only if blur is enabled + if props.enable_blur: layout.separator() - # Tiling mode selection - layout.prop(props, "tiling_mode", text="Tiling Mode") - - layout.separator() - - # Tile count controls - col = layout.column(align=True) - col.label(text="Tile Count:", icon='DUPLICATE') - - # Show appropriate controls based on mode - if props.tiling_mode in ['TILE', 'MIRROR']: - # Both X and Y for full tiling modes - row = col.row(align=True) - row.prop(props, "tile_count_x", text="X") - row.prop(props, "tile_count_y", text="Y") - elif props.tiling_mode == 'REPEAT_X': - # Only X for horizontal repeat - col.prop(props, "tile_count_x", text="X Count") - elif props.tiling_mode == 'REPEAT_Y': - # Only Y for vertical repeat - col.prop(props, "tile_count_y", text="Y Count") - - layout.separator() - - # Canvas and spacing options - col = layout.column(align=True) - col.prop(props, "expand_tiling_canvas", text="Expand Canvas") - col.prop(props, "tile_spacing", text="Tile Spacing") + # Blur radius setting + layout.prop(props, "blur_radius", text="Blur Radius", slider=True) # Info box with usage tips layout.separator() info_box = layout.box() info_box.scale_y = 0.8 - info_box.label(text="💡 Tiling creates repeated patterns:", icon='INFO') - - if props.tiling_mode == 'TILE': - info_box.label(text="• Tile: Simple repetition in grid pattern") - elif props.tiling_mode == 'MIRROR': - info_box.label(text="• Mirror: Alternating normal/mirrored tiles") - elif props.tiling_mode == 'REPEAT_X': - info_box.label(text="• Repeat X: Horizontal tiling only") - elif props.tiling_mode == 'REPEAT_Y': - info_box.label(text="• Repeat Y: Vertical tiling only") - - if props.expand_tiling_canvas: - info_box.label(text="• Expand Canvas: Enlarges texture to fit tiles") - else: - info_box.label(text="• Scale Down: Fits tiles in original size") - -def draw_batch_processing_section(layout, props): - """Draw batch processing controls""" - # Word list management - layout.label(text="Word List:", icon='TEXT') - layout.prop(props, "batch_word_list", text="") - - # Import/Export buttons for word list - row = layout.row(align=True) - row.operator("text_texture.import_word_list", text="Import List", icon='IMPORT') - row.operator("text_texture.export_word_list", text="Export List", icon='EXPORT') - - layout.separator() - - # Output settings - layout.label(text="Output Settings:", icon='FILE_FOLDER') - layout.prop(props, "batch_output_folder", text="Output Folder") - - row = layout.row(align=True) - row.prop(props, "batch_filename_prefix", text="Prefix") - row.prop(props, "batch_auto_save", text="Auto Save", toggle=True) - - layout.separator() - - # Template management - layout.label(text="Template Management:", icon='PRESET') - layout.prop(props, "batch_template_name", text="Template Name") - - row = layout.row(align=True) - row.operator("text_texture.save_batch_template", text="Save Template", icon='FILE_NEW') - row.operator("text_texture.load_batch_template", text="Load Template", icon='FILE_FOLDER') - - layout.separator() - - # Progress display (show only when processing) - if props.batch_progress > 0 and props.batch_progress < 100: - layout.prop(props, "batch_progress", text="Progress", slider=True) - layout.separator() - - # Batch generation button - batch_row = layout.row() - batch_row.scale_y = 1.5 - batch_row.operator("text_texture.batch_generate", text="Generate Batch", icon='PLAY') - - # Info box with usage tips - layout.separator() - info_box = layout.box() - info_box.scale_y = 0.8 - info_box.label(text="💡 Batch Processing:", icon='INFO') - info_box.label(text="• Enter comma-separated words/phrases") - info_box.label(text="• Templates save all current settings") - info_box.label(text="• Auto-save creates PNG files in output folder") - info_box.label(text="• Progress bar shows generation status") + info_box.label(text="💡 Blur effect softens text edges:", icon='INFO') + info_box.label(text="• Applied to main text content only") + info_box.label(text="• Higher radius = softer/more blurred") + info_box.label(text="• Works with all other effects") + info_box.label(text="• Radius 0 = no blur effect") # ============================================================================ # MENUS @@ -6247,10 +5752,10 @@ class TEXT_TEXTURE_PT_panel(Panel): if shadow_glow_section: draw_shadow_glow_section(shadow_glow_section, props) - # Tiling Section (collapsed by default) - tiling_section = draw_collapsible_header(layout, "expand_tiling", "Tiling System", 'GRID') - if tiling_section: - draw_tiling_section(tiling_section, props) + # Blur Section (collapsed by default) + blur_section = draw_collapsible_header(layout, "expand_blur", "Blur Effects", 'MOD_SMOOTH') + if blur_section: + draw_blur_section(blur_section, props) # Normal Maps Section (collapsed by default) normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE') @@ -6348,15 +5853,14 @@ class TEXT_TEXTURE_PT_panel_3d(Panel): if shadow_glow_section: draw_shadow_glow_section(shadow_glow_section, props) - # Tiling Section (collapsed by default) - tiling_section = draw_collapsible_header(layout, "expand_tiling", "Tiling System", 'GRID') - if tiling_section: - draw_tiling_section(tiling_section, props) + # Tiling Section removed - feature was removed from addon - # Batch Processing Section (collapsed by default) - batch_section = draw_collapsible_header(layout, "expand_batch_processing", "Batch Processing", 'LINENUMBERS_ON') - if batch_section: - draw_batch_processing_section(batch_section, props) + # Blur Section (collapsed by default) + blur_section = draw_collapsible_header(layout, "expand_blur", "Blur Effects", 'MOD_SMOOTH') + if blur_section: + draw_blur_section(blur_section, props) + + # Batch Processing Section removed - feature was removed from addon # Normal Maps Section (collapsed by default) normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE') @@ -6443,15 +5947,14 @@ class TEXT_TEXTURE_PT_panel_properties(Panel): if shadow_glow_section: draw_shadow_glow_section(shadow_glow_section, props) - # Tiling Section (collapsed by default) - tiling_section = draw_collapsible_header(layout, "expand_tiling", "Tiling System", 'GRID') - if tiling_section: - draw_tiling_section(tiling_section, props) + # Tiling Section removed - feature was removed from addon - # Batch Processing Section (collapsed by default) - batch_section = draw_collapsible_header(layout, "expand_batch_processing", "Batch Processing", 'LINENUMBERS_ON') - if batch_section: - draw_batch_processing_section(batch_section, props) + # Blur Section (collapsed by default) + blur_section = draw_collapsible_header(layout, "expand_blur", "Blur Effects", 'MOD_SMOOTH') + if blur_section: + draw_blur_section(blur_section, props) + + # Batch Processing Section removed - feature was removed from addon # Normal Maps Section (collapsed by default) normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE') @@ -6494,11 +5997,6 @@ classes = ( TEXT_TEXTURE_OT_export_presets, TEXT_TEXTURE_OT_import_presets, TEXT_TEXTURE_OT_show_migration_report, - TEXT_TEXTURE_OT_batch_generate, - TEXT_TEXTURE_OT_import_word_list, - TEXT_TEXTURE_OT_export_word_list, - TEXT_TEXTURE_OT_save_batch_template, - TEXT_TEXTURE_OT_load_batch_template, TEXT_TEXTURE_PT_panel, TEXT_TEXTURE_PT_panel_3d, TEXT_TEXTURE_PT_panel_properties, diff --git a/demo.blend b/demo.blend index 452f7f6..db0dfbe 100644 Binary files a/demo.blend and b/demo.blend differ diff --git a/demo.blend1 b/demo.blend1 index 0496026..44516cc 100644 Binary files a/demo.blend1 and b/demo.blend1 differ