diff --git a/Archive.zip b/Archive.zip index b5fea3c..00aca0d 100644 Binary files a/Archive.zip and b/Archive.zip differ diff --git a/__init__.py b/__init__.py index 842490f..b9cbbdd 100644 --- a/__init__.py +++ b/__init__.py @@ -94,7 +94,7 @@ def get_font_enum_items(self, context): return items def get_preset_path(): - """Get path for storing presets""" + """Get path for storing presets (legacy local storage)""" addon_dir = os.path.dirname(os.path.realpath(__file__)) presets_dir = os.path.join(addon_dir, "presets") if not os.path.exists(presets_dir): @@ -104,6 +104,173 @@ def get_preset_path(): print(f"Error creating presets directory: {e}") return presets_dir +def get_blend_file_presets(): + """Get presets stored in the current blend file""" + try: + # Get custom properties from the scene + import bpy + scene = bpy.context.scene + + # DETAILED LOGGING: Function entry + print(f"[GET_BLEND_FILE_PRESETS DEBUG] ==================== LOADING FROM BLEND FILE ==================") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Function called to get presets from blend file") + + # Store presets in scene custom properties + if "text_texture_presets" not in scene: + print(f"[GET_BLEND_FILE_PRESETS DEBUG] No 'text_texture_presets' found in scene, creating empty") + scene["text_texture_presets"] = "{}" + else: + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Found 'text_texture_presets' in scene") + + preset_data = scene.get("text_texture_presets", "{}") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data type: {type(preset_data)}") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data length: {len(str(preset_data))} characters") + + if isinstance(preset_data, str): + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is string, attempting JSON parse...") + try: + import json + parsed_presets = json.loads(preset_data) + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Successfully parsed JSON") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets type: {type(parsed_presets)}") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets count: {len(parsed_presets) if isinstance(parsed_presets, dict) else 'N/A'}") + + if isinstance(parsed_presets, dict): + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset names: {list(parsed_presets.keys())}") + + # DETAILED VERIFICATION: Check shader properties in each preset + for preset_name, preset_content in parsed_presets.items(): + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset '{preset_name}' analysis:") + if isinstance(preset_content, dict): + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Keys: {list(preset_content.keys())}") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_shadows: {preset_content.get('disable_shadows', 'NOT FOUND')}") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_reflections: {preset_content.get('disable_reflections', 'NOT FOUND')}") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] disable_backfacing: {preset_content.get('disable_backfacing', 'NOT FOUND')}") + else: + print(f"[GET_BLEND_FILE_PRESETS DEBUG] ERROR: Preset content is not a dict: {type(preset_content)}") + + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Returning parsed presets") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================") + return parsed_presets + + except json.JSONDecodeError as json_err: + print(f"[GET_BLEND_FILE_PRESETS DEBUG] JSON decode error: {json_err}") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw data causing error: {preset_data[:200]}...") # First 200 chars + print("Error parsing blend file presets, resetting to empty") + scene["text_texture_presets"] = "{}" + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Reset to empty, returning empty dict") + return {} + else: + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is not string, returning empty dict") + print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================") + return {} + except Exception as e: + print(f"[GET_BLEND_FILE_PRESETS DEBUG] EXCEPTION occurred: {e}") + import traceback + print(f"[GET_BLEND_FILE_PRESETS DEBUG] Full traceback: {traceback.format_exc()}") + print(f"Error getting blend file presets: {e}") + return {} + +def save_blend_file_preset(preset_name, preset_data): + """Save a preset to the current blend file""" + try: + import bpy + import json + scene = bpy.context.scene + + # DETAILED LOGGING: Function entry + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ==================== SAVING TO BLEND FILE ===================") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Function called with preset_name: '{preset_name}'") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data type: {type(preset_data)}") + if isinstance(preset_data, dict): + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data keys: {list(preset_data.keys())}") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Shader properties in preset_data:") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") + else: + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: preset_data is not a dictionary!") + + # Get existing presets + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Getting existing presets from blend file...") + presets = get_blend_file_presets() + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing presets count: {len(presets)}") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing preset names: {list(presets.keys())}") + + # Add or update the preset + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Adding/updating preset '{preset_name}'...") + presets[preset_name] = preset_data + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Presets now contains {len(presets)} items") + + # VERIFY: Check that shader properties are in the preset we're about to save + if preset_name in presets and isinstance(presets[preset_name], dict): + saved_preset = presets[preset_name] + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] VERIFICATION - Shader properties in final preset:") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {saved_preset.get('disable_shadows', 'NOT FOUND')}") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {saved_preset.get('disable_reflections', 'NOT FOUND')}") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {saved_preset.get('disable_backfacing', 'NOT FOUND')}") + + # Save back to scene custom properties + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Converting presets to JSON...") + json_data = json.dumps(presets, indent=2) + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] JSON data length: {len(json_data)} characters") + + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Saving to scene custom properties...") + scene["text_texture_presets"] = json_data + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Successfully saved to scene['text_texture_presets']") + + # FINAL VERIFICATION: Read back from scene to confirm save + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] FINAL VERIFICATION - Reading back from scene...") + readback_data = scene.get("text_texture_presets", "{}") + if readback_data: + try: + readback_presets = json.loads(readback_data) + if preset_name in readback_presets: + readback_preset = readback_presets[preset_name] + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] READBACK VERIFICATION - Shader properties:") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_shadows: {readback_preset.get('disable_shadows', 'NOT FOUND')}") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_reflections: {readback_preset.get('disable_reflections', 'NOT FOUND')}") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] disable_backfacing: {readback_preset.get('disable_backfacing', 'NOT FOUND')}") + else: + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Preset '{preset_name}' not found in readback data!") + except json.JSONDecodeError as json_err: + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Could not parse readback JSON: {json_err}") + else: + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: No data in scene['text_texture_presets'] after save!") + + print(f"Saved preset '{preset_name}' to blend file") + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ================================================================") + return True + except Exception as e: + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] EXCEPTION occurred: {e}") + import traceback + print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Full traceback: {traceback.format_exc()}") + print(f"Error saving preset to blend file: {e}") + return False + +def delete_blend_file_preset(preset_name): + """Delete a preset from the current blend file""" + try: + import bpy + import json + scene = bpy.context.scene + + # Get existing presets + presets = get_blend_file_presets() + + # Remove the preset if it exists + if preset_name in presets: + del presets[preset_name] + scene["text_texture_presets"] = json.dumps(presets, indent=2) + print(f"Deleted preset '{preset_name}' from blend file") + return True + else: + print(f"Preset '{preset_name}' not found in blend file") + return False + except Exception as e: + print(f"Error deleting preset from blend file: {e}") + return False + def initialize_presets(): """Initialize presets during addon registration - prevents race conditions""" try: @@ -115,19 +282,53 @@ def initialize_presets(): if not props: return - # Clear existing presets - props.presets.clear() + # Get existing preset names for optimization + existing_presets = {p.name: p for p in props.presets} + # Load presets from blend file first (primary source) + blend_presets = get_blend_file_presets() + updated_presets = set() + + for preset_name in blend_presets.keys(): + updated_presets.add(preset_name) + if preset_name in existing_presets: + # Update existing preset source if needed + if existing_presets[preset_name].source != 'BLEND_FILE': + existing_presets[preset_name].source = 'BLEND_FILE' + else: + # Add new preset + preset = props.presets.add() + preset.name = preset_name + preset.source = 'BLEND_FILE' + + # Load legacy local presets as fallback preset_dir = get_preset_path() if os.path.exists(preset_dir): try: for filename in os.listdir(preset_dir): if filename.endswith('.json'): preset_name = os.path.splitext(filename)[0] - preset = props.presets.add() - preset.name = preset_name + # Only add if not already present from blend file + if preset_name not in blend_presets: + updated_presets.add(preset_name) + if preset_name in existing_presets: + # Update existing preset source if needed + if existing_presets[preset_name].source != 'LOCAL_FILE': + existing_presets[preset_name].source = 'LOCAL_FILE' + else: + # Add new preset + preset = props.presets.add() + preset.name = preset_name + preset.source = 'LOCAL_FILE' except OSError as e: - print(f"Error loading presets during initialization: {e}") + print(f"Error loading local presets during initialization: {e}") + + # Remove presets that no longer exist + for i in range(len(props.presets) - 1, -1, -1): + if props.presets[i].name not in updated_presets: + props.presets.remove(i) + + print(f"Initialized {len(props.presets)} presets ({len(blend_presets)} from blend file)") except Exception as e: print(f"Error in initialize_presets: {e}") @@ -315,6 +516,113 @@ def get_system_fallback_fonts(): "/usr/share/fonts/truetype/opensans/OpenSans-Regular.ttf" # Open Sans - mixed-case ] +# ============================================================================ +# NORMAL MAP GENERATION FUNCTIONS +# ============================================================================ + +def generate_normal_map_from_alpha(img, strength=1.0, blur_radius=1.0, invert=False): + """Generate a normal map from an image's alpha channel using height-based algorithm""" + try: + from PIL import Image, ImageFilter + import numpy as np + + print(f"[Normal Map] Starting generation with strength={strength}, blur={blur_radius}, invert={invert}") + + # Extract alpha channel as height map + if img.mode != 'RGBA': + img = img.convert('RGBA') + + # Get alpha channel + alpha_channel = img.split()[3] # Alpha is the 4th channel + width, height = alpha_channel.size + + print(f"[Normal Map] Processing {width}x{height} alpha channel") + + # Apply blur if specified + if blur_radius > 0: + alpha_channel = alpha_channel.filter(ImageFilter.GaussianBlur(radius=blur_radius)) + print(f"[Normal Map] Applied blur with radius {blur_radius}") + + # Convert to numpy array for gradient calculations + height_map = np.array(alpha_channel, dtype=np.float32) / 255.0 + + # Apply strength multiplier + height_map *= strength + + # Calculate gradients using Sobel operators + # Sobel X kernel for horizontal gradients + sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32) + # Sobel Y kernel for vertical gradients + sobel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32) + + # Pad the height map to handle edges + padded_height = np.pad(height_map, ((1, 1), (1, 1)), mode='edge') + + # Calculate gradients + grad_x = np.zeros_like(height_map) + grad_y = np.zeros_like(height_map) + + for i in range(height): + for j in range(width): + # Extract 3x3 neighborhood + neighborhood = padded_height[i:i+3, j:j+3] + # Apply Sobel operators + grad_x[i, j] = np.sum(neighborhood * sobel_x) + grad_y[i, j] = np.sum(neighborhood * sobel_y) + + print(f"[Normal Map] Calculated gradients") + + # Convert gradients to normal vectors + # Normal map RGB values are calculated as: + # R = (grad_x + 1) * 0.5 -> maps -1,1 to 0,1 + # G = (-grad_y + 1) * 0.5 -> maps -1,1 to 0,1 (Y is flipped for standard normal maps) + # B = sqrt(1 - grad_x^2 - grad_y^2) -> Z component, always pointing up + + # Clamp gradients to reasonable range + grad_x = np.clip(grad_x, -1, 1) + grad_y = np.clip(grad_y, -1, 1) + + # Apply invert if specified + if invert: + grad_x = -grad_x + grad_y = -grad_y + print(f"[Normal Map] Applied inversion") + + # Calculate normal map channels + # Red channel: X gradient mapped to 0-1 + normal_r = ((grad_x + 1.0) * 0.5 * 255).astype(np.uint8) + + # Green channel: Y gradient mapped to 0-1 (flipped) + normal_g = ((-grad_y + 1.0) * 0.5 * 255).astype(np.uint8) + + # Blue channel: Z component (pointing up) + # Calculate Z from X and Y to maintain unit length + grad_magnitude_sq = grad_x**2 + grad_y**2 + grad_z = np.sqrt(np.maximum(0, 1.0 - grad_magnitude_sq)) + normal_b = (grad_z * 255).astype(np.uint8) + + print(f"[Normal Map] Calculated normal vectors") + + # Create the normal map image + normal_map = Image.new('RGB', (width, height)) + + # Combine channels + for y in range(height): + for x in range(width): + r = int(normal_r[y, x]) + g = int(normal_g[y, x]) + b = int(normal_b[y, x]) + normal_map.putpixel((x, y), (r, g, b)) + + print(f"[Normal Map] Normal map generation completed successfully") + return normal_map + + except Exception as e: + print(f"[Normal Map] Error generating normal map: {e}") + import traceback + traceback.print_exc() + return None + # ============================================================================ # ENHANCED POSITIONING FUNCTIONS # ============================================================================ @@ -378,7 +686,8 @@ def get_anchor_point_matrix(): ] def generate_texture_image(props, width, height): - """Generate the actual texture image with overlays - width and height are REQUIRED""" + """Generate the actual texture image with overlays - width and height are REQUIRED + Returns: (diffuse_image, normal_map_image) or (diffuse_image, None) if normal map disabled""" try: install_pillow() from PIL import Image, ImageDraw, ImageFont @@ -392,6 +701,7 @@ def generate_texture_image(props, width, height): height = max(16, height) print(f"DEBUG: Generating texture - requested: {width}x{height}, texture settings: {props.texture_width}x{props.texture_height}") + print(f"DEBUG: Normal map generation enabled: {props.generate_normal_map}") # Scale font size proportionally for preview scale_factor = width / props.texture_width @@ -687,13 +997,29 @@ def generate_texture_image(props, width, height): final_img = Image.alpha_composite(final_img, temp_img) print(f"Final composite image created with {len(elements)} elements") - return final_img + + # Generate normal map if enabled + normal_map_img = None + if props.generate_normal_map: + print(f"[Normal Map] Generating normal map with settings: strength={props.normal_map_strength}, blur={props.normal_map_blur_radius}, invert={props.normal_map_invert}") + normal_map_img = generate_normal_map_from_alpha( + final_img, + strength=props.normal_map_strength, + blur_radius=props.normal_map_blur_radius, + invert=props.normal_map_invert + ) + if normal_map_img: + print(f"[Normal Map] Successfully generated normal map") + else: + print(f"[Normal Map] Failed to generate normal map") + + return final_img, normal_map_img except Exception as e: print(f"Error in generate_texture_image: {e}") import traceback traceback.print_exc() - return None + return None, None def generate_preview(context): """Generate preview texture""" @@ -715,12 +1041,24 @@ def generate_preview(context): print(f"Starting preview generation at {preview_w}x{preview_h} (full resolution)") print(f"DEBUG: Texture size is {props.texture_width}x{props.texture_height}, preview at {preview_w}x{preview_h}") - # Generate the image - img = generate_texture_image(props, preview_w, preview_h) - if not img: + # Generate the images (diffuse and normal map) + result = generate_texture_image(props, preview_w, preview_h) + if not result: print("Failed to generate PIL image") return None + # Extract diffuse image (and optionally normal map) + if isinstance(result, tuple): + img, normal_map_img = result + else: + # Backward compatibility in case function returns single image + img = result + normal_map_img = None + + if not img: + print("Failed to generate diffuse image") + return None + # Apply preview background if needed if props.preview_bg_type != 'transparent': from PIL import Image, ImageDraw @@ -923,6 +1261,16 @@ class TEXT_TEXTURE_Preset(PropertyGroup): description="Name of the preset", default="New Preset" ) + + source: EnumProperty( + name="Source", + description="Where this preset is stored", + items=[ + ('BLEND_FILE', 'Blend File', 'Preset stored in the blend file (travels with the file)'), + ('LOCAL_FILE', 'Local File', 'Preset stored locally (legacy system)') + ], + default='BLEND_FILE' + ) class TEXT_TEXTURE_Properties(PropertyGroup): """Properties for text texture generation""" @@ -1359,6 +1707,120 @@ class TEXT_TEXTURE_Properties(PropertyGroup): description="Internal flag for Enter key save requests", default=False ) + + # ============================================================================ + # SHADER GENERATION PROPERTIES + # ============================================================================ + + expand_shader: BoolProperty( + name="Shader Settings", + description="Show/hide Shader Settings section", + default=False + ) + + shader_type: EnumProperty( + name="Shader Type", + description="Type of shader to generate", + items=[ + ('PRINCIPLED', 'Principled BSDF', 'Standard PBR shader with text texture'), + ('EMISSION', 'Emission', 'Emissive shader for glowing text effects'), + ('TRANSPARENT', 'Transparent', 'Transparent shader with alpha blending'), + ], + default='PRINCIPLED' + ) + + shader_connection: EnumProperty( + name="Texture Connection", + description="How to connect the texture to the shader", + items=[ + ('BASE_COLOR', 'Base Color', 'Connect texture to base color input'), + ('EMISSION', 'Emission', 'Connect texture to emission input'), + ('ALPHA', 'Alpha', 'Connect texture alpha to transparency'), + ], + default='BASE_COLOR' + ) + + use_alpha: BoolProperty( + name="Use Alpha Channel", + description="Use texture alpha channel for transparency", + default=True + ) + + emission_strength: FloatProperty( + name="Emission Strength", + description="Strength of emission shader", + default=1.0, + min=0.0, + max=10.0 + ) + + auto_assign_material: BoolProperty( + name="Auto-Assign Material", + description="Automatically assign generated material to active object", + default=True + ) + + disable_shadows: BoolProperty( + name="Disable Shadows", + description="Make material not cast shadows using light path", + default=False + ) + + disable_reflections: BoolProperty( + name="Disable Reflections", + description="Make material not appear in reflections using light path", + default=False + ) + + disable_backfacing: BoolProperty( + name="Disable Backfacing", + description="Make material not render on backfacing geometry using light path", + default=False + ) + + # ============================================================================ + # NORMAL MAP GENERATION PROPERTIES + # ============================================================================ + + expand_normal_maps: BoolProperty( + name="Normal Maps", + description="Show/hide Normal Maps section", + default=False + ) + + generate_normal_map: BoolProperty( + name="Generate Normal Map", + description="Automatically generate normal map from text alpha channel", + default=False, + update=update_live_preview + ) + + normal_map_strength: FloatProperty( + name="Normal Strength", + description="Intensity of the normal map effect (higher = more pronounced)", + default=1.0, + min=0.1, + max=5.0, + precision=2, + update=update_live_preview + ) + + normal_map_blur_radius: FloatProperty( + name="Blur Radius", + description="Blur radius for smoothing normal map (0 = no blur)", + default=1.0, + min=0.0, + max=10.0, + precision=1, + update=update_live_preview + ) + + normal_map_invert: BoolProperty( + name="Invert Normal Map", + description="Invert the normal map for different height effects (emboss vs deboss)", + default=False, + update=update_live_preview + ) # ============================================================================ # OPERATORS @@ -1382,19 +1844,34 @@ class TEXT_TEXTURE_OT_generate(Operator): print(f"Generating texture at {final_width}x{final_height}px") # Generate at FULL resolution specified by user - img = generate_texture_image(props, final_width, final_height) + result = generate_texture_image(props, final_width, final_height) + if not result: + self.report({'ERROR'}, "Failed to generate texture image") + return {'CANCELLED'} - # Create final name + # Extract diffuse and normal map images + if isinstance(result, tuple): + img, normal_map_img = result + else: + # Backward compatibility + img = result + normal_map_img = None + + if not img: + self.report({'ERROR'}, "Failed to generate diffuse image") + return {'CANCELLED'} + + # Create diffuse texture img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}" - # Remove existing + # Remove existing diffuse texture if img_name in bpy.data.images: bpy.data.images.remove(bpy.data.images[img_name]) - # Create new image with USER SPECIFIED dimensions + # Create new diffuse image with USER SPECIFIED dimensions blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True) - # Convert PIL to Blender + # Convert PIL to Blender for diffuse texture pixels = [] for y in range(final_height): for x in range(final_width): @@ -1404,25 +1881,511 @@ class TEXT_TEXTURE_OT_generate(Operator): blender_img.pixels = pixels blender_img.pack() # PACK FINAL IMAGE + # Create normal map texture if enabled and generated + normal_map_blender_img = None + if normal_map_img and props.generate_normal_map: + normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}" + + # Remove existing normal map texture + if normal_map_name in bpy.data.images: + bpy.data.images.remove(bpy.data.images[normal_map_name]) + + # Create new normal map image + normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False) + normal_map_blender_img.colorspace_settings.name = 'Non-Color' # Important for normal maps + + # Convert PIL to Blender for normal map + normal_pixels = [] + for y in range(final_height): + for x in range(final_width): + r, g, b = normal_map_img.getpixel((x, final_height - 1 - y)) + normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0]) # Alpha always 1.0 for normal maps + + normal_map_blender_img.pixels = normal_pixels + normal_map_blender_img.pack() # PACK NORMAL MAP + print(f"[Normal Map] Created and packed normal map texture: {normal_map_name}") + # Add to shader if context.space_data and context.space_data.type == 'NODE_EDITOR': if context.space_data.tree_type == 'ShaderNodeTree': material = context.space_data.id if material and material.use_nodes: nodes = material.node_tree.nodes + + # Add diffuse texture node img_node = nodes.new(type='ShaderNodeTexImage') img_node.image = blender_img img_node.location = (0, 0) img_node.select = True nodes.active = img_node + + # Add normal map node if normal map was created + if normal_map_blender_img: + normal_node = nodes.new(type='ShaderNodeTexImage') + normal_node.image = normal_map_blender_img + normal_node.location = (0, -300) + normal_node.label = "Normal Map" + print(f"[Normal Map] Added normal map node to shader editor") - self.report({'INFO'}, f"Generated and packed: {img_name} ({final_width}x{final_height}px)") + # Provide comprehensive feedback + if normal_map_blender_img: + self.report({'INFO'}, f"Generated and packed: {img_name} + {normal_map_blender_img.name} ({final_width}x{final_height}px)") + else: + self.report({'INFO'}, f"Generated and packed: {img_name} ({final_width}x{final_height}px)") return {'FINISHED'} except Exception as e: self.report({'ERROR'}, f"Failed: {str(e)}") return {'CANCELLED'} +class TEXT_TEXTURE_OT_generate_shader(Operator): + """Generate complete shader with text texture""" + bl_idname = "text_texture.generate_shader" + bl_label = "Generate Shader" + bl_description = "Generate a complete material shader with the text texture" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + try: + props = context.scene.text_texture_props + + # Generate texture first + final_width = props.texture_width + final_height = props.texture_height + + print(f"Generating shader with texture at {final_width}x{final_height}px") + + result = generate_texture_image(props, final_width, final_height) + if not result: + self.report({'ERROR'}, "Failed to generate texture image") + return {'CANCELLED'} + + # Extract diffuse and normal map images + if isinstance(result, tuple): + img, normal_map_img = result + else: + # Backward compatibility + img = result + normal_map_img = None + + if not img: + self.report({'ERROR'}, "Failed to generate diffuse image") + return {'CANCELLED'} + + # Create diffuse texture name + img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}" + + # Remove existing diffuse texture if it exists + if img_name in bpy.data.images: + bpy.data.images.remove(bpy.data.images[img_name]) + + # Create new diffuse texture + blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True) + + # Convert PIL to Blender for diffuse texture + pixels = [] + for y in range(final_height): + for x in range(final_width): + r, g, b, a = img.getpixel((x, final_height - 1 - y)) + pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) + + blender_img.pixels = pixels + blender_img.pack() + + # Create normal map texture if enabled and generated + normal_map_blender_img = None + if normal_map_img and props.generate_normal_map: + normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}" + + # Remove existing normal map texture + if normal_map_name in bpy.data.images: + bpy.data.images.remove(bpy.data.images[normal_map_name]) + + # Create new normal map image + normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False) + normal_map_blender_img.colorspace_settings.name = 'Non-Color' # Important for normal maps + + # Convert PIL to Blender for normal map + normal_pixels = [] + for y in range(final_height): + for x in range(final_width): + r, g, b = normal_map_img.getpixel((x, final_height - 1 - y)) + normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0]) # Alpha always 1.0 for normal maps + + normal_map_blender_img.pixels = normal_pixels + normal_map_blender_img.pack() + print(f"[Normal Map] Created normal map texture for shader: {normal_map_name}") + + # Create or get material + material_name = f"TextTexture_Mat_{props.text[:15].replace(' ', '_')}" + + if material_name in bpy.data.materials: + mat = bpy.data.materials[material_name] + # Clear existing nodes + mat.node_tree.nodes.clear() + else: + mat = bpy.data.materials.new(name=material_name) + mat.use_nodes = True + mat.node_tree.nodes.clear() + + # Set up shader nodes + nodes = mat.node_tree.nodes + links = mat.node_tree.links + + # Create nodes + output_node = nodes.new(type='ShaderNodeOutputMaterial') + output_node.location = (400, 0) + + # Shader type based on settings + if props.shader_type == 'PRINCIPLED': + shader_node = nodes.new(type='ShaderNodeBsdfPrincipled') + shader_node.location = (200, 0) + + # Create diffuse texture node + tex_node = nodes.new(type='ShaderNodeTexImage') + tex_node.image = blender_img + tex_node.location = (-200, 0) + + # Connect texture to shader + if props.shader_connection == 'BASE_COLOR': + links.new(tex_node.outputs['Color'], shader_node.inputs['Base Color']) + elif props.shader_connection == 'EMISSION': + links.new(tex_node.outputs['Color'], shader_node.inputs['Emission Color']) + shader_node.inputs['Emission Strength'].default_value = props.emission_strength + elif props.shader_connection == 'ALPHA': + links.new(tex_node.outputs['Alpha'], shader_node.inputs['Alpha']) + # Also connect color for visibility + links.new(tex_node.outputs['Color'], shader_node.inputs['Base Color']) + mat.blend_method = 'BLEND' + + # Connect alpha if enabled + if props.use_alpha: + links.new(tex_node.outputs['Alpha'], shader_node.inputs['Alpha']) + mat.blend_method = 'BLEND' + + # Add normal map nodes if normal map was generated + if normal_map_blender_img: + # Create normal map texture node + normal_tex_node = nodes.new(type='ShaderNodeTexImage') + normal_tex_node.image = normal_map_blender_img + normal_tex_node.location = (-200, -300) + normal_tex_node.label = "Normal Map" + + # Create normal map node + normal_map_node = nodes.new(type='ShaderNodeNormalMap') + normal_map_node.location = (0, -300) + + # Connect normal map texture to normal map node + links.new(normal_tex_node.outputs['Color'], normal_map_node.inputs['Color']) + + # Connect normal map node to shader + links.new(normal_map_node.outputs['Normal'], shader_node.inputs['Normal']) + + print(f"[Normal Map] Added normal map nodes to Principled BSDF shader") + + # Handle shadow/reflection disabling + current_shader = shader_node + final_x_location = 400 + + # Disable shadows if enabled + if props.disable_shadows: + # Create light path node + light_path_node = nodes.new(type='ShaderNodeLightPath') + light_path_node.location = (50, 150) + + # Create transparent BSDF + transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') + transparent_node.location = (200, 150) + + # Create mix shader for shadows + shadow_mix_node = nodes.new(type='ShaderNodeMixShader') + shadow_mix_node.location = (350, 50) + + # Connect light path "Is Shadow Ray" to mix factor + links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac']) + + # Mix principled BSDF with transparent BSDF + links.new(current_shader.outputs['BSDF'], shadow_mix_node.inputs[1]) # Shader 1 (not shadow) + links.new(transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2]) # Shader 2 (shadow) + + current_shader = shadow_mix_node + final_x_location = 500 + + # Disable reflections if enabled + if props.disable_reflections: + # Create or reuse light path node + if not props.disable_shadows: + light_path_node = nodes.new(type='ShaderNodeLightPath') + light_path_node.location = (50, 200) + + # Create transparent BSDF for reflections + reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') + reflection_transparent_node.location = (200, 200) + + # Create mix shader for reflections + reflection_mix_node = nodes.new(type='ShaderNodeMixShader') + reflection_mix_node.location = (final_x_location - 50, 100) + + # Connect light path "Is Reflection Ray" to mix factor + links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac']) + + # Mix current shader with transparent BSDF + links.new(current_shader.outputs['Shader' if props.disable_shadows else 'BSDF'], reflection_mix_node.inputs[1]) # Shader 1 (not reflection) + links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2]) # Shader 2 (reflection) + + current_shader = reflection_mix_node + final_x_location = final_x_location + 50 + + # Disable backfacing if enabled + if props.disable_backfacing: + # Create or reuse light path node + if not props.disable_shadows and not props.disable_reflections: + light_path_node = nodes.new(type='ShaderNodeLightPath') + light_path_node.location = (50, 250) + + # Create geometry node for backfacing detection + geometry_node = nodes.new(type='ShaderNodeNewGeometry') + geometry_node.location = (50, 300) + + # Create transparent BSDF for backfaces + backface_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') + backface_transparent_node.location = (200, 250) + + # Create mix shader for backfaces + backface_mix_node = nodes.new(type='ShaderNodeMixShader') + backface_mix_node.location = (final_x_location - 50, 150) + + # Connect geometry "Backfacing" to mix factor + links.new(geometry_node.outputs['Backfacing'], backface_mix_node.inputs['Fac']) + + # Mix current shader with transparent BSDF + current_output = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'BSDF' + links.new(current_shader.outputs[current_output], backface_mix_node.inputs[1]) # Shader 1 (front face) + links.new(backface_transparent_node.outputs['BSDF'], backface_mix_node.inputs[2]) # Shader 2 (backface) + + current_shader = backface_mix_node + final_x_location = final_x_location + 50 + + # Update output node position + output_node.location = (final_x_location, 0) + + # Connect final shader to output + output_socket = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'BSDF' + links.new(current_shader.outputs[output_socket], output_node.inputs['Surface']) + + elif props.shader_type == 'EMISSION': + shader_node = nodes.new(type='ShaderNodeEmission') + shader_node.location = (200, 0) + + # Create texture node + tex_node = nodes.new(type='ShaderNodeTexImage') + tex_node.image = blender_img + tex_node.location = (-200, 0) + + # Connect texture to emission + links.new(tex_node.outputs['Color'], shader_node.inputs['Color']) + shader_node.inputs['Strength'].default_value = props.emission_strength + + # Handle shadow/reflection disabling + current_shader = shader_node + final_x_location = 400 + + # Disable shadows if enabled + if props.disable_shadows: + # Create light path node + light_path_node = nodes.new(type='ShaderNodeLightPath') + light_path_node.location = (50, 150) + + # Create transparent BSDF + transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') + transparent_node.location = (200, 150) + + # Create mix shader for shadows + shadow_mix_node = nodes.new(type='ShaderNodeMixShader') + shadow_mix_node.location = (350, 50) + + # Connect light path "Is Shadow Ray" to mix factor + links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac']) + + # Mix emission with transparent BSDF + links.new(current_shader.outputs['Emission'], shadow_mix_node.inputs[1]) # Shader 1 (not shadow) + links.new(transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2]) # Shader 2 (shadow) + + current_shader = shadow_mix_node + final_x_location = 500 + + # Disable reflections if enabled + if props.disable_reflections: + # Create or reuse light path node + if not props.disable_shadows: + light_path_node = nodes.new(type='ShaderNodeLightPath') + light_path_node.location = (50, 200) + + # Create transparent BSDF for reflections + reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') + reflection_transparent_node.location = (200, 200) + + # Create mix shader for reflections + reflection_mix_node = nodes.new(type='ShaderNodeMixShader') + reflection_mix_node.location = (final_x_location - 50, 100) + + # Connect light path "Is Reflection Ray" to mix factor + links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac']) + + # Mix current shader with transparent BSDF + links.new(current_shader.outputs['Shader' if props.disable_shadows else 'Emission'], reflection_mix_node.inputs[1]) # Shader 1 (not reflection) + links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2]) # Shader 2 (reflection) + + current_shader = reflection_mix_node + final_x_location = final_x_location + 50 + + # Update output node position + output_node.location = (final_x_location, 0) + + # Connect final shader to output + output_socket = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'Emission' + links.new(current_shader.outputs[output_socket], output_node.inputs['Surface']) + + elif props.shader_type == 'TRANSPARENT': + shader_node = nodes.new(type='ShaderNodeBsdfTransparent') + mix_node = nodes.new(type='ShaderNodeMixShader') + principled_node = nodes.new(type='ShaderNodeBsdfPrincipled') + + shader_node.location = (200, 100) + mix_node.location = (350, 0) + principled_node.location = (200, -100) + + # Create texture node + tex_node = nodes.new(type='ShaderNodeTexImage') + tex_node.image = blender_img + tex_node.location = (-200, 0) + + # Connect texture + links.new(tex_node.outputs['Color'], principled_node.inputs['Base Color']) + links.new(tex_node.outputs['Alpha'], mix_node.inputs['Fac']) + + # Connect shaders to mix + links.new(shader_node.outputs['BSDF'], mix_node.inputs[1]) + links.new(principled_node.outputs['BSDF'], mix_node.inputs[2]) + + # Handle shadow/reflection disabling + current_shader = mix_node + final_x_location = 500 + + # Disable shadows if enabled + if props.disable_shadows: + # Create light path node + light_path_node = nodes.new(type='ShaderNodeLightPath') + light_path_node.location = (50, 200) + + # Create transparent BSDF for shadows (reuse existing transparent node) + shadow_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') + shadow_transparent_node.location = (350, 150) + + # Create mix shader for shadows + shadow_mix_node = nodes.new(type='ShaderNodeMixShader') + shadow_mix_node.location = (500, 50) + + # Connect light path "Is Shadow Ray" to mix factor + links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac']) + + # Mix current shader with transparent BSDF + links.new(current_shader.outputs['Shader'], shadow_mix_node.inputs[1]) # Shader 1 (not shadow) + links.new(shadow_transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2]) # Shader 2 (shadow) + + current_shader = shadow_mix_node + final_x_location = 650 + + # Disable reflections if enabled + if props.disable_reflections: + # Create or reuse light path node + if not props.disable_shadows: + light_path_node = nodes.new(type='ShaderNodeLightPath') + light_path_node.location = (50, 250) + + # Create transparent BSDF for reflections + reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') + reflection_transparent_node.location = (350, 200) + + # Create mix shader for reflections + reflection_mix_node = nodes.new(type='ShaderNodeMixShader') + reflection_mix_node.location = (final_x_location - 50, 100) + + # Connect light path "Is Reflection Ray" to mix factor + links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac']) + + # Mix current shader with transparent BSDF + links.new(current_shader.outputs['Shader'], reflection_mix_node.inputs[1]) # Shader 1 (not reflection) + links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2]) # Shader 2 (reflection) + + current_shader = reflection_mix_node + final_x_location = final_x_location + 50 + + # Disable backfacing if enabled + if props.disable_backfacing: + # Create or reuse light path node + if not props.disable_shadows and not props.disable_reflections: + light_path_node = nodes.new(type='ShaderNodeLightPath') + light_path_node.location = (50, 300) + + # Create geometry node for backfacing detection + geometry_node = nodes.new(type='ShaderNodeNewGeometry') + geometry_node.location = (50, 350) + + # Create transparent BSDF for backfaces + backface_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') + backface_transparent_node.location = (350, 300) + + # Create mix shader for backfaces + backface_mix_node = nodes.new(type='ShaderNodeMixShader') + backface_mix_node.location = (final_x_location + 50, 150) + + # Connect geometry "Backfacing" to mix factor + links.new(geometry_node.outputs['Backfacing'], backface_mix_node.inputs['Fac']) + + # Mix current shader with transparent BSDF + links.new(current_shader.outputs['Shader'], backface_mix_node.inputs[1]) # Shader 1 (front face) + links.new(backface_transparent_node.outputs['BSDF'], backface_mix_node.inputs[2]) # Shader 2 (backface) + + current_shader = backface_mix_node + final_x_location = final_x_location + 150 + + # Update output node position + output_node.location = (final_x_location, 0) + + # Connect final shader to output + links.new(current_shader.outputs['Shader'], output_node.inputs['Surface']) + + mat.blend_method = 'BLEND' + + # Assign material to active object if enabled and possible + if props.auto_assign_material and context.active_object and context.active_object.type in ['MESH', 'CURVE']: + obj = context.active_object + # Always create a new material slot + obj.data.materials.append(mat) + + self.report({'INFO'}, f"Generated shader '{material_name}' and assigned to {obj.name}") + else: + self.report({'INFO'}, f"Generated shader '{material_name}' (assign manually to object)") + + # Switch to Shader Editor if available and set material + for area in context.screen.areas: + if area.type == 'NODE_EDITOR': + for space in area.spaces: + if space.type == 'NODE_EDITOR': + space.shader_type = 'OBJECT' + space.id = mat + area.tag_redraw() + break + + return {'FINISHED'} + + except Exception as e: + self.report({'ERROR'}, f"Shader generation failed: {str(e)}") + import traceback + traceback.print_exc() + return {'CANCELLED'} + class TEXT_TEXTURE_OT_refresh_preview(Operator): """Force refresh the preview""" bl_idname = "text_texture.refresh_preview" @@ -1599,15 +2562,25 @@ class TEXT_TEXTURE_OT_save_preset(Operator): default=False ) + preset_name: bpy.props.StringProperty( + name="Preset Name", + description="Name of preset to save/overwrite", + default="" + ) + def execute(self, context): props = context.scene.text_texture_props - # Validate preset name - preset_name = props.new_preset_name.strip() + # Use provided preset name or fallback to input field + preset_name = self.preset_name.strip() if self.preset_name else props.new_preset_name.strip() if not preset_name: self.report({'ERROR'}, "Please enter a preset name") return {'CANCELLED'} + # When overwriting from button, update the input field for consistency + if self.preset_name and self.preset_name.strip(): + props.new_preset_name = preset_name + # Validate preset name characters (avoid filesystem issues) import re # Allow Unicode letters, numbers, spaces, hyphens, underscores, and common accented characters @@ -1636,6 +2609,18 @@ class TEXT_TEXTURE_OT_save_preset(Operator): self.report({'ERROR'}, f"Preset '{preset_name}' already exists. Suggestion: try '{suggested_name}' or delete the existing preset first.") return {'CANCELLED'} + # DETAILED LOGGING: Current shader properties before saving + print(f"[PRESET SAVE DEBUG] ==================== SAVING PRESET: '{preset_name}' ====================") + print(f"[PRESET SAVE DEBUG] Current shader properties from props:") + print(f"[PRESET SAVE DEBUG] disable_shadows: {props.disable_shadows}") + print(f"[PRESET SAVE DEBUG] disable_reflections: {props.disable_reflections}") + print(f"[PRESET SAVE DEBUG] disable_backfacing: {props.disable_backfacing}") + print(f"[PRESET SAVE DEBUG] shader_type: {props.shader_type}") + print(f"[PRESET SAVE DEBUG] shader_connection: {props.shader_connection}") + print(f"[PRESET SAVE DEBUG] use_alpha: {props.use_alpha}") + print(f"[PRESET SAVE DEBUG] emission_strength: {props.emission_strength}") + print(f"[PRESET SAVE DEBUG] auto_assign_material: {props.auto_assign_material}") + preset_data = { 'text': props.text, 'texture_width': props.texture_width, @@ -1654,9 +2639,27 @@ class TEXT_TEXTURE_OT_save_preset(Operator): 'prepend_margin': props.prepend_margin, 'append_margin': props.append_margin, 'prepend_append_layout': props.prepend_append_layout, + 'shader_type': props.shader_type, + 'shader_connection': props.shader_connection, + 'use_alpha': props.use_alpha, + 'emission_strength': props.emission_strength, + 'auto_assign_material': props.auto_assign_material, + 'disable_shadows': props.disable_shadows, + 'disable_reflections': props.disable_reflections, + 'disable_backfacing': props.disable_backfacing, + 'generate_normal_map': props.generate_normal_map, + 'normal_map_strength': props.normal_map_strength, + 'normal_map_blur_radius': props.normal_map_blur_radius, + 'normal_map_invert': props.normal_map_invert, 'image_overlays': [] } + # DETAILED LOGGING: Verify shader properties in preset_data + print(f"[PRESET SAVE DEBUG] Shader properties in preset_data dictionary:") + print(f"[PRESET SAVE DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") + print(f"[PRESET SAVE DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") + print(f"[PRESET SAVE DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") + # Save overlay data for overlay in props.image_overlays: overlay_data = { @@ -1672,8 +2675,35 @@ class TEXT_TEXTURE_OT_save_preset(Operator): preset_data['image_overlays'].append(overlay_data) try: - with open(preset_file, 'w') as f: - json.dump(preset_data, f, indent=2) + # Primary: Save to blend file (new system) + print(f"[PRESET SAVE DEBUG] Attempting to save to blend file...") + blend_success = save_blend_file_preset(preset_name, preset_data) + print(f"[PRESET SAVE DEBUG] Blend file save result: {blend_success}") + + # ALWAYS save to local file for cross-blend-file sharing + print(f"[PRESET SAVE DEBUG] Attempting to save to local file: {preset_file}") + local_success = True + try: + with open(preset_file, 'w') as f: + json.dump(preset_data, f, indent=2) + print(f"[PRESET SAVE DEBUG] Successfully saved preset '{preset_name}' to local storage for cross-file sharing") + + # VERIFY: Read back the local file to confirm shader properties were saved + print(f"[PRESET SAVE DEBUG] Verifying local file contents...") + with open(preset_file, 'r') as f: + saved_data = json.load(f) + print(f"[PRESET SAVE DEBUG] Local file shader properties verification:") + print(f"[PRESET SAVE DEBUG] disable_shadows: {saved_data.get('disable_shadows', 'NOT FOUND')}") + print(f"[PRESET SAVE DEBUG] disable_reflections: {saved_data.get('disable_reflections', 'NOT FOUND')}") + print(f"[PRESET SAVE DEBUG] disable_backfacing: {saved_data.get('disable_backfacing', 'NOT FOUND')}") + + except Exception as local_error: + print(f"[PRESET SAVE DEBUG] Warning: Failed to save local preset file: {local_error}") + local_success = False + + if not blend_success and not local_success: + self.report({'ERROR'}, f"Failed to save preset to both blend file and local storage") + return {'CANCELLED'} # Check if preset already exists in the collection and update it existing_preset = None @@ -1685,15 +2715,27 @@ class TEXT_TEXTURE_OT_save_preset(Operator): if not existing_preset: preset = props.presets.add() preset.name = preset_name + preset.source = 'BLEND_FILE' if blend_success else 'LOCAL_FILE' + else: + # Update source based on where it was successfully saved + existing_preset.source = 'BLEND_FILE' if blend_success else 'LOCAL_FILE' - # Clear the input field - props.new_preset_name = "New Preset" + # Don't clear the input field - keep the name for easy re-saving + # props.new_preset_name = "New Preset" # Provide clear feedback + storage_info = "" + if blend_success and local_success: + storage_info = " (saved to blend file + local backup)" + elif blend_success: + storage_info = " (saved to blend file)" + elif local_success: + storage_info = " (saved locally only)" + if self.overwrite: - self.report({'INFO'}, f"✅ Preset '{preset_name}' updated successfully") + self.report({'INFO'}, f"✅ Preset '{preset_name}' updated successfully{storage_info}") else: - self.report({'INFO'}, f"✅ Preset '{preset_name}' saved successfully") + self.report({'INFO'}, f"✅ Preset '{preset_name}' saved successfully{storage_info}") except (OSError, IOError, json.JSONEncodeError) as e: self.report({'ERROR'}, f"Failed to save preset: {str(e)}") @@ -1745,18 +2787,57 @@ class TEXT_TEXTURE_OT_load_preset(Operator): def execute(self, context): props = context.scene.text_texture_props + # DETAILED LOGGING: Current shader properties before loading + print(f"[PRESET LOAD DEBUG] ==================== LOADING PRESET: '{self.preset_name}' ====================") + print(f"[PRESET LOAD DEBUG] Current shader properties BEFORE loading:") + print(f"[PRESET LOAD DEBUG] disable_shadows: {props.disable_shadows}") + print(f"[PRESET LOAD DEBUG] disable_reflections: {props.disable_reflections}") + print(f"[PRESET LOAD DEBUG] disable_backfacing: {props.disable_backfacing}") + print(f"[PRESET LOAD DEBUG] shader_type: {props.shader_type}") + print(f"[PRESET LOAD DEBUG] shader_connection: {props.shader_connection}") + try: - preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") - if not os.path.exists(preset_file): - self.report({'ERROR'}, f"Preset file not found: {self.preset_name}") + # First try to load from blend file + blend_presets = get_blend_file_presets() + preset_data = None + source = 'BLEND_FILE' + + print(f"[PRESET LOAD DEBUG] Available blend file presets: {list(blend_presets.keys())}") + + if self.preset_name in blend_presets: + preset_data = blend_presets[self.preset_name] + print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from blend file") + print(f"[PRESET LOAD DEBUG] Blend file preset shader properties:") + print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") + print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") + print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") + else: + # Fallback to local file + preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") + print(f"[PRESET LOAD DEBUG] Preset not in blend file, trying local file: {preset_file}") + if os.path.exists(preset_file): + with open(preset_file, 'r') as f: + preset_data = json.load(f) + source = 'LOCAL_FILE' + print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from local file") + print(f"[PRESET LOAD DEBUG] Local file preset shader properties:") + print(f"[PRESET LOAD DEBUG] disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}") + print(f"[PRESET LOAD DEBUG] disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}") + print(f"[PRESET LOAD DEBUG] disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}") + else: + print(f"[PRESET LOAD DEBUG] Local file does not exist: {preset_file}") + self.report({'ERROR'}, f"Preset not found in blend file or local storage: {self.preset_name}") + return {'CANCELLED'} + + if not preset_data: + print(f"[PRESET LOAD DEBUG] No preset data found!") + self.report({'ERROR'}, f"No data found for preset: {self.preset_name}") return {'CANCELLED'} - - with open(preset_file, 'r') as f: - preset_data = json.load(f) props.is_updating = True - props.text = preset_data.get('text', props.text) + # Skip loading text to prevent overwriting current text + # props.text = preset_data.get('text', props.text) props.texture_width = preset_data.get('texture_width', props.texture_width) props.texture_height = preset_data.get('texture_height', props.texture_height) props.font_size = preset_data.get('font_size', props.font_size) @@ -1774,6 +2855,63 @@ class TEXT_TEXTURE_OT_load_preset(Operator): props.append_margin = preset_data.get('append_margin', props.append_margin) props.prepend_append_layout = preset_data.get('prepend_append_layout', props.prepend_append_layout) + # DETAILED LOGGING: Shader property loading with before/after values + print(f"[PRESET LOAD DEBUG] Loading shader properties...") + + old_shader_type = props.shader_type + props.shader_type = preset_data.get('shader_type', props.shader_type) + print(f"[PRESET LOAD DEBUG] shader_type: {old_shader_type} -> {props.shader_type}") + + old_shader_connection = props.shader_connection + props.shader_connection = preset_data.get('shader_connection', props.shader_connection) + print(f"[PRESET LOAD DEBUG] shader_connection: {old_shader_connection} -> {props.shader_connection}") + + old_use_alpha = props.use_alpha + props.use_alpha = preset_data.get('use_alpha', props.use_alpha) + print(f"[PRESET LOAD DEBUG] use_alpha: {old_use_alpha} -> {props.use_alpha}") + + old_emission_strength = props.emission_strength + props.emission_strength = preset_data.get('emission_strength', props.emission_strength) + print(f"[PRESET LOAD DEBUG] emission_strength: {old_emission_strength} -> {props.emission_strength}") + + old_auto_assign = props.auto_assign_material + props.auto_assign_material = preset_data.get('auto_assign_material', props.auto_assign_material) + print(f"[PRESET LOAD DEBUG] auto_assign_material: {old_auto_assign} -> {props.auto_assign_material}") + + # CRITICAL SHADER PROPERTIES - Most detailed logging + print(f"[PRESET LOAD DEBUG] ==================== CRITICAL SHADER PROPERTIES ====================") + + old_disable_shadows = props.disable_shadows + new_disable_shadows = preset_data.get('disable_shadows', props.disable_shadows) + props.disable_shadows = new_disable_shadows + print(f"[PRESET LOAD DEBUG] disable_shadows:") + print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_shadows} (type: {type(old_disable_shadows)})") + print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_shadows', 'NOT FOUND')} (type: {type(preset_data.get('disable_shadows', 'N/A'))})") + print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_shadows} (type: {type(props.disable_shadows)})") + + old_disable_reflections = props.disable_reflections + new_disable_reflections = preset_data.get('disable_reflections', props.disable_reflections) + props.disable_reflections = new_disable_reflections + print(f"[PRESET LOAD DEBUG] disable_reflections:") + print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_reflections} (type: {type(old_disable_reflections)})") + print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_reflections', 'NOT FOUND')} (type: {type(preset_data.get('disable_reflections', 'N/A'))})") + print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_reflections} (type: {type(props.disable_reflections)})") + + old_disable_backfacing = props.disable_backfacing + new_disable_backfacing = preset_data.get('disable_backfacing', props.disable_backfacing) + props.disable_backfacing = new_disable_backfacing + print(f"[PRESET LOAD DEBUG] disable_backfacing:") + print(f"[PRESET LOAD DEBUG] OLD VALUE: {old_disable_backfacing} (type: {type(old_disable_backfacing)})") + print(f"[PRESET LOAD DEBUG] PRESET DATA: {preset_data.get('disable_backfacing', 'NOT FOUND')} (type: {type(preset_data.get('disable_backfacing', 'N/A'))})") + print(f"[PRESET LOAD DEBUG] NEW VALUE: {props.disable_backfacing} (type: {type(props.disable_backfacing)})") + + print(f"[PRESET LOAD DEBUG] ================================================================") + + props.generate_normal_map = preset_data.get('generate_normal_map', props.generate_normal_map) + props.normal_map_strength = preset_data.get('normal_map_strength', props.normal_map_strength) + props.normal_map_blur_radius = preset_data.get('normal_map_blur_radius', props.normal_map_blur_radius) + props.normal_map_invert = preset_data.get('normal_map_invert', props.normal_map_invert) + # Load overlay data props.image_overlays.clear() overlay_data_list = preset_data.get('image_overlays', []) @@ -1789,10 +2927,20 @@ class TEXT_TEXTURE_OT_load_preset(Operator): overlay.enabled = overlay_data.get('enabled', True) props.is_updating = False + + # FINAL VERIFICATION: Check what the properties actually are after loading + print(f"[PRESET LOAD DEBUG] ==================== FINAL VERIFICATION ====================") + print(f"[PRESET LOAD DEBUG] Properties AFTER loading preset '{self.preset_name}':") + print(f"[PRESET LOAD DEBUG] disable_shadows: {props.disable_shadows} (type: {type(props.disable_shadows)})") + print(f"[PRESET LOAD DEBUG] disable_reflections: {props.disable_reflections} (type: {type(props.disable_reflections)})") + print(f"[PRESET LOAD DEBUG] disable_backfacing: {props.disable_backfacing} (type: {type(props.disable_backfacing)})") + print(f"[PRESET LOAD DEBUG] ================================================================") + # Always generate preview after loading preset generate_preview(context) - self.report({'INFO'}, f"Loaded: {self.preset_name}") + source_info = "from blend file" if source == 'BLEND_FILE' else "from local storage" + self.report({'INFO'}, f"✅ Loaded preset '{self.preset_name}' {source_info}") except (OSError, IOError) as e: props.is_updating = False @@ -1822,18 +2970,32 @@ class TEXT_TEXTURE_OT_delete_preset(Operator): props = context.scene.text_texture_props try: + # Delete from both blend file and local storage + blend_deleted = delete_blend_file_preset(self.preset_name) + preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") + local_deleted = False if os.path.exists(preset_file): os.remove(preset_file) - else: - self.report({'WARNING'}, f"Preset file not found: {self.preset_name}") + local_deleted = True + if not blend_deleted and not local_deleted: + self.report({'WARNING'}, f"Preset '{self.preset_name}' not found in blend file or local storage") + + # Remove from UI list for i, preset in enumerate(props.presets): if preset.name == self.preset_name: props.presets.remove(i) break - self.report({'INFO'}, f"🗑️ Deleted preset: {self.preset_name}") + delete_info = [] + if blend_deleted: + delete_info.append("blend file") + if local_deleted: + delete_info.append("local storage") + + location_text = " and ".join(delete_info) if delete_info else "nowhere (not found)" + self.report({'INFO'}, f"🗑️ Deleted preset '{self.preset_name}' from {location_text}") except (OSError, IOError) as e: self.report({'ERROR'}, f"Failed to delete preset file: {str(e)}") @@ -1896,6 +3058,36 @@ class TEXT_TEXTURE_OT_refresh_fonts(Operator): self.report({'INFO'}, f"Found {len(get_font_enum_items.cached_fonts)} fonts") return {'FINISHED'} +class TEXT_TEXTURE_OT_refresh_presets(Operator): + """Refresh presets from blend file and local storage""" + bl_idname = "text_texture.refresh_presets" + bl_label = "Refresh Presets" + bl_description = "Reload presets from blend file and local storage" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + try: + # Reload presets + initialize_presets() + + props = context.scene.text_texture_props + blend_presets = get_blend_file_presets() + total_presets = len(props.presets) + blend_count = len(blend_presets) + local_count = total_presets - blend_count + + self.report({'INFO'}, f"✅ Refreshed {total_presets} presets ({blend_count} from blend file, {local_count} local)") + + # Force UI redraw + for area in context.screen.areas: + area.tag_redraw() + + return {'FINISHED'} + + except Exception as e: + self.report({'ERROR'}, f"Failed to refresh presets: {str(e)}") + return {'CANCELLED'} + class TEXT_TEXTURE_OT_add_overlay(Operator): """Add new image overlay""" bl_idname = "text_texture.add_overlay" @@ -2163,12 +3355,11 @@ def draw_presets_section(layout, props): save_row.prop(props, "new_preset_name", text="", placeholder="Preset Name") save_row.operator("text_texture.save_preset", text="Save", icon='FILE_NEW') - # Status info - if props.new_preset_name and props.new_preset_name.strip() != "New Preset": - preset_file = os.path.join(get_preset_path(), f"{props.new_preset_name.strip()}.json") - if os.path.exists(preset_file): - status_row = layout.row() - status_row.label(text="⚠️ Preset exists - will prompt to overwrite", icon='INFO') + layout.separator() + + # Add refresh button at the top of presets section + refresh_row = layout.row(align=True) + refresh_row.operator("text_texture.refresh_presets", text="Refresh Presets", icon='FILE_REFRESH') layout.separator() @@ -2176,16 +3367,146 @@ def draw_presets_section(layout, props): if props.presets: layout.label(text=f"📁 Saved Presets ({len(props.presets)}):", icon='PRESET') - for i, preset in enumerate(props.presets): - row = layout.row() - load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}") - load_btn.preset_name = preset.name - - delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') - delete_btn.preset_name = preset.name + # Separate presets by source + blend_presets = [p for p in props.presets if p.source == 'BLEND_FILE'] + local_presets = [p for p in props.presets if p.source == 'LOCAL_FILE'] + + # Show blend file presets first + if blend_presets: + layout.label(text="🎯 Blend File Presets (travel with file):", icon='FILE_BLEND') + for preset in blend_presets: + row = layout.row() + load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}") + load_btn.preset_name = preset.name + + # Quick overwrite button + overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH') + overwrite_btn.overwrite = True + # Set the preset name so it can be overwritten + overwrite_btn.preset_name = preset.name + + delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') + delete_btn.preset_name = preset.name + + # Show local presets second + if local_presets: + if blend_presets: + layout.separator() + layout.label(text="💻 Local Presets (this machine only):", icon='DISK_DRIVE') + for preset in local_presets: + row = layout.row() + load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}") + load_btn.preset_name = preset.name + + # Quick overwrite button + overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH') + overwrite_btn.overwrite = True + # Set the preset name so it can be overwritten + overwrite_btn.preset_name = preset.name + + delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH') + delete_btn.preset_name = preset.name else: layout.label(text="📂 No presets saved yet", icon='INFO') layout.label(text="💡 Save your first preset above!", icon='LIGHT_DATA') + layout.separator() + info_box = layout.box() + info_box.label(text="ℹ️ Presets are now saved in the blend file", icon='INFO') + info_box.label(text="They will travel with your .blend file!") + +def draw_shader_section(layout, props): + """Draw shader generation controls""" + # Create Shader Button (as requested) + row = layout.row() + row.scale_y = 1.5 + row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL') + + layout.separator() + + # Clean minimalist controls + col = layout.column(align=True) + col.prop(props, "shader_type", text="Type") + + # Show relevant options based on shader type + if props.shader_type == 'PRINCIPLED': + col.prop(props, "shader_connection", text="Connect") + if props.shader_connection == 'EMISSION': + col.prop(props, "emission_strength", text="Strength") + elif props.shader_type == 'EMISSION': + col.prop(props, "emission_strength", text="Strength") + + layout.separator() + + # Simple toggles + col = layout.column(align=True) + col.prop(props, "use_alpha", text="Use Alpha Channel") + col.prop(props, "auto_assign_material", text="Auto-Assign to Object") + + # Clean status info + if props.auto_assign_material: + layout.separator() + info_row = layout.row() + info_row.scale_y = 0.8 + try: + context = bpy.context + if context and context.active_object and context.active_object.type in ['MESH', 'CURVE']: + info_row.label(text=f"→ {context.active_object.name}", icon='CHECKMARK') + else: + info_row.label(text="Select mesh/curve object", icon='INFO') + except: + info_row.label(text="Select mesh/curve object", icon='INFO') + +def draw_shader_options_only(layout, props): + """Draw only shader options without the Create button - for collapsible section""" + # Clean grid layout for shader options + grid = layout.grid_flow(row_major=True, columns=2, align=True) + + grid.prop(props, "shader_type", text="Type") + + if props.shader_type == 'PRINCIPLED': + grid.prop(props, "shader_connection", text="Connect") + if props.shader_connection == 'EMISSION': + grid.prop(props, "emission_strength", text="Strength") + elif props.shader_type == 'EMISSION': + grid.prop(props, "emission_strength", text="Strength") + + # Simple row for toggles + layout.separator() + row = layout.row(align=True) + row.prop(props, "use_alpha", toggle=True) + row.prop(props, "auto_assign_material", toggle=True) + + # Shadow, reflection, and backfacing toggles + row = layout.row(align=True) + row.prop(props, "disable_shadows", toggle=True) + row.prop(props, "disable_reflections", toggle=True) + row.prop(props, "disable_backfacing", toggle=True) + +def draw_normal_maps_section(layout, props): + """Draw normal map generation controls""" + # Main toggle for normal map generation + layout.prop(props, "generate_normal_map", text="Generate Normal Map", icon='TEXTURE') + + # Show controls only if normal map generation is enabled + if props.generate_normal_map: + layout.separator() + + # Normal map settings in a clean layout + col = layout.column(align=True) + col.prop(props, "normal_map_strength", text="Strength", slider=True) + col.prop(props, "normal_map_blur_radius", text="Blur Radius", slider=True) + + layout.separator() + layout.prop(props, "normal_map_invert", text="Invert (Emboss ↔ Deboss)") + + # Info box with usage tips + layout.separator() + info_box = layout.box() + info_box.scale_y = 0.8 + info_box.label(text="💡 Normal maps add depth and detail", icon='INFO') + info_box.label(text="• Higher strength = more pronounced effect") + info_box.label(text="• Blur smooths sharp edges") + info_box.label(text="• Invert changes raised/recessed effect") # ============================================================================ # MENUS @@ -2256,17 +3577,30 @@ class TEXT_TEXTURE_PT_panel(Panel): # Generate Button - layout.separator() + layout.separator(factor=0.5) draw_generate_button(layout) - # Preview Button at top level - layout.separator() - layout.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + # CREATE SHADER BUTTON AT TOP LEVEL - AS REQUESTED + layout.separator(factor=0.5) + create_shader_row = layout.row() + create_shader_row.scale_y = 1.5 + create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL') - layout.separator() + # Preview Button at top level + layout.separator(factor=0.5) + preview_row = layout.row() + preview_row.scale_y = 1.5 + preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + + layout.separator(factor=1.2) # COLLAPSIBLE SECTIONS + # Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON + shader_section = draw_collapsible_header(layout, "expand_shader", "Shader Options", 'SETTINGS') + if shader_section: + draw_shader_options_only(shader_section, props) + # Image Overlays Section (collapsed by default) overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') if overlay_section: @@ -2282,6 +3616,11 @@ class TEXT_TEXTURE_PT_panel(Panel): if font_section: draw_font_settings_section(font_section, props) + # Normal Maps Section (collapsed by default) + normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE') + if normal_maps_section: + draw_normal_maps_section(normal_maps_section, props) + # Colors Section (collapsed by default) colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR') if colors_section: @@ -2314,17 +3653,30 @@ class TEXT_TEXTURE_PT_panel_3d(Panel): # Generate Button - layout.separator() + layout.separator(factor=0.5) draw_generate_button(layout) - # Preview Button at top level - layout.separator() - layout.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + # CREATE SHADER BUTTON AT TOP LEVEL - AS REQUESTED + layout.separator(factor=0.5) + create_shader_row = layout.row() + create_shader_row.scale_y = 1.5 + create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL') - layout.separator() + # Preview Button at top level + layout.separator(factor=0.5) + preview_row = layout.row() + preview_row.scale_y = 1.5 + preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + + layout.separator(factor=1.2) # COLLAPSIBLE SECTIONS + # Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON + shader_section = draw_collapsible_header(layout, "expand_shader", "Shader Options", 'SETTINGS') + if shader_section: + draw_shader_options_only(shader_section, props) + # Image Overlays Section (collapsed by default) overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') if overlay_section: @@ -2340,6 +3692,11 @@ class TEXT_TEXTURE_PT_panel_3d(Panel): if font_section: draw_font_settings_section(font_section, props) + # Normal Maps Section (collapsed by default) + normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE') + if normal_maps_section: + draw_normal_maps_section(normal_maps_section, props) + # Colors Section (collapsed by default) colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR') if colors_section: @@ -2372,14 +3729,16 @@ class TEXT_TEXTURE_PT_panel_properties(Panel): # Generate Button - layout.separator() + layout.separator(factor=0.5) draw_generate_button(layout) # Preview Button at top level - layout.separator() - layout.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + layout.separator(factor=0.5) + preview_row = layout.row() + preview_row.scale_y = 1.5 + preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') - layout.separator() + layout.separator(factor=1.2) # COLLAPSIBLE SECTIONS @@ -2398,6 +3757,11 @@ class TEXT_TEXTURE_PT_panel_properties(Panel): if font_section: draw_font_settings_section(font_section, props) + # Normal Maps Section (collapsed by default) + normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE') + if normal_maps_section: + draw_normal_maps_section(normal_maps_section, props) + # Colors Section (collapsed by default) colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR') if colors_section: @@ -2417,6 +3781,7 @@ classes = ( TEXT_TEXTURE_Preset, TEXT_TEXTURE_Properties, TEXT_TEXTURE_OT_generate, + TEXT_TEXTURE_OT_generate_shader, TEXT_TEXTURE_OT_refresh_preview, TEXT_TEXTURE_OT_view_preview_zoom, TEXT_TEXTURE_OT_add_overlay, @@ -2429,6 +3794,7 @@ classes = ( TEXT_TEXTURE_OT_load_preset, TEXT_TEXTURE_OT_delete_preset, TEXT_TEXTURE_OT_refresh_fonts, + TEXT_TEXTURE_OT_refresh_presets, TEXT_TEXTURE_PT_panel, TEXT_TEXTURE_PT_panel_3d, TEXT_TEXTURE_PT_panel_properties, @@ -2477,6 +3843,24 @@ def register(): print("[TTG DEBUG] Registration completed - scheduling preset initialization") bpy.app.timers.register(delayed_preset_init, first_interval=0.1) + + # Add handler to reload presets when blend file changes + @bpy.app.handlers.persistent + def on_file_load(dummy): + """Reload presets when a blend file is loaded""" + try: + print("[TTG] Reloading presets after file load...") + def delayed_reload(): + initialize_presets() + return None # Stop timer + bpy.app.timers.register(delayed_reload, first_interval=0.1) + except Exception as e: + print(f"[TTG] Error reloading presets: {e}") + + # 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) + print("[TTG DEBUG] ========== ADDON REGISTRATION END ==========") def unregister(): @@ -2492,6 +3876,14 @@ def unregister(): if "TextTexturePreview" in bpy.data.images: bpy.data.images.remove(bpy.data.images["TextTexturePreview"]) + # Remove file load handler + try: + for handler in bpy.app.handlers.load_post[:]: + if handler.__name__ == 'on_file_load' and 'TTG' in str(handler): + bpy.app.handlers.load_post.remove(handler) + except Exception as e: + print(f"Error removing file load handler: {e}") + # Remove from menus bpy.types.NODE_MT_add.remove(menu_func_node_editor) bpy.types.VIEW3D_MT_add.remove(menu_func_view3d) diff --git a/test_positioning_system.py b/test_positioning_system.py deleted file mode 100644 index eee2d5d..0000000 --- a/test_positioning_system.py +++ /dev/null @@ -1,558 +0,0 @@ -#!/usr/bin/env python3 -""" -Comprehensive test suite for the Text Texture Generator's enhanced positioning system. -This script validates the positioning calculations and identifies potential issues -without requiring Blender to be installed. -""" - -import sys -import traceback -from typing import Dict, Tuple, List, Any - -# Mock Blender properties for testing -class MockProps: - """Mock properties class to simulate Blender addon properties""" - def __init__(self): - # Enhanced positioning properties - self.anchor_point = 'MIDDLE_CENTER' - self.position_mode = 'PRESET' - self.margin_x = 0.0 - self.margin_y = 0.0 - self.margins_linked = False - self.offset_x = 0 - self.offset_y = 0 - self.manual_position_x = 50.0 - self.manual_position_y = 50.0 - self.manual_position_unit = 'PERCENTAGE' - self.constrain_to_canvas = True - - # Legacy properties for backward compatibility testing - self.text_align = 'center' - self.padding = 20 - -# Copy of the enhanced positioning function from the addon -def calculate_text_position(props, canvas_width, canvas_height, text_width, text_height): - """Calculate text position based on enhanced positioning properties""" - - if props.position_mode == 'MANUAL': - # Manual positioning mode - if props.manual_position_unit == 'PERCENTAGE': - x = int((props.manual_position_x / 100.0) * canvas_width) - y = int((props.manual_position_y / 100.0) * canvas_height) - else: # PIXELS - x = int(props.manual_position_x) - y = int(props.manual_position_y) - else: - # Preset anchor-based positioning - margin_x_px = int((props.margin_x / 100.0) * canvas_width) - margin_y_px = int((props.margin_y / 100.0) * canvas_height) - - # Calculate base position based on anchor point - if 'LEFT' in props.anchor_point: - base_x = margin_x_px - elif 'RIGHT' in props.anchor_point: - base_x = canvas_width - text_width - margin_x_px - else: # CENTER - base_x = (canvas_width - text_width) // 2 - - if 'TOP' in props.anchor_point: - base_y = margin_y_px - elif 'BOTTOM' in props.anchor_point: - base_y = canvas_height - text_height - margin_y_px - else: # MIDDLE - base_y = (canvas_height - text_height) // 2 - - # Apply fine-tuning offsets - x = base_x + props.offset_x - y = base_y + props.offset_y - - # Apply canvas constraints if enabled - if props.constrain_to_canvas: - x = max(0, min(x, canvas_width - text_width)) - y = max(0, min(y, canvas_height - text_height)) - - return x, y - -def sync_margin_values(props, changed_margin): - """Synchronize margin values when linked""" - if props.margins_linked: - if changed_margin == 'x': - props.margin_y = props.margin_x - elif changed_margin == 'y': - props.margin_x = props.margin_y - -def get_anchor_point_matrix(): - """Return 3x3 matrix of anchor point identifiers for UI layout""" - return [ - ['TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT'], - ['MIDDLE_LEFT', 'MIDDLE_CENTER', 'MIDDLE_RIGHT'], - ['BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT'] - ] - -class PositioningTestSuite: - """Comprehensive test suite for positioning system""" - - def __init__(self): - self.test_results = [] - self.issues_found = [] - self.canvas_width = 1024 - self.canvas_height = 1024 - self.text_width = 200 - self.text_height = 50 - - def log_test(self, test_name: str, passed: bool, details: str = "", expected=None, actual=None): - """Log test result""" - result = { - 'test': test_name, - 'passed': passed, - 'details': details, - 'expected': expected, - 'actual': actual - } - self.test_results.append(result) - - if not passed: - self.issues_found.append(f"❌ {test_name}: {details}") - if expected is not None and actual is not None: - self.issues_found.append(f" Expected: {expected}, Got: {actual}") - else: - print(f"✅ {test_name}") - - def test_anchor_point_calculations(self): - """Test all 9 anchor point calculations""" - print("\n=== Testing Anchor Point Calculations ===") - - anchor_matrix = get_anchor_point_matrix() - expected_positions = { - 'TOP_LEFT': (0, 0), - 'TOP_CENTER': (412, 0), # (1024-200)/2 = 412 - 'TOP_RIGHT': (824, 0), # 1024-200 = 824 - 'MIDDLE_LEFT': (0, 487), # (1024-50)/2 = 487 - 'MIDDLE_CENTER': (412, 487), - 'MIDDLE_RIGHT': (824, 487), - 'BOTTOM_LEFT': (0, 974), # 1024-50 = 974 - 'BOTTOM_CENTER': (412, 974), - 'BOTTOM_RIGHT': (824, 974) - } - - for row in anchor_matrix: - for anchor in row: - props = MockProps() - props.anchor_point = anchor - props.position_mode = 'PRESET' - props.margin_x = 0.0 - props.margin_y = 0.0 - props.offset_x = 0 - props.offset_y = 0 - - try: - x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, - self.text_width, self.text_height) - expected = expected_positions[anchor] - - if (x, y) == expected: - self.log_test(f"Anchor {anchor}", True, f"Position: ({x}, {y})") - else: - self.log_test(f"Anchor {anchor}", False, - f"Incorrect position calculation", expected, (x, y)) - - except Exception as e: - self.log_test(f"Anchor {anchor}", False, f"Exception: {str(e)}") - - def test_margin_calculations(self): - """Test margin calculations with various percentages""" - print("\n=== Testing Margin Calculations ===") - - test_cases = [ - (10.0, 10.0), # 10% margins - (25.0, 25.0), # 25% margins - (50.0, 50.0), # 50% margins (maximum) - (0.0, 0.0), # No margins - ] - - for margin_x, margin_y in test_cases: - props = MockProps() - props.anchor_point = 'TOP_LEFT' - props.position_mode = 'PRESET' - props.margin_x = margin_x - props.margin_y = margin_y - props.offset_x = 0 - props.offset_y = 0 - - expected_x = int((margin_x / 100.0) * self.canvas_width) - expected_y = int((margin_y / 100.0) * self.canvas_height) - - try: - x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, - self.text_width, self.text_height) - - if x == expected_x and y == expected_y: - self.log_test(f"Margins {margin_x}%/{margin_y}%", True, f"Position: ({x}, {y})") - else: - self.log_test(f"Margins {margin_x}%/{margin_y}%", False, - f"Incorrect margin calculation", (expected_x, expected_y), (x, y)) - - except Exception as e: - self.log_test(f"Margins {margin_x}%/{margin_y}%", False, f"Exception: {str(e)}") - - def test_manual_positioning(self): - """Test manual positioning mode with percentage and pixel units""" - print("\n=== Testing Manual Positioning ===") - - # Test percentage mode - props = MockProps() - props.position_mode = 'MANUAL' - props.manual_position_unit = 'PERCENTAGE' - props.manual_position_x = 25.0 # 25% - props.manual_position_y = 75.0 # 75% - - expected_x = int((25.0 / 100.0) * self.canvas_width) # 256 - expected_y = int((75.0 / 100.0) * self.canvas_height) # 768 - - try: - x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, - self.text_width, self.text_height) - - if x == expected_x and y == expected_y: - self.log_test("Manual Percentage Mode", True, f"Position: ({x}, {y})") - else: - self.log_test("Manual Percentage Mode", False, - f"Incorrect calculation", (expected_x, expected_y), (x, y)) - - except Exception as e: - self.log_test("Manual Percentage Mode", False, f"Exception: {str(e)}") - - # Test pixel mode - props.manual_position_unit = 'PIXELS' - props.manual_position_x = 100.0 - props.manual_position_y = 200.0 - - expected_x = 100 - expected_y = 200 - - try: - x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, - self.text_width, self.text_height) - - if x == expected_x and y == expected_y: - self.log_test("Manual Pixel Mode", True, f"Position: ({x}, {y})") - else: - self.log_test("Manual Pixel Mode", False, - f"Incorrect calculation", (expected_x, expected_y), (x, y)) - - except Exception as e: - self.log_test("Manual Pixel Mode", False, f"Exception: {str(e)}") - - def test_canvas_constraints(self): - """Test canvas constraint functionality""" - print("\n=== Testing Canvas Constraints ===") - - # Test position that would go out of bounds - props = MockProps() - props.position_mode = 'MANUAL' - props.manual_position_unit = 'PIXELS' - props.manual_position_x = 1000.0 # Would put text partially outside (text_width=200) - props.manual_position_y = 1000.0 # Would put text partially outside (text_height=50) - props.constrain_to_canvas = True - - expected_x = self.canvas_width - self.text_width # 824 - expected_y = self.canvas_height - self.text_height # 974 - - try: - x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, - self.text_width, self.text_height) - - if x == expected_x and y == expected_y: - self.log_test("Canvas Constraints (constrained)", True, f"Position: ({x}, {y})") - else: - self.log_test("Canvas Constraints (constrained)", False, - f"Constraint failed", (expected_x, expected_y), (x, y)) - - except Exception as e: - self.log_test("Canvas Constraints (constrained)", False, f"Exception: {str(e)}") - - # Test with constraints disabled - props.constrain_to_canvas = False - expected_x = 1000 # Should not be constrained - expected_y = 1000 - - try: - x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, - self.text_width, self.text_height) - - if x == expected_x and y == expected_y: - self.log_test("Canvas Constraints (unconstrained)", True, f"Position: ({x}, {y})") - else: - self.log_test("Canvas Constraints (unconstrained)", False, - f"Should not be constrained", (expected_x, expected_y), (x, y)) - - except Exception as e: - self.log_test("Canvas Constraints (unconstrained)", False, f"Exception: {str(e)}") - - def test_offset_application(self): - """Test pixel offset fine-tuning""" - print("\n=== Testing Pixel Offset Application ===") - - props = MockProps() - props.anchor_point = 'MIDDLE_CENTER' - props.position_mode = 'PRESET' - props.margin_x = 0.0 - props.margin_y = 0.0 - props.offset_x = 50 # 50px right - props.offset_y = -25 # 25px up - - base_x = (self.canvas_width - self.text_width) // 2 # 412 - base_y = (self.canvas_height - self.text_height) // 2 # 487 - expected_x = base_x + props.offset_x # 462 - expected_y = base_y + props.offset_y # 462 - - try: - x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, - self.text_width, self.text_height) - - if x == expected_x and y == expected_y: - self.log_test("Pixel Offset Application", True, f"Position: ({x}, {y})") - else: - self.log_test("Pixel Offset Application", False, - f"Offset not applied correctly", (expected_x, expected_y), (x, y)) - - except Exception as e: - self.log_test("Pixel Offset Application", False, f"Exception: {str(e)}") - - def test_margin_synchronization(self): - """Test margin link functionality""" - print("\n=== Testing Margin Synchronization ===") - - props = MockProps() - props.margins_linked = True - props.margin_x = 15.0 - props.margin_y = 10.0 - - # Test X margin sync - sync_margin_values(props, 'x') - if props.margin_y == props.margin_x: - self.log_test("Margin Sync (X to Y)", True, f"Y margin updated to {props.margin_y}") - else: - self.log_test("Margin Sync (X to Y)", False, - f"Y margin not synced", props.margin_x, props.margin_y) - - # Reset and test Y margin sync - props.margin_x = 20.0 - props.margin_y = 25.0 - sync_margin_values(props, 'y') - if props.margin_x == props.margin_y: - self.log_test("Margin Sync (Y to X)", True, f"X margin updated to {props.margin_x}") - else: - self.log_test("Margin Sync (Y to X)", False, - f"X margin not synced", props.margin_y, props.margin_x) - - # Test unlinked margins - props.margins_linked = False - original_x = props.margin_x - original_y = props.margin_y - sync_margin_values(props, 'x') - - if props.margin_x == original_x and props.margin_y == original_y: - self.log_test("Margin Sync (Unlinked)", True, "Margins unchanged when unlinked") - else: - self.log_test("Margin Sync (Unlinked)", False, - "Margins changed when they shouldn't") - - def test_edge_cases(self): - """Test edge cases and boundary conditions""" - print("\n=== Testing Edge Cases ===") - - # Test zero canvas dimensions - props = MockProps() - try: - x, y = calculate_text_position(props, 0, 0, self.text_width, self.text_height) - self.log_test("Zero Canvas Dimensions", True, f"Handled gracefully: ({x}, {y})") - except Exception as e: - self.log_test("Zero Canvas Dimensions", False, f"Exception: {str(e)}") - - # Test text larger than canvas - props = MockProps() - props.anchor_point = 'TOP_LEFT' - props.constrain_to_canvas = True - large_text_width = 2000 - large_text_height = 2000 - - try: - x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, - large_text_width, large_text_height) - - if x <= 0 and y <= 0: - self.log_test("Text Larger Than Canvas", True, f"Constrained to: ({x}, {y})") - else: - self.log_test("Text Larger Than Canvas", False, - f"Not properly constrained: ({x}, {y})") - except Exception as e: - self.log_test("Text Larger Than Canvas", False, f"Exception: {str(e)}") - - # Test maximum margin values (50%) - props = MockProps() - props.anchor_point = 'TOP_LEFT' - props.margin_x = 50.0 - props.margin_y = 50.0 - - try: - x, y = calculate_text_position(props, self.canvas_width, self.canvas_height, - self.text_width, self.text_height) - - expected_x = int(0.5 * self.canvas_width) # 512 - expected_y = int(0.5 * self.canvas_height) # 512 - - if x == expected_x and y == expected_y: - self.log_test("Maximum Margins (50%)", True, f"Position: ({x}, {y})") - else: - self.log_test("Maximum Margins (50%)", False, - f"Incorrect calculation", (expected_x, expected_y), (x, y)) - except Exception as e: - self.log_test("Maximum Margins (50%)", False, f"Exception: {str(e)}") - - def analyze_ui_integration(self): - """Analyze UI integration potential issues""" - print("\n=== Analyzing UI Integration ===") - - issues = [] - - # Check anchor point matrix structure - matrix = get_anchor_point_matrix() - if len(matrix) != 3 or any(len(row) != 3 for row in matrix): - issues.append("❌ Anchor point matrix is not 3x3") - else: - print("✅ Anchor point matrix structure is correct") - - # Check all anchor points are defined - all_anchors = [anchor for row in matrix for anchor in row] - expected_anchors = [ - 'TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT', - 'MIDDLE_LEFT', 'MIDDLE_CENTER', 'MIDDLE_RIGHT', - 'BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT' - ] - - missing_anchors = set(expected_anchors) - set(all_anchors) - if missing_anchors: - issues.append(f"❌ Missing anchor points: {missing_anchors}") - else: - print("✅ All required anchor points are defined") - - # Check for property type consistency - props = MockProps() - - # Test property ranges - if hasattr(props, 'margin_x') and hasattr(props, 'margin_y'): - print("✅ Margin properties exist") - else: - issues.append("❌ Margin properties missing") - - if hasattr(props, 'offset_x') and hasattr(props, 'offset_y'): - print("✅ Offset properties exist") - else: - issues.append("❌ Offset properties missing") - - self.issues_found.extend(issues) - - def run_all_tests(self): - """Run all tests and generate comprehensive report""" - print("🔍 ENHANCED POSITIONING SYSTEM TEST SUITE") - print("=" * 50) - - try: - self.test_anchor_point_calculations() - self.test_margin_calculations() - self.test_manual_positioning() - self.test_canvas_constraints() - self.test_offset_application() - self.test_margin_synchronization() - self.test_edge_cases() - self.analyze_ui_integration() - - except Exception as e: - print(f"❌ Critical error during testing: {e}") - traceback.print_exc() - - self.generate_report() - - def generate_report(self): - """Generate comprehensive test report""" - print("\n" + "=" * 60) - print("📊 COMPREHENSIVE TEST REPORT") - print("=" * 60) - - total_tests = len(self.test_results) - passed_tests = sum(1 for result in self.test_results if result['passed']) - failed_tests = total_tests - passed_tests - - print(f"📈 SUMMARY:") - print(f" Total Tests: {total_tests}") - print(f" ✅ Passed: {passed_tests}") - print(f" ❌ Failed: {failed_tests}") - print(f" Success Rate: {(passed_tests/total_tests*100):.1f}%" if total_tests > 0 else "No tests run") - - if self.issues_found: - print(f"\n🐛 ISSUES IDENTIFIED ({len(self.issues_found)}):") - for issue in self.issues_found: - print(f" {issue}") - - # Categorize issues by severity - critical_issues = [] - major_issues = [] - minor_issues = [] - - for result in self.test_results: - if not result['passed']: - if 'exception' in result['details'].lower(): - critical_issues.append(result) - elif 'constraint' in result['test'].lower() or 'calculation' in result['details'].lower(): - major_issues.append(result) - else: - minor_issues.append(result) - - if critical_issues: - print(f"\n🚨 CRITICAL ISSUES ({len(critical_issues)}):") - for issue in critical_issues: - print(f" • {issue['test']}: {issue['details']}") - - if major_issues: - print(f"\n⚠️ MAJOR ISSUES ({len(major_issues)}):") - for issue in major_issues: - print(f" • {issue['test']}: {issue['details']}") - - if minor_issues: - print(f"\n ℹ️ MINOR ISSUES ({len(minor_issues)}):") - for issue in minor_issues: - print(f" • {issue['test']}: {issue['details']}") - - # Recommendations - print(f"\n🎯 RECOMMENDATIONS:") - - if critical_issues: - print(" 1. Fix critical exceptions before deployment") - print(" 2. Add proper error handling for edge cases") - - if major_issues: - print(" 3. Review positioning calculation logic") - print(" 4. Test constraint functionality thoroughly") - - print(" 5. Add comprehensive unit tests to the addon") - print(" 6. Implement validation for property ranges") - print(" 7. Add user feedback for invalid input combinations") - - return { - 'total_tests': total_tests, - 'passed': passed_tests, - 'failed': failed_tests, - 'issues': self.issues_found, - 'critical_issues': len(critical_issues), - 'major_issues': len(major_issues), - 'minor_issues': len(minor_issues) - } - -if __name__ == "__main__": - print("🚀 Starting Enhanced Positioning System Test Suite...") - test_suite = PositioningTestSuite() - results = test_suite.run_all_tests() - - print(f"\n✨ Testing completed!") - print(f"Check the detailed report above for analysis and recommendations.") \ No newline at end of file diff --git a/test_preset_unicode.py b/test_preset_unicode.py deleted file mode 100644 index acaea6f..0000000 --- a/test_preset_unicode.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to verify that preset names with Unicode characters like 'ö' work correctly. -This script tests the validation logic outside of Blender. -""" - -import re -import json -import os -import tempfile - -def test_preset_name_validation(): - """Test the new preset name validation regex""" - - # Test cases with expected results - test_cases = [ - # Valid names (should pass) - ("Test Preset", True), - ("Mein schönes Preset", True), # German with ö - ("Préférence française", True), # French with accents - ("Configuración español", True), # Spanish with ñ - ("Test_123", True), - ("Hello-World", True), - ("Тест", True), # Cyrillic - ("テスト", True), # Japanese - ("测试", True), # Chinese - - # Invalid names (should fail) - ("TestPreset", False), # Contains > - ("Test:Preset", False), # Contains : - ("Test\"Preset", False), # Contains " - ("Test|Preset", False), # Contains | - ("Test?Preset", False), # Contains ? - ("Test*Preset", False), # Contains * - ("Test/Preset", False), # Contains / - ("Test\\Preset", False), # Contains \ - ("", False), # Empty string - ] - - print("Testing preset name validation...") - - for preset_name, should_pass in test_cases: - # Test the Unicode regex pattern - unicode_pattern_match = bool(re.match(r'^[\w\s\-]+$', preset_name, re.UNICODE)) - - # Test filesystem-unsafe characters - invalid_chars = ['<', '>', ':', '"', '|', '?', '*', '/', '\\'] - has_invalid_chars = any(char in preset_name for char in invalid_chars) - - # Combined validation (same as in the actual code) - validation_passes = unicode_pattern_match and not has_invalid_chars and preset_name.strip() - - status = "✅ PASS" if validation_passes == should_pass else "❌ FAIL" - print(f"{status}: '{preset_name}' -> Expected: {should_pass}, Got: {validation_passes}") - - if validation_passes != should_pass: - print(f" Unicode match: {unicode_pattern_match}, Has invalid chars: {has_invalid_chars}") - -def test_json_serialization(): - """Test that Unicode characters work correctly with JSON serialization/deserialization""" - - print("\nTesting JSON serialization with Unicode characters...") - - test_data = { - 'name': 'Schönes Preset', - 'text': 'Höllö Wörld', - 'texture_width': 1024, - 'texture_height': 1024, - 'font_size': 96 - } - - try: - # Test JSON encoding/decoding - json_str = json.dumps(test_data, indent=2, ensure_ascii=False) - decoded_data = json.loads(json_str) - - print("✅ JSON serialization successful") - print(f"Original name: {test_data['name']}") - print(f"Decoded name: {decoded_data['name']}") - print(f"Names match: {test_data['name'] == decoded_data['name']}") - - # Test file I/O - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: - json.dump(test_data, f, indent=2, ensure_ascii=False) - temp_file = f.name - - with open(temp_file, 'r', encoding='utf-8') as f: - file_data = json.load(f) - - os.unlink(temp_file) - - print("✅ File I/O successful") - print(f"File data name: {file_data['name']}") - print(f"File names match: {test_data['name'] == file_data['name']}") - - except Exception as e: - print(f"❌ JSON serialization failed: {e}") - -def test_filesystem_compatibility(): - """Test creating files with Unicode names""" - - print("\nTesting filesystem compatibility...") - - test_names = [ - "schönes_preset", - "café_preset", - "naïve_preset", - "test_ñ_preset" - ] - - temp_dir = tempfile.mkdtemp() - print(f"Testing in directory: {temp_dir}") - - for name in test_names: - try: - filename = f"{name}.json" - filepath = os.path.join(temp_dir, filename) - - # Create file - with open(filepath, 'w', encoding='utf-8') as f: - json.dump({'name': name}, f, ensure_ascii=False) - - # Read file back - with open(filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - - # Clean up - os.unlink(filepath) - - print(f"✅ {name}: File created and read successfully") - - except Exception as e: - print(f"❌ {name}: Failed - {e}") - - # Clean up temp directory - os.rmdir(temp_dir) - -if __name__ == "__main__": - print("=" * 60) - print("Testing Text Texture Generator Preset Unicode Support") - print("=" * 60) - - test_preset_name_validation() - test_json_serialization() - test_filesystem_compatibility() - - print("\n" + "=" * 60) - print("Test completed! Check results above.") - print("=" * 60) \ No newline at end of file diff --git a/test_ui_and_compatibility.py b/test_ui_and_compatibility.py deleted file mode 100644 index 69c949d..0000000 --- a/test_ui_and_compatibility.py +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env python3 - -""" -Advanced UI Integration and Backward Compatibility Test Suite -for the Text Texture Generator's enhanced positioning system. - -This focuses on potential issues that the basic positioning tests might miss. -""" - -import sys -import re - -class UICompatibilityTestSuite: - """Test suite focused on UI integration and compatibility issues""" - - def __init__(self): - self.issues_found = [] - self.warnings = [] - - def analyze_ui_layout_structure(self): - """Analyze the UI layout structure from the addon code""" - print("\n=== Analyzing UI Layout Structure ===") - - # Read the addon code to analyze UI structure - try: - with open('__init__.py', 'r') as f: - addon_code = f.read() - except FileNotFoundError: - self.issues_found.append("❌ Cannot read __init__.py for UI analysis") - return - - # Check for Positioning & Alignment section - if "Positioning & Alignment" in addon_code: - print("✅ 'Positioning & Alignment' section found in UI") - else: - self.issues_found.append("❌ 'Positioning & Alignment' section not found in UI") - - # Check for position mode toggle - if 'position_mode' in addon_code and 'PRESET' in addon_code and 'MANUAL' in addon_code: - print("✅ Position mode toggle implementation found") - else: - self.issues_found.append("❌ Position mode toggle not properly implemented") - - # Check for 3x3 anchor grid implementation - anchor_grid_patterns = [ - r'get_anchor_point_matrix\(\)', - r'for.*anchor.*in.*matrix', - r'grid_row.*=.*position_col\.row' - ] - - grid_found = all(re.search(pattern, addon_code, re.DOTALL) for pattern in anchor_grid_patterns) - if grid_found: - print("✅ 3x3 anchor grid UI implementation found") - else: - self.issues_found.append("❌ 3x3 anchor grid UI implementation incomplete") - - # Check for margin controls with link functionality - if 'margins_linked' in addon_code and 'LINKED' in addon_code and 'UNLINKED' in addon_code: - print("✅ Margin link/unlink controls found") - else: - self.issues_found.append("❌ Margin link/unlink controls not properly implemented") - - # Check for conditional UI visibility - if 'position_mode == ' in addon_code: - print("✅ Conditional UI visibility based on position mode found") - else: - self.warnings.append("⚠️ No conditional UI visibility found - controls might always be visible") - - def test_backward_compatibility_integration(self): - """Test how the new system integrates with existing text_align property""" - print("\n=== Testing Backward Compatibility Integration ===") - - try: - with open('__init__.py', 'r') as f: - addon_code = f.read() - except FileNotFoundError: - self.issues_found.append("❌ Cannot read addon code for compatibility analysis") - return - - # Find the backward compatibility section in generate_texture_image - compat_section = re.search( - r'# Apply legacy text_align.*?props\.anchor_point == \'MIDDLE_CENTER\'.*?# Apply offsets even in legacy mode', - addon_code, re.DOTALL - ) - - if compat_section: - print("✅ Backward compatibility section found in texture generation") - - # Check if all text_align values are handled - compat_code = compat_section.group(0) - align_values = ['left', 'center', 'right'] - missing_aligns = [] - - for align in align_values: - if f"text_align == '{align}'" not in compat_code: - missing_aligns.append(align) - - if missing_aligns: - self.issues_found.append(f"❌ Missing text_align compatibility for: {missing_aligns}") - else: - print("✅ All text_align values have compatibility handling") - - # Check if legacy mode only applies to MIDDLE_CENTER - if "anchor_point == 'MIDDLE_CENTER'" in compat_code: - print("✅ Legacy compatibility properly scoped to MIDDLE_CENTER anchor") - else: - self.issues_found.append("❌ Legacy compatibility scope issue - should only apply to MIDDLE_CENTER") - - else: - self.issues_found.append("❌ Backward compatibility section not found in texture generation") - - # Check if text_align property is still defined - if re.search(r'text_align.*EnumProperty', addon_code): - print("✅ Legacy text_align property still defined") - else: - self.issues_found.append("❌ Legacy text_align property not found - breaking change!") - - def analyze_property_update_chains(self): - """Analyze property update chains for consistency""" - print("\n=== Analyzing Property Update Chains ===") - - try: - with open('__init__.py', 'r') as f: - addon_code = f.read() - except FileNotFoundError: - self.issues_found.append("❌ Cannot read addon code for update chain analysis") - return - - # Find all properties with update=update_live_preview - update_properties = re.findall(r'(\w+):\s*\w+Property.*?update=update_live_preview', addon_code, re.DOTALL) - - expected_positioning_props = [ - 'anchor_point', 'position_mode', 'margin_x', 'margin_y', - 'margins_linked', 'offset_x', 'offset_y', 'manual_position_x', - 'manual_position_y', 'manual_position_unit', 'constrain_to_canvas' - ] - - missing_updates = [] - for prop in expected_positioning_props: - if prop not in update_properties: - missing_updates.append(prop) - - if missing_updates: - self.issues_found.append(f"❌ Properties missing live preview updates: {missing_updates}") - else: - print("✅ All positioning properties have live preview updates") - - # Check for visual feedback properties - visual_props = ['show_position_guides', 'show_text_bounds', 'show_canvas_grid'] - missing_visual_updates = [] - for prop in visual_props: - if prop not in update_properties: - missing_visual_updates.append(prop) - - if missing_visual_updates: - self.issues_found.append(f"❌ Visual feedback properties missing updates: {missing_visual_updates}") - else: - print("✅ All visual feedback properties have live preview updates") - - def run_compatibility_tests(self): - """Run all compatibility and UI integration tests""" - print("🔍 UI INTEGRATION & COMPATIBILITY TEST SUITE") - print("=" * 55) - - try: - self.analyze_ui_layout_structure() - self.test_backward_compatibility_integration() - self.analyze_property_update_chains() - - except Exception as e: - print(f"❌ Critical error during compatibility testing: {e}") - import traceback - traceback.print_exc() - - self.generate_compatibility_report() - - def generate_compatibility_report(self): - """Generate compatibility and UI integration report""" - print("\n" + "=" * 60) - print("📊 UI INTEGRATION & COMPATIBILITY REPORT") - print("=" * 60) - - total_issues = len(self.issues_found) - total_warnings = len(self.warnings) - - print(f"📈 SUMMARY:") - print(f" 🚨 Critical Issues: {total_issues}") - print(f" ⚠️ Warnings: {total_warnings}") - - if self.issues_found: - print(f"\n🚨 CRITICAL ISSUES FOUND ({total_issues}):") - for issue in self.issues_found: - print(f" {issue}") - - if self.warnings: - print(f"\n⚠️ WARNINGS ({total_warnings}):") - for warning in self.warnings: - print(f" {warning}") - - if not self.issues_found and not self.warnings: - print("\n🎉 NO CRITICAL ISSUES OR WARNINGS FOUND!") - print(" The UI integration and compatibility appear to be well implemented.") - - # Recommendations based on findings - print(f"\n🎯 RECOMMENDATIONS:") - - if total_issues > 0: - print(" 1. 🚨 Address all critical issues before deployment") - print(" 2. Test UI elements thoroughly in actual Blender environment") - - if total_warnings > 0: - print(" 3. ⚠️ Review and address warnings for robustness") - print(" 4. Add explicit error handling for edge cases") - - print(" 5. ✅ Add automated UI tests to prevent regressions") - print(" 6. ✅ Test backward compatibility with existing presets") - print(" 7. ✅ Validate all property ranges in actual use cases") - - return { - 'critical_issues': total_issues, - 'warnings': total_warnings, - 'issues_list': self.issues_found, - 'warnings_list': self.warnings - } - -if __name__ == "__main__": - print("🚀 Starting UI Integration & Compatibility Test Suite...") - test_suite = UICompatibilityTestSuite() - results = test_suite.run_compatibility_tests() - - print(f"\n✨ UI & Compatibility testing completed!") \ No newline at end of file