From 4b39131efadec389c6efd161aa99c62b2a2ee7ad Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 11 Aug 2025 12:48:35 +0700 Subject: [PATCH] update --- Archive.zip | Bin 9626 -> 18883 bytes __init__.py | 1707 ++++++++++++++++++++++++++++------ test_positioning_system.py | 558 +++++++++++ test_ui_and_compatibility.py | 231 +++++ 4 files changed, 2202 insertions(+), 294 deletions(-) create mode 100644 test_positioning_system.py create mode 100644 test_ui_and_compatibility.py diff --git a/Archive.zip b/Archive.zip index 9a05eb364808bb8b5b34fb1aa6f0cf640f73d3f8..35251b6204a78eef196d7023a07f36bc21bceb7a 100644 GIT binary patch delta 18479 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 delta 9165 zcmV;;BQo5>lL4Ab4Nyx11QY-W00;npYY34IFn{rwm1y&rm1yypm1ylf?Q+{VlK=A* zIHj&k%_#CmW>UMZ)6_Y!>`|R#du@3#dnuQSk|3Kq6saXC$G)8JHSU4##jd;YK>!3P zS#dIZb)%VbBm!svjYfY1jOL3pE7*AP@FJ35=RCiCG0W09KJ69{3!XDscDCSISfp8p zoqvZ3kKvyzg-6B1ah5I@9dmmh)e0_W1>aPb&%MytU6-Lw;W{ z{IkqBJLZYNXsaV?h0Ed=enGwWVK!m!aRxE_mpT0S*E#*@&iPNO_8rf1m@-rk_B!l! zhwZgx$vEc8lp~*AOtxhJWFh z{U^J;4FL}8Pa_zmQPHX-Q5*rK{KVdefb=MgWfe8nDbFWav`}5knRB3y@P@-u5JGZB zw1dCcr>MAP;bgL;YEwXQf|Jczm`nl3c@ZWBi=#Wv78$>TX{erHe*7lQ9t0M}V4&;~ zSE%=l9{_hX9Gnb>e`QBkr|*V?vwzde=kDUgl+VOu<9M`);yC>jwD-k4Tdjd8VCPi8 zq8Urlf`xZs6o)_oP=jg~*@I|;A4#(3gA;nh?fi$L`Pn^$(! z?RHyjU8~$R-N`MV{5G0E<=`W1oi4xUlVyQJ>9E$Tc?&JZi`(3b4I($d zg+j{!sqqL}y7;<@)3JV5Hm5#dk3W{^5WV*_ljw>kLA%-<7ED#|vDSwunWmreRyjJ= z#b_F3*v&_)gs|~$e_OWO#(!bw+j19}pEU0x24FcBuy>X@&w|!}Z67QahhY(Jp8!LM zx8FyTEX~td;eu;h;I6G-uuavbgu%7G>lVcfmlVHD#FrGFl#O_7Px&y+K1Ik6*h@AK zCuf&Wnfmr60op!^##xv>)X%@Qx{4n~PP1n~1$~*~#4N1joKSEQC4bBNC!t`OXWRL0 znDOmd{p_()Osl-2sviqoPuup|jb~qhjEw!4X%qpQEYW4}At1VPbign2Kvit-SLT5* zQT_n9a$wbJR`&=R8z7hH;-5hE_cu^NbZumfSNE^|3gX ze;fcG6%4?i>NsD-5r4{2J(6*lbF35S5x3hPcdpk4tiN|N4!mu`gVrlt9M)==Pe$^| zN~rlq4ewe3$1&A5l!B^H|G<(oV`KvGU^DS5$z&W94M8dSN(m?{1AEspiAKsn`5Xay z=XsFxc-Eo*a8ijdQ2-NKz-^d^MUe%bF?ZP3Bm`MK6_Rz!AU#j*u2~lbSryD>-)5aA zFlGzbxE&T~YUq@-(55{I5J*!Da#$ZerJ8wHARj1r|HqwcV*;YDaIpaWHz-NH!^{p^ zh|+!bSwDLOB!5Qid$!v)CLUYM&m_Uz4Xqk*gazGN4)0&uz28+6s?6GZ9g2Va@PzK(h(mec)Bk#0on3yo0 ze+6Co{G$KMp#Om#_fPv52dGVdWwrW7njj--e2mBx34h3F?SSGqMY6f&(amjPF0`~; zidzoWNwEY&N4A%y%q!89%oNd@f#75p{l8odF8YUlrQFf*oAuL<_~$SSKXp7BtNsG^ zNiGJ;?_sE>4F!|D150k|==w7?KT`IoTbxo@u+KE9$NSG>N{=m%I{Olg@*EKXmTEKy zzE^gr>wgH`^g!FY4|d;3G?%a>!pd@q|7vGyz=!>}SI7Iz7`IxES3tLaUvl77)BVc$ zJGvT>G&akj4lR9Gk%MYsKW&`lPd5Irh@PJ#Aq=9!^o z%#5%HAj5mmN#!cQA|R$kOI?S3XLZpo(XjxAgv%gW@0hJsQEl70)s=Xq>ab2`>qm)> zX^D2l*^RlBwd5FZwZ%8@QmX!snqi)=1?(kz2Yg-oI>;m)hm+rKvUHi0TM*c_KvP*Z zDu1#t$rr#sKwjA@?Hn;oo4=dzPr=s3@!Nwf5TrKWf(JXo|ASgWIW{smEP&&i(IkzD zSc>H$<^dvL0!y{`!MwsDJPe z#=lB7xgfRd!d}^h%~I`JgCPz&9;f#~AbhK=bul8r}-!(xO>dsiFw< zMAQ-(q=s66xmbO4>@o7f82p^p`k#7C8z1!x|HUi*$5L;+)Yck%7uU6sS(r12&0B&E zyGJx~vU=HP?o!HHtmY+q3n~<_uzwk7EXb@u^8)t6ij>VFAf!-PtvJackOH$1BCQmo zD|^IV?*~wZ)x$W`?2aJsXSQ6ms{UKtuv$Q;0-aJAzWln zXmzkrD^QuDaTYzfT@l`<^N^Z(TUGJ-G|#a5JOz<{ck$0WzT-tS35Bn*p?|sEZY_NJ zyF9q#Nt984Ya^%DLA((rT(7a>(c7@vWJRS3j#+-a1Qqnv1;1IwVYYI{wVh|WR%XA{6Y7+5@Og!xR;)(f&p^7@6Yi^LW3)%+;A zc$Fhkv?P-<*TTCiB;C&#jE3LV_G6=r7Oqm;_1rYbBN6(9rHjdyFh}bSRvE4Ye$~(` zZgby=t7BY*dFkOXEu9ePE#v681)Ts45AC=P#>)bgzR;(%%&S121q8@rzno8-!a$O2me%K)rau}m|`K)7uX`rBU({1 z>j>XfgN|zU3_nb1n9;A&8pEI6yvU+O&?X7MS|F_Alzm9Ec-pECsKQrhPxqLAFX4mQ z&4h&`%T?DQXdm~k0e?^QZRgt4#CG4wHm`M?-Rp|38k}8H%%#KrooDG`bQht&P(J!4 zjF+6gw;NPhs(?&Pd;h36pFaB%by(UQ`wZAjG}nuVYhRB7>xn(67+l9(`T2gVrii8#7R=G@ktOHvt8IqFVZ6OMULTYomTkP?I=1l4mrxi;J0 zW_yNTs19g`6Y;>Tf`lK^8`j`%!lrI56BRTCc86I-m`_qVHXm>v60NUHBs()PfT1{| zn$|9Sg1f7%ZacaxCs$d?`iD~aijEH-KEbiV^d32z0aNL~+Pm@o&1bGDA z8=PGZlDuPfNJqNSgGyXh$8VIQU5-nKVujF=?HEkL45rG*9k!LjCgW!0R7#lB)ND(& zTK)lRl!`3Lo@&>nMuIRD(x^(=;w3wXKZOsup0Az_#(zBK=^f=o-Dxz8c!HTxQ$`6| zB^*m)ykr_TKphGo$wZ1<@C>cyFq!a?Z+A#O@3d!rm^{*9Ll zhC(YKD?|hzvpW*o)v%JNIucb)@v6fdmEecq8s=WMfc(UE$>jPFO?4Co7?C4^#GF1+ zm*xDtm4B2Ru@JFsb?W%%No6;pKrjzZRQ2~ISVgrXsdVyPO(*W?yi+h%W_5JW9EOVE z>1XXqwOdcMTMsi*T;;L|vqDDJi8>|y6CWzjH72{FsGWrO$O4m?_seSJ&o{ZB6LlII z#Eo?Nl=z^YP{Y9dTBQ|XCT?l4$s0q*;1Ez3?R?ekK zDdr6h2162IXQ8d}Y@CK!mE3?q8O4}<1_o0?D{26%5|wDAbhF(z^>iHU?)|C5%0D># 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 Xn3a=JNh< 3: bg_color += (int(props.background_color[3] * 255),) - img = Image.new('RGBA', (width, height), bg_color) + base_img = Image.new('RGBA', (width, height), bg_color) - print(f"Image created: {img.size}") + print(f"Base image created: {base_img.size}") - draw = ImageDraw.Draw(img) + # Collect all elements with z-index (text is z-index 0) + elements = [] - # Load font with proper size handling + # Add image overlays + for overlay in props.image_overlays: + if overlay.enabled and overlay.image_path and os.path.exists(overlay.image_path): + try: + overlay_img = Image.open(overlay.image_path).convert('RGBA') + + # Scale overlay + if overlay.scale != 1.0: + new_size = (int(overlay_img.width * overlay.scale * scale_factor), + int(overlay_img.height * overlay.scale * scale_factor)) + overlay_img = overlay_img.resize(new_size, Image.LANCZOS) + + # Rotate overlay if needed + if overlay.rotation != 0: + overlay_img = overlay_img.rotate(overlay.rotation, expand=True) + + # Calculate position + x_pos = int(overlay.x_position * width - overlay_img.width / 2) + y_pos = int((1.0 - overlay.y_position) * height - overlay_img.height / 2) # Flip Y for intuitive top-to-bottom + + elements.append((overlay.z_index, 'image', overlay_img, x_pos, y_pos)) + print(f"Added overlay {overlay.name} at z-index {overlay.z_index}, pos ({x_pos}, {y_pos})") + + except Exception as e: + print(f"Error loading overlay {overlay.name}: {e}") + + # Create text element (z-index 0) + text_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) + draw = ImageDraw.Draw(text_img) + + # Load font with enhanced mixed-case validation font = None font_loaded = False - try: - if props.use_custom_font and props.custom_font_path and os.path.exists(props.custom_font_path): - test_font = ImageFont.truetype(props.custom_font_path, font_size_pixels) - # Test the font before accepting it - test_draw = ImageDraw.Draw(Image.new('RGB', (10, 10))) - test_draw.textbbox((0, 0), "A", font=test_font) - font = test_font - font_loaded = True - print(f"Loaded custom font: {props.custom_font_path}") - elif props.font_path != "default" and os.path.exists(props.font_path): - test_font = ImageFont.truetype(props.font_path, font_size_pixels) - # Test the font before accepting it - test_draw = ImageDraw.Draw(Image.new('RGB', (10, 10))) - test_draw.textbbox((0, 0), "A", font=test_font) - font = test_font - font_loaded = True - print(f"Loaded font: {props.font_path}") - except Exception as e: - print(f"Font loading error: {e}") - font_loaded = False + print("DEBUG UPPERCASE FIX: Starting enhanced font loading with mixed-case validation") - # Better fallback - try to find a system TrueType font + # Try custom font first with validation + if props.use_custom_font and props.custom_font_path: + print(f"DEBUG UPPERCASE FIX: Testing custom font: {props.custom_font_path}") + font, error_msg = get_validated_font(props.custom_font_path, font_size_pixels) + if font: + font_loaded = True + print(f"SUCCESS: Loaded and validated custom font: {props.custom_font_path}") + else: + print(f"FAILED: Custom font validation failed: {error_msg}") + + # Try selected system font with validation + if not font_loaded and props.font_path != "default": + print(f"DEBUG UPPERCASE FIX: Testing system font: {props.font_path}") + font, error_msg = get_validated_font(props.font_path, font_size_pixels) + if font: + font_loaded = True + print(f"SUCCESS: Loaded and validated system font: {props.font_path}") + else: + print(f"FAILED: System font validation failed: {error_msg}") + + # Enhanced fallback with mixed-case validation if not font_loaded: - try: - # Try to find a basic system font - system = platform.system() - fallback_fonts = [] - - if system == "Windows": - fallback_fonts = [ - "C:/Windows/Fonts/arial.ttf", - "C:/Windows/Fonts/Arial.ttf", - "C:/Windows/Fonts/calibri.ttf", - "C:/Windows/Fonts/tahoma.ttf" - ] - elif system == "Darwin": # macOS - fallback_fonts = [ - "/System/Library/Fonts/Helvetica.ttc", - "/Library/Fonts/Arial.ttf", - "/System/Library/Fonts/Avenir.ttc" - ] - else: # Linux - fallback_fonts = [ - "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", - "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf" - ] - - for fallback_path in fallback_fonts: - if os.path.exists(fallback_path): - try: - font = ImageFont.truetype(fallback_path, font_size_pixels) - # Test font by trying to get bbox of a simple character - test_draw = ImageDraw.Draw(Image.new('RGB', (10, 10))) - test_draw.textbbox((0, 0), "A", font=font) - font_loaded = True - print(f"Using fallback font: {fallback_path}") - break - except Exception as font_error: - print(f"Failed to load fallback font {fallback_path}: {font_error}") - continue - except Exception: - pass + print("DEBUG UPPERCASE FIX: Starting validated fallback font search...") + fallback_fonts = get_system_fallback_fonts() + + for fallback_path in fallback_fonts: + print(f"DEBUG UPPERCASE FIX: Testing fallback font: {fallback_path}") + if os.path.exists(fallback_path): + font, error_msg = get_validated_font(fallback_path, font_size_pixels) + if font: + font_loaded = True + print(f"SUCCESS: Using validated fallback font: {fallback_path}") + break + else: + print(f"FAILED: Fallback font validation failed: {error_msg}") + else: + print(f"SKIP: Fallback font not found: {fallback_path}") - # Last resort - use default (but it won't respect size) + # Last resort - use default font but warn about mixed-case limitations if not font_loaded: font = ImageFont.load_default() - print("Warning: Using default font - size will not be accurate!") + font_loaded = True + print("WARNING: Using default font - this may not support mixed-case properly!") + print("WARNING: Consider installing additional fonts for proper mixed-case support") - # Calculate text position with error handling + # Calculate text position text = props.text if props.text and props.text.strip() else "Hello World" + # ENHANCED DIAGNOSTIC LOGGING for uppercase text issue + print(f"DEBUG UPPERCASE ISSUE - Original text input: '{props.text}'") + print(f"DEBUG UPPERCASE ISSUE - Processed text: '{text}'") + print(f"DEBUG UPPERCASE ISSUE - Text type: {type(text)}") + print(f"DEBUG UPPERCASE ISSUE - Has upper chars: {any(c.isupper() for c in text)}") + print(f"DEBUG UPPERCASE ISSUE - Has lower chars: {any(c.islower() for c in text)}") + print(f"DEBUG UPPERCASE ISSUE - Font being used: {font}") + if hasattr(font, 'path'): + print(f"DEBUG UPPERCASE ISSUE - Font path: {font.path}") + + # Test the current font's mixed-case rendering capability + print("DEBUG UPPERCASE ISSUE - Testing current font mixed-case capability...") + if test_font_mixed_case_support(font): + print("SUCCESS: Current font supports mixed-case rendering properly!") + else: + print("WARNING: Current font may not support mixed-case rendering properly!") + print("This could be the cause of uppercase-only text rendering") + try: bbox = draw.textbbox((0, 0), text, font=font) text_width = max(1, bbox[2] - bbox[0]) text_height = max(1, bbox[3] - bbox[1]) except (OSError, ZeroDivisionError, ValueError) as e: print(f"Error getting text bbox: {e}") - # Fallback: estimate text size text_width = int(len(text) * font_size_pixels * 0.6) text_height = font_size_pixels - # Calculate X position based on alignment - if props.text_align == 'center': - x = (width - text_width) // 2 - elif props.text_align == 'right': - x = width - text_width - padding - else: - x = padding + # ============================================================================ + # ENHANCED POSITIONING SYSTEM - Replaces hardcoded vertical centering + # ============================================================================ - # Center vertically - y = (height - text_height) // 2 + # Use enhanced positioning system + x, y = calculate_text_position(props, width, height, text_width, text_height) + + # Apply legacy text_align for backward compatibility if using MIDDLE_CENTER anchor + if props.anchor_point == 'MIDDLE_CENTER' and props.position_mode == 'PRESET': + if props.text_align == 'center': + x = (width - text_width) // 2 + elif props.text_align == 'right': + x = width - text_width - padding + elif props.text_align == 'left': + x = padding + + # Apply offsets even in legacy mode + x += props.offset_x + y += props.offset_y # Draw text text_color = tuple(int(c * 255) for c in props.text_color[:3]) if len(props.text_color) > 3: text_color += (int(props.text_color[3] * 255),) + print(f"DEBUG UPPERCASE ISSUE - About to draw text: '{text}' with font: {font}") draw.text((x, y), text, fill=text_color, font=font) print(f"Text drawn at ({x}, {y})") + print(f"DEBUG UPPERCASE ISSUE - Text drawing completed") - return img + # Add text to elements list + elements.append((0, 'text', text_img, 0, 0)) + + # Sort elements by z-index + elements.sort(key=lambda x: x[0]) + + # Composite all elements + final_img = base_img.copy() + for z_index, element_type, element_img, pos_x, pos_y in elements: + if element_type == 'text': + final_img = Image.alpha_composite(final_img, element_img) + elif element_type == 'image': + # Create a temporary image to paste the overlay + temp_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) + temp_img.paste(element_img, (pos_x, pos_y), element_img) + final_img = Image.alpha_composite(final_img, temp_img) + + print(f"Final composite image created with {len(elements)} elements") + return final_img except Exception as e: print(f"Error in generate_texture_image: {e}") @@ -267,22 +592,13 @@ def generate_preview(context): # Always generate preview - removed live_preview_enabled check try: - # Generate at preview size for performance, not full texture size - preview_size = props.preview_size # Default is 256 - # Calculate aspect ratio preserving dimensions - aspect_ratio = props.texture_width / props.texture_height - if aspect_ratio > 1: # Wider than tall - preview_w = preview_size - preview_h = int(preview_size / aspect_ratio) - else: # Taller than wide or square - preview_w = int(preview_size * aspect_ratio) - preview_h = preview_size + # Generate at full texture size for 100% quality preview + preview_w = props.texture_width + preview_h = props.texture_height - # Ensure minimum dimensions - preview_w = max(16, preview_w) - preview_h = max(16, preview_h) + print(f"[TTG] Generating 100% quality preview at full resolution: {preview_w}x{preview_h}") - print(f"Starting preview generation at {preview_w}x{preview_h} (preview size: {preview_size})") + 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 @@ -292,7 +608,7 @@ def generate_preview(context): return None # Apply preview background if needed - if props.preview_bg_type != 'transparent' or not props.background_transparent: + if props.preview_bg_type != 'transparent': from PIL import Image, ImageDraw # Create background image @@ -417,6 +733,75 @@ def update_live_preview(self, context): # PROPERTY GROUPS # ============================================================================ +class TEXT_TEXTURE_ImageOverlay(PropertyGroup): + """Single image overlay item""" + name: StringProperty( + name="Image Name", + description="Name identifier for this image overlay", + default="Overlay", + update=update_live_preview + ) + + image_path: StringProperty( + name="Image File", + description="Path to image file", + subtype='FILE_PATH', + default="", + update=update_live_preview + ) + + x_position: FloatProperty( + name="X Position", + description="X position (0.0 = left, 1.0 = right)", + default=0.5, + min=0.0, + max=1.0, + update=update_live_preview + ) + + y_position: FloatProperty( + name="Y Position", + description="Y position (0.0 = bottom, 1.0 = top)", + default=0.5, + min=0.0, + max=1.0, + update=update_live_preview + ) + + scale: FloatProperty( + name="Scale", + description="Scale of the image", + default=1.0, + min=0.1, + max=5.0, + update=update_live_preview + ) + + rotation: FloatProperty( + name="Rotation", + description="Rotation in degrees", + default=0.0, + min=-360.0, + max=360.0, + update=update_live_preview + ) + + z_index: IntProperty( + name="Z-Index", + description="Layer order (0=text level, -1=below text, 1+=above text)", + default=1, + min=-10, + max=10, + update=update_live_preview + ) + + enabled: BoolProperty( + name="Enabled", + description="Enable this image overlay", + default=True, + update=update_live_preview + ) + class TEXT_TEXTURE_Preset(PropertyGroup): """Single preset item""" name: StringProperty( @@ -435,6 +820,37 @@ class TEXT_TEXTURE_Properties(PropertyGroup): name="Preview Image" ) + # Collapsible section expand/collapse states + expand_image_overlays: BoolProperty( + name="Image Overlays", + description="Show/hide Image Overlays section", + default=False + ) + + expand_positioning: BoolProperty( + name="Positioning & Layout", + description="Show/hide Positioning & Layout section", + default=False + ) + + expand_font_settings: BoolProperty( + name="Font Settings", + description="Show/hide Font Settings section", + default=False + ) + + expand_preview_options: BoolProperty( + name="Preview Options", + description="Show/hide Preview Options section", + default=False + ) + + expand_presets: BoolProperty( + name="Presets", + description="Show/hide Presets section", + default=False + ) + text: StringProperty( name="Text", description="Text to render as texture", @@ -453,7 +869,7 @@ class TEXT_TEXTURE_Properties(PropertyGroup): ) texture_height: IntProperty( - name="Height", + name="Height", description="Texture height in pixels", default=1024, min=64, @@ -542,14 +958,181 @@ class TEXT_TEXTURE_Properties(PropertyGroup): update=update_live_preview ) + # ============================================================================ + # ENHANCED POSITIONING & ALIGNMENT PROPERTIES + # ============================================================================ + + # 9-point anchor system + anchor_point: EnumProperty( + name="Anchor Point", + description="Text anchor position on canvas", + items=[ + ('TOP_LEFT', 'Top Left', 'Position text from top-left corner'), + ('TOP_CENTER', 'Top Center', 'Position text from top center'), + ('TOP_RIGHT', 'Top Right', 'Position text from top-right corner'), + ('MIDDLE_LEFT', 'Middle Left', 'Position text from middle-left'), + ('MIDDLE_CENTER', 'Middle Center', 'Position text from center (default)'), + ('MIDDLE_RIGHT', 'Middle Right', 'Position text from middle-right'), + ('BOTTOM_LEFT', 'Bottom Left', 'Position text from bottom-left corner'), + ('BOTTOM_CENTER', 'Bottom Center', 'Position text from bottom center'), + ('BOTTOM_RIGHT', 'Bottom Right', 'Position text from bottom-right corner'), + ], + default='MIDDLE_CENTER', + update=update_live_preview + ) + + # Position mode toggle + position_mode: EnumProperty( + name="Position Mode", + description="Choose between preset anchor positions or manual positioning", + items=[ + ('PRESET', 'Preset', 'Use anchor points with margins and offsets'), + ('MANUAL', 'Manual', 'Manually specify X,Y coordinates'), + ], + default='PRESET', + update=update_live_preview + ) + + # Percentage-based margins (0-50%) + margin_x: FloatProperty( + name="X Margin", + description="Horizontal margin from edge as percentage (0-50%)", + subtype='PERCENTAGE', + default=0.0, + min=0.0, + max=50.0, + precision=1, + update=update_live_preview + ) + + margin_y: FloatProperty( + name="Y Margin", + description="Vertical margin from edge as percentage (0-50%)", + subtype='PERCENTAGE', + default=0.0, + min=0.0, + max=50.0, + precision=1, + update=update_live_preview + ) + + # Link margins option + margins_linked: BoolProperty( + name="Link Margins", + description="Keep X and Y margins synchronized", + default=False, + update=update_live_preview + ) + + # Pixel offset fine-tuning (-200 to +200px) + offset_x: IntProperty( + name="X Offset", + description="Fine-tune X position in pixels (-200 to +200)", + default=0, + min=-200, + max=200, + update=update_live_preview + ) + + offset_y: IntProperty( + name="Y Offset", + description="Fine-tune Y position in pixels (-200 to +200)", + default=0, + min=-200, + max=200, + update=update_live_preview + ) + + # Manual positioning properties + manual_position_x: FloatProperty( + name="Manual X", + description="Manual X position as percentage (0-100%)", + subtype='PERCENTAGE', + default=50.0, + min=0.0, + max=100.0, + precision=1, + update=update_live_preview + ) + + manual_position_y: FloatProperty( + name="Manual Y", + description="Manual Y position as percentage (0-100%)", + subtype='PERCENTAGE', + default=50.0, + min=0.0, + max=100.0, + precision=1, + update=update_live_preview + ) + + manual_position_unit: EnumProperty( + name="Position Unit", + description="Units for manual positioning", + items=[ + ('PERCENTAGE', 'Percentage', 'Position as percentage of canvas'), + ('PIXELS', 'Pixels', 'Position in absolute pixels'), + ], + default='PERCENTAGE', + update=update_live_preview + ) + + # Canvas constraint option + constrain_to_canvas: BoolProperty( + name="Constrain to Canvas", + description="Keep text within canvas bounds", + default=True, + update=update_live_preview + ) + + # Visual feedback options + show_position_guides: BoolProperty( + name="Show Position Guides", + description="Display positioning guide lines in preview", + default=False, + update=update_live_preview + ) + + show_text_bounds: BoolProperty( + name="Show Text Bounds", + description="Display text bounding box in preview", + default=False, + update=update_live_preview + ) + + show_canvas_grid: BoolProperty( + name="Show Canvas Grid", + description="Display alignment grid overlay in preview", + default=False, + update=update_live_preview + ) + + grid_density: IntProperty( + name="Grid Density", + description="Number of grid divisions (4-16)", + default=8, + min=4, + max=16, + update=update_live_preview + ) + # Removed live_preview_enabled - preview is always on preview_size: IntProperty( name="Preview Size", description="Size of the preview image (does not affect final output)", - default=256, + default=512, min=128, - max=512, + max=2048, + update=update_live_preview + ) + + preview_zoom_level: FloatProperty( + name="Zoom", + description="Zoom level for preview", + default=1.0, + min=0.5, + max=4.0, update=update_live_preview ) @@ -588,14 +1171,26 @@ class TEXT_TEXTURE_Properties(PropertyGroup): update=update_live_preview ) + # Image overlays + image_overlays: CollectionProperty(type=TEXT_TEXTURE_ImageOverlay) + active_overlay_index: IntProperty(default=0) + + # Presets presets: CollectionProperty(type=TEXT_TEXTURE_Preset) active_preset_index: IntProperty(default=0) new_preset_name: StringProperty( name="Preset Name", - description="Name for new preset", + description="Name for new preset (Press Enter to save)", default="New Preset" ) + + # Property to track when user wants to save with Enter + preset_save_requested: BoolProperty( + name="Save Requested", + description="Internal flag for Enter key save requests", + default=False + ) # ============================================================================ # OPERATORS @@ -687,7 +1282,141 @@ class TEXT_TEXTURE_OT_refresh_preview(Operator): return {'FINISHED'} -# Removed view_preview operator - we want inline display only +class TEXT_TEXTURE_OT_view_preview_zoom(Operator): + """Open preview in new Image Editor window with zoom support""" + bl_idname = "text_texture.view_preview_zoom" + bl_label = "Enable Zoom" + bl_description = "Open preview in new Image Editor window with zoom and pan controls" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + props = context.scene.text_texture_props + + # Generate preview image when button is clicked + print("[TTG] Generating preview on button click...") + result = generate_preview(context) + + if not result: + self.report({'ERROR'}, "Failed to generate preview") + return {'CANCELLED'} + + if not props.preview_image: + self.report({'ERROR'}, "No preview image to display") + return {'CANCELLED'} + + # Open a new window with Image Editor + print(f"[ZOOM] Opening new window for preview image: {props.preview_image.name}") + + try: + # Create a new window + bpy.ops.screen.userpref_show('INVOKE_DEFAULT') + + # Wait a moment and then change the newly opened window to Image Editor + def setup_image_window(): + # Find the newest window (should be the one we just opened) + newest_window = None + for window in bpy.context.window_manager.windows: + if window.screen.name.startswith("temp"): + newest_window = window + break + + if not newest_window: + # Fallback: use any non-main window + for window in bpy.context.window_manager.windows: + if window != bpy.context.window: + newest_window = window + break + + if newest_window: + # Set the area type to Image Editor + for area in newest_window.screen.areas: + if area.type == 'PREFERENCES': + area.type = 'IMAGE_EDITOR' + print("[ZOOM] Changed area to Image Editor") + + # Set the preview image + for space in area.spaces: + if space.type == 'IMAGE_EDITOR': + space.image = props.preview_image + print(f"[ZOOM] Set preview image: {props.preview_image.name}") + + # Set up proper viewing + override = { + 'window': newest_window, + 'screen': newest_window.screen, + 'area': area, + 'region': area.regions[0] if area.regions else None + } + + with bpy.context.temp_override(**override): + try: + bpy.ops.image.view_all(fit_view=True) + print("[ZOOM] Applied fit to view") + except Exception as e: + print(f"[ZOOM] Error applying fit to view: {e}") + + break + break + + self.report({'INFO'}, "āœ… NEW WINDOW OPENED! Use mouse wheel to zoom, middle-mouse to pan") + print("[ZOOM] New window zoom feature activated successfully") + else: + print("[ZOOM] Could not find new window") + self.report({'ERROR'}, "Failed to setup new window") + + return None # Stop timer + + # Use a timer to set up the window after it opens + bpy.app.timers.register(setup_image_window, first_interval=0.1) + + except Exception as e: + print(f"[ZOOM] Error creating new window: {e}") + # Fallback to old behavior if new window creation fails + self.report({'WARNING'}, "New window failed, trying area split instead") + + # Find any area to split + for area in context.screen.areas: + if area.type in ['NODE_EDITOR', 'VIEW_3D', 'PROPERTIES']: + print(f"[ZOOM] Fallback: Splitting {area.type} to create Image Editor") + + # Split horizontally to create a new area + with context.temp_override(area=area): + bpy.ops.screen.area_split(direction='HORIZONTAL', factor=0.6) + + # Find the newly created area + for new_area in context.screen.areas: + if new_area != area and new_area.y != area.y: + new_area.type = 'IMAGE_EDITOR' + + # Set the image in the Image Editor + for space in new_area.spaces: + if space.type == 'IMAGE_EDITOR': + space.image = props.preview_image + print("[ZOOM] Fallback: Image set in split Image Editor") + break + + # Tag for redraw and fit to view + new_area.tag_redraw() + override = context.copy() + override['area'] = new_area + override['region'] = new_area.regions[0] + + with context.temp_override(**override): + try: + bpy.ops.image.view_all(fit_view=True) + print("[ZOOM] Fallback: Fit to view applied") + except Exception as e: + print(f"[ZOOM] Fallback: Error applying fit to view: {e}") + + self.report({'INFO'}, "āœ… ZOOM ENABLED! Use mouse wheel to zoom, middle-mouse to pan") + return {'FINISHED'} + break + + self.report({'ERROR'}, "Failed to create Image Editor") + return {'CANCELLED'} + + return {'FINISHED'} + class TEXT_TEXTURE_OT_save_preset(Operator): """Save current settings as preset""" @@ -696,11 +1425,39 @@ class TEXT_TEXTURE_OT_save_preset(Operator): bl_description = "Save current settings as preset" bl_options = {'REGISTER', 'UNDO'} + overwrite: bpy.props.BoolProperty( + name="Overwrite", + description="Overwrite existing preset", + default=False + ) + def execute(self, context): props = context.scene.text_texture_props - if not props.new_preset_name.strip(): - self.report({'ERROR'}, "Enter a preset name") + # Validate preset name + preset_name = props.new_preset_name.strip() + if not preset_name: + self.report({'ERROR'}, "Please enter a preset name") + return {'CANCELLED'} + + # Validate preset name characters (avoid filesystem issues) + import re + if not re.match(r'^[a-zA-Z0-9_\-\s]+$', preset_name): + self.report({'ERROR'}, "Preset name can only contain letters, numbers, spaces, hyphens, and underscores") + return {'CANCELLED'} + + # Check for duplicate names + preset_file = os.path.join(get_preset_path(), f"{preset_name}.json") + if os.path.exists(preset_file) and not self.overwrite: + # Suggest alternative names + base_name = preset_name + counter = 1 + suggested_name = f"{base_name}_{counter}" + while os.path.exists(os.path.join(get_preset_path(), f"{suggested_name}.json")): + counter += 1 + suggested_name = f"{base_name}_{counter}" + + self.report({'ERROR'}, f"Preset '{preset_name}' already exists. Suggestion: try '{suggested_name}' or delete the existing preset first.") return {'CANCELLED'} preset_data = { @@ -716,24 +1473,84 @@ class TEXT_TEXTURE_OT_save_preset(Operator): 'custom_font_path': props.custom_font_path, 'use_custom_font': props.use_custom_font, 'text_align': props.text_align, + 'image_overlays': [] } + # Save overlay data + for overlay in props.image_overlays: + overlay_data = { + 'name': overlay.name, + 'image_path': overlay.image_path, + 'x_position': overlay.x_position, + 'y_position': overlay.y_position, + 'scale': overlay.scale, + 'rotation': overlay.rotation, + 'z_index': overlay.z_index, + 'enabled': overlay.enabled + } + preset_data['image_overlays'].append(overlay_data) + try: - preset_file = os.path.join(get_preset_path(), f"{props.new_preset_name}.json") with open(preset_file, 'w') as f: json.dump(preset_data, f, indent=2) - preset = props.presets.add() - preset.name = props.new_preset_name + # Check if preset already exists in the collection and update it + existing_preset = None + for preset in props.presets: + if preset.name == preset_name: + existing_preset = preset + break - self.report({'INFO'}, f"Saved: {props.new_preset_name}") + if not existing_preset: + preset = props.presets.add() + preset.name = preset_name + + # Clear the input field props.new_preset_name = "New Preset" + # Provide clear feedback + if self.overwrite: + self.report({'INFO'}, f"āœ… Preset '{preset_name}' updated successfully") + else: + self.report({'INFO'}, f"āœ… Preset '{preset_name}' saved successfully") + + except (OSError, IOError, json.JSONEncodeError) as e: + self.report({'ERROR'}, f"Failed to save preset: {str(e)}") + return {'CANCELLED'} except Exception as e: - self.report({'ERROR'}, f"Failed: {str(e)}") + self.report({'ERROR'}, f"Unexpected error saving preset: {str(e)}") return {'CANCELLED'} return {'FINISHED'} + + def invoke(self, context, event): + # Check if preset exists and prompt for overwrite + props = context.scene.text_texture_props + preset_name = props.new_preset_name.strip() + + if preset_name and preset_name != "New Preset": + preset_file = os.path.join(get_preset_path(), f"{preset_name}.json") + if os.path.exists(preset_file): + return context.window_manager.invoke_confirm(self, event) + + return self.execute(context) + + def draw(self, context): + layout = self.layout + props = context.scene.text_texture_props + preset_name = props.new_preset_name.strip() + layout.label(text=f"Overwrite existing preset '{preset_name}'?") + +class TEXT_TEXTURE_OT_save_preset_enter(Operator): + """Save preset when Enter key is pressed in name field""" + bl_idname = "text_texture.save_preset_enter" + bl_label = "Save Preset (Enter)" + bl_description = "Save current settings as preset when Enter is pressed" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + # Delegate to the main save preset operator + return bpy.ops.text_texture.save_preset('INVOKE_DEFAULT') class TEXT_TEXTURE_OT_load_preset(Operator): """Load preset""" @@ -749,6 +1566,10 @@ class TEXT_TEXTURE_OT_load_preset(Operator): 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}") + return {'CANCELLED'} + with open(preset_file, 'r') as f: preset_data = json.load(f) @@ -767,15 +1588,37 @@ class TEXT_TEXTURE_OT_load_preset(Operator): props.use_custom_font = preset_data.get('use_custom_font', props.use_custom_font) props.text_align = preset_data.get('text_align', props.text_align) + # Load overlay data + props.image_overlays.clear() + overlay_data_list = preset_data.get('image_overlays', []) + for overlay_data in overlay_data_list: + overlay = props.image_overlays.add() + overlay.name = overlay_data.get('name', 'Overlay') + overlay.image_path = overlay_data.get('image_path', '') + overlay.x_position = overlay_data.get('x_position', 0.5) + overlay.y_position = overlay_data.get('y_position', 0.5) + overlay.scale = overlay_data.get('scale', 1.0) + overlay.rotation = overlay_data.get('rotation', 0.0) + overlay.z_index = overlay_data.get('z_index', 1) + overlay.enabled = overlay_data.get('enabled', True) + props.is_updating = False # Always generate preview after loading preset generate_preview(context) self.report({'INFO'}, f"Loaded: {self.preset_name}") + except (OSError, IOError) as e: + props.is_updating = False + self.report({'ERROR'}, f"Failed to read preset file: {str(e)}") + return {'CANCELLED'} + except json.JSONDecodeError as e: + props.is_updating = False + self.report({'ERROR'}, f"Invalid preset file format: {str(e)}") + return {'CANCELLED'} except Exception as e: props.is_updating = False - self.report({'ERROR'}, f"Failed: {str(e)}") + self.report({'ERROR'}, f"Unexpected error loading preset: {str(e)}") return {'CANCELLED'} return {'FINISHED'} @@ -796,19 +1639,33 @@ class TEXT_TEXTURE_OT_delete_preset(Operator): preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json") if os.path.exists(preset_file): os.remove(preset_file) + else: + self.report({'WARNING'}, f"Preset file not found: {self.preset_name}") for i, preset in enumerate(props.presets): if preset.name == self.preset_name: props.presets.remove(i) break - self.report({'INFO'}, f"Deleted: {self.preset_name}") + self.report({'INFO'}, f"šŸ—‘ļø Deleted preset: {self.preset_name}") + except (OSError, IOError) as e: + self.report({'ERROR'}, f"Failed to delete preset file: {str(e)}") + return {'CANCELLED'} except Exception as e: - self.report({'ERROR'}, f"Failed: {str(e)}") + self.report({'ERROR'}, f"Unexpected error deleting preset: {str(e)}") return {'CANCELLED'} return {'FINISHED'} + + def invoke(self, context, event): + # Add confirmation dialog for better UX + return context.window_manager.invoke_confirm(self, event) + + def draw(self, context): + layout = self.layout + layout.label(text=f"Delete preset '{self.preset_name}'?") + layout.label(text="This action cannot be undone.", icon='ERROR') class TEXT_TEXTURE_OT_open_panel(Operator): """Open Text Texture Generator panel""" @@ -853,6 +1710,272 @@ 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_add_overlay(Operator): + """Add new image overlay""" + bl_idname = "text_texture.add_overlay" + bl_label = "Add Overlay" + bl_description = "Add new image overlay" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + props = context.scene.text_texture_props + + overlay = props.image_overlays.add() + overlay.name = f"Overlay {len(props.image_overlays)}" + props.active_overlay_index = len(props.image_overlays) - 1 + + # Force UI redraw + for area in context.screen.areas: + area.tag_redraw() + + self.report({'INFO'}, f"Added overlay: {overlay.name}") + return {'FINISHED'} + +class TEXT_TEXTURE_OT_remove_overlay(Operator): + """Remove active image overlay""" + bl_idname = "text_texture.remove_overlay" + bl_label = "Remove Overlay" + bl_description = "Remove active image overlay" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + props = context.scene.text_texture_props + + if props.image_overlays and props.active_overlay_index < len(props.image_overlays): + overlay_name = props.image_overlays[props.active_overlay_index].name + props.image_overlays.remove(props.active_overlay_index) + + # Update active index + if props.active_overlay_index >= len(props.image_overlays) and props.image_overlays: + props.active_overlay_index = len(props.image_overlays) - 1 + + # Force UI redraw + for area in context.screen.areas: + area.tag_redraw() + + self.report({'INFO'}, f"Removed overlay: {overlay_name}") + else: + self.report({'WARNING'}, "No overlay to remove") + + return {'FINISHED'} + +class TEXT_TEXTURE_OT_set_anchor_point(Operator): + """Set anchor point for text positioning""" + bl_idname = "text_texture.set_anchor_point" + bl_label = "Set Anchor Point" + bl_description = "Set text anchor position" + bl_options = {'REGISTER', 'UNDO'} + + anchor_point: StringProperty() + + def execute(self, context): + props = context.scene.text_texture_props + props.anchor_point = self.anchor_point + return {'FINISHED'} + +class TEXT_TEXTURE_OT_sync_margins(Operator): + """Sync margin values when linked""" + bl_idname = "text_texture.sync_margins" + bl_label = "Sync Margins" + bl_description = "Synchronize margin values" + bl_options = {'REGISTER', 'UNDO'} + + margin_type: StringProperty() + + def execute(self, context): + props = context.scene.text_texture_props + sync_margin_values(props, self.margin_type) + return {'FINISHED'} + + +# ============================================================================ +# SHARED UI COMPONENTS +# ============================================================================ + +def draw_collapsible_header(layout, prop_name, text, icon='TRIA_DOWN'): + """Draw a collapsible section header with expand/collapse arrow""" + props = bpy.context.scene.text_texture_props + expanded = getattr(props, prop_name) + + box = layout.box() + header_row = box.row() + + # Draw arrow icon and toggle expanded state + arrow_icon = 'TRIA_DOWN' if expanded else 'TRIA_RIGHT' + header_row.prop(props, prop_name, text="", icon=arrow_icon, emboss=False) + header_row.label(text=text, icon=icon) + + return box if expanded else None + +def draw_text_input_section(layout, props): + """Draw the main text input field""" + layout.prop(props, "text", text="") + +def draw_font_dropdown_section(layout, props): + """Draw basic font dropdown for top level""" + row = layout.row(align=True) + row.prop(props, "font_path", text="") + row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH') + +def draw_generate_button(layout): + """Draw the main generate button""" + row = layout.row() + row.scale_y = 1.5 + row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA') + +def draw_image_overlays_section(layout, props): + """Draw image overlays controls""" + # Add/Remove buttons + row = layout.row(align=True) + row.operator("text_texture.add_overlay", text="Add Image Overlay", icon='ADD') + if props.image_overlays: + row.operator("text_texture.remove_overlay", text="Remove", icon='REMOVE') + + # Overlay list + if props.image_overlays: + layout.separator() + for i, overlay in enumerate(props.image_overlays): + overlay_row = layout.row() + overlay_row.prop(overlay, "enabled", text="") + overlay_row.prop(overlay, "name", text="") + + if overlay.enabled: + layout.prop(overlay, "image_path", text="Image File") + + pos_row = layout.row(align=True) + pos_row.prop(overlay, "x_position", text="X") + pos_row.prop(overlay, "y_position", text="Y") + + trans_row = layout.row(align=True) + trans_row.prop(overlay, "scale", text="Scale") + trans_row.prop(overlay, "rotation", text="Rotation") + trans_row.prop(overlay, "z_index", text="Z-Level") + +def draw_positioning_section(layout, props): + """Draw positioning and alignment controls""" + # Dimensions moved here from top level + layout.label(text="Dimensions:", icon='SETTINGS') + row = layout.row(align=True) + row.prop(props, "texture_width", text="Width") + row.prop(props, "texture_height", text="Height") + + layout.separator() + layout.prop(props, "position_mode", text="Mode") + + if props.position_mode == 'PRESET': + layout.separator() + layout.label(text="Anchor Point:", icon='ANCHOR_TOP') + + anchor_matrix = get_anchor_point_matrix() + for row_idx, row in enumerate(anchor_matrix): + grid_row = layout.row(align=True) + grid_row.scale_y = 1.2 + for col_idx, anchor in enumerate(row): + if anchor == props.anchor_point: + op = grid_row.operator("text_texture.set_anchor_point", text="ā—", depress=True) + else: + op = grid_row.operator("text_texture.set_anchor_point", text="ā—‹") + op.anchor_point = anchor + + # Margins + layout.separator() + margin_row = layout.row(align=True) + margin_row.label(text="Margins:") + margin_row.prop(props, "margins_linked", text="", icon='LINKED' if props.margins_linked else 'UNLINKED', toggle=True) + + margins_row = layout.row(align=True) + margins_row.prop(props, "margin_x", text="X") + if not props.margins_linked: + margins_row.prop(props, "margin_y", text="Y") + + # Offsets + layout.separator() + layout.label(text="Fine-tune (pixels):") + offset_row = layout.row(align=True) + offset_row.prop(props, "offset_x", text="X") + offset_row.prop(props, "offset_y", text="Y") + + else: # MANUAL mode + layout.separator() + layout.prop(props, "manual_position_unit", text="Unit") + manual_row = layout.row(align=True) + manual_row.prop(props, "manual_position_x", text="X") + manual_row.prop(props, "manual_position_y", text="Y") + + layout.separator() + layout.prop(props, "constrain_to_canvas") + +def draw_font_settings_section(layout, props): + """Draw extended font settings""" + # Font size moved here from top level + layout.prop(props, "font_size") + + layout.separator() + layout.label(text="Font Selection:") + # Default font dropdown moved here from top level + row = layout.row(align=True) + row.prop(props, "font_path", text="") + row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH') + + layout.separator() + # Custom font controls moved here from top level + layout.prop(props, "use_custom_font") + if props.use_custom_font: + layout.prop(props, "custom_font_path", text="") + + layout.separator() + row = layout.row(align=True) + row.prop(props, "padding") + row.prop(props, "text_align") + + layout.separator() + layout.label(text="Colors:") + layout.prop(props, "text_color") + layout.prop(props, "background_transparent") + if not props.background_transparent: + layout.prop(props, "background_color") + +def draw_preview_options_section(layout, props): + """Draw preview options""" + layout.prop(props, "preview_size", text="Preview Size") + layout.prop(props, "preview_bg_type", text="Background") + if props.preview_bg_type == 'color': + layout.prop(props, "preview_bg_color", text="Background Color") + elif props.preview_bg_type == 'gradient': + layout.prop(props, "preview_bg_color", text="Color 1") + layout.prop(props, "preview_bg_color2", text="Color 2") + +def draw_presets_section(layout, props): + """Draw presets save/load controls""" + # Save preset section + save_row = layout.row(align=True) + save_row.prop(props, "new_preset_name", text="", placeholder="Preset Name") + save_row.operator("text_texture.save_preset", text="Save", icon='FILE_NEW') + + # 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() + + # Load presets section + 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 + else: + layout.label(text="šŸ“‚ No presets saved yet", icon='INFO') + layout.label(text="šŸ’” Save your first preset above!", icon='LIGHT_DATA') + # ============================================================================ # MENUS # ============================================================================ @@ -887,128 +2010,78 @@ class TEXT_TEXTURE_PT_panel(Panel): @classmethod def poll(cls, context): - return context.space_data.type == 'NODE_EDITOR' and context.space_data.tree_type == 'ShaderNodeTree' + # DEBUG: Log poll method calls + print(f"[TTG DEBUG] Panel poll called - space_data: {context.space_data}") + print(f"[TTG DEBUG] space_data.type: {context.space_data.type if context.space_data else 'None'}") + if hasattr(context.space_data, 'tree_type'): + print(f"[TTG DEBUG] tree_type: {context.space_data.tree_type}") + else: + print("[TTG DEBUG] No tree_type attribute") + + # More permissive poll condition to ensure panel shows up + if context.space_data.type == 'NODE_EDITOR': + if hasattr(context.space_data, 'tree_type'): + result = context.space_data.tree_type == 'ShaderNodeTree' + print(f"[TTG DEBUG] Poll result: {result}") + return result + else: + # Allow panel even without tree_type (sometimes happens) + print("[TTG DEBUG] Poll result: True (no tree_type but NODE_EDITOR)") + return True + + print("[TTG DEBUG] Poll result: False (not NODE_EDITOR)") + return False def draw(self, context): layout = self.layout props = context.scene.text_texture_props - # Always ensure preview exists and is up to date - # Generate initial preview on first draw - if not props.preview_image: - # Schedule preview generation using timer - def generate_preview_delayed(): - generate_preview(context) - return None # Stop timer - bpy.app.timers.register(generate_preview_delayed, first_interval=0.1) + # TOP LEVEL - Always Visible Controls - # LIVE PREVIEW - Simplified and clean - if props.preview_image: - box = layout.box() - box.template_ID_preview(props, "preview_image", rows=8, cols=8) - else: - box = layout.box() - box.label(text="Generating preview...", icon='TIME') - - # TEXT INPUT - BIG + # Text Input box = layout.box() - box.label(text="Text Input:", icon='TEXT') - row = box.row() - row.scale_y = 2.0 - row.prop(props, "text", text="") + box.label(text="Text:", icon='TEXT') + draw_text_input_section(box, props) - # SETTINGS - box = layout.box() - box.label(text="Texture Settings:", icon='SETTINGS') - # Dimensions - col = box.column() - row = col.row(align=True) - row.prop(props, "texture_width", text="Width") - row.prop(props, "texture_height", text="Height") - col.label(text=f"Output: {props.texture_width}Ɨ{props.texture_height}px", icon='INFO') - - # Font Settings - col.separator() - col.label(text="Font:", icon='FONT_DATA') - col.prop(props, "use_custom_font") - if props.use_custom_font: - col.prop(props, "custom_font_path", text="") - else: - row = col.row(align=True) - row.prop(props, "font_path", text="") - row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH') - - row = col.row(align=True) - row.prop(props, "font_size") - row.prop(props, "padding") - col.prop(props, "text_align") - - # Colors - col.separator() - col.label(text="Colors:", icon='COLOR') - col.prop(props, "text_color") - col.prop(props, "background_transparent") - if not props.background_transparent: - col.prop(props, "background_color") - - # Preview settings (compact) - col.separator() - row = col.row(align=True) - row.prop(props, "preview_size", text="Preview") - row.prop(props, "preview_bg_type", text="") - if props.preview_bg_type == 'color': - col.prop(props, "preview_bg_color", text="") - elif props.preview_bg_type == 'gradient': - row = col.row(align=True) - row.prop(props, "preview_bg_color", text="") - row.prop(props, "preview_bg_color2", text="") - - # GENERATE BUTTON + # Generate Button layout.separator() - row = layout.row() - row.scale_y = 2.0 - row.operator("text_texture.generate", text="GENERATE FINAL TEXTURE", icon='IMAGE_DATA') - -class TEXT_TEXTURE_PT_presets(Panel): - """Presets Panel""" - bl_label = "Presets" - bl_idname = "TEXT_TEXTURE_PT_presets" - bl_parent_id = "TEXT_TEXTURE_PT_panel" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - props = context.scene.text_texture_props + draw_generate_button(layout) - # Load presets - if not props.presets: - preset_dir = get_preset_path() - if os.path.exists(preset_dir): - for filename in os.listdir(preset_dir): - if filename.endswith('.json'): - preset_name = os.path.splitext(filename)[0] - if preset_name not in [p.name for p in props.presets]: - preset = props.presets.add() - preset.name = preset_name + layout.separator() - # Save preset - box = layout.box() - box.label(text="Save Current Settings:", icon='EXPORT') - row = box.row(align=True) - row.prop(props, "new_preset_name", text="") - row.operator("text_texture.save_preset", text="Save", icon='ADD') + # COLLAPSIBLE SECTIONS - # Load preset - if props.presets: - box = layout.box() - box.label(text="Load Preset:", icon='IMPORT') - for preset in props.presets: - row = box.row(align=True) - row.operator("text_texture.load_preset", text=preset.name).preset_name = preset.name - row.operator("text_texture.delete_preset", text="", icon='X').preset_name = preset.name + # Image Overlays Section (collapsed by default) + overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') + if overlay_section: + draw_image_overlays_section(overlay_section, props) + + # Positioning & Layout Section (collapsed by default) + position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER') + if position_section: + draw_positioning_section(position_section, props) + + # Font Settings Section (collapsed by default) + font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA') + if font_section: + draw_font_settings_section(font_section, props) + + # Preview Options Section (collapsed by default) + preview_section = draw_collapsible_header(layout, "expand_preview_options", "Preview Options", 'IMAGE_DATA') + if preview_section: + draw_preview_options_section(preview_section, props) + + # Preview Button in expanded section + layout.separator() + preview_section.separator() + button_row = preview_section.row() + button_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + + # Presets Section (collapsed by default) + presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET') + if presets_section: + draw_presets_section(presets_section, props) class TEXT_TEXTURE_PT_panel_3d(Panel): """3D View Panel - Access from sidebar (N key)""" @@ -1023,46 +2096,51 @@ class TEXT_TEXTURE_PT_panel_3d(Panel): layout = self.layout props = context.scene.text_texture_props - # Generate initial preview if needed - if not props.preview_image: - # Schedule preview generation using timer - def generate_preview_delayed(): - generate_preview(context) - return None # Stop timer - bpy.app.timers.register(generate_preview_delayed, first_interval=0.1) + # TOP LEVEL - Always Visible Controls - # LIVE PREVIEW - Simplified - if props.preview_image: - box = layout.box() - box.template_ID_preview(props, "preview_image", rows=6, cols=6) - else: - box = layout.box() - box.label(text="Generating preview...", icon='TIME') - - # TEXT INPUT + # Text Input box = layout.box() box.label(text="Text:", icon='TEXT') - row = box.row() - row.scale_y = 1.5 - row.prop(props, "text", text="") + draw_text_input_section(box, props) - # Quick Settings - col = layout.column() - row = col.row(align=True) - row.prop(props, "texture_width", text="W") - row.prop(props, "texture_height", text="H") - col.prop(props, "font_size") - col.prop(props, "text_color") - col.prop(props, "background_transparent") - # Preview Toggle - # Preview is always enabled - removed toggle - - # Generate + # Generate Button layout.separator() - row = layout.row() - row.scale_y = 1.5 - row.operator("text_texture.generate", text="Generate Texture", icon='IMAGE_DATA') + draw_generate_button(layout) + + layout.separator() + + # COLLAPSIBLE SECTIONS + + # Image Overlays Section (collapsed by default) + overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') + if overlay_section: + draw_image_overlays_section(overlay_section, props) + + # Positioning & Layout Section (collapsed by default) + position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER') + if position_section: + draw_positioning_section(position_section, props) + + # Font Settings Section (collapsed by default) + font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA') + if font_section: + draw_font_settings_section(font_section, props) + + # Preview Options Section (collapsed by default) + preview_section = draw_collapsible_header(layout, "expand_preview_options", "Preview Options", 'IMAGE_DATA') + if preview_section: + draw_preview_options_section(preview_section, props) + + # Preview Button in expanded section + preview_section.separator() + button_row = preview_section.row() + button_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + + # Presets Section (collapsed by default) + presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET') + if presets_section: + draw_presets_section(presets_section, props) class TEXT_TEXTURE_PT_panel_properties(Panel): """Properties Panel""" @@ -1077,71 +2155,102 @@ class TEXT_TEXTURE_PT_panel_properties(Panel): layout = self.layout props = context.scene.text_texture_props - # Generate initial preview if needed - if not props.preview_image: - # Schedule preview generation using timer - def generate_preview_delayed(): - generate_preview(context) - return None # Stop timer - bpy.app.timers.register(generate_preview_delayed, first_interval=0.1) + # TOP LEVEL - Always Visible Controls - # LIVE PREVIEW - Simplified - if props.preview_image: - box = layout.box() - box.template_ID_preview(props, "preview_image", rows=6, cols=6) - else: - box = layout.box() - box.label(text="Generating preview...", icon='TIME') - - # TEXT INPUT + # Text Input box = layout.box() box.label(text="Text:", icon='TEXT') - row = box.row() - row.scale_y = 1.5 - row.prop(props, "text", text="") + draw_text_input_section(box, props) - # Quick Settings - col = layout.column() - row = col.row(align=True) - row.prop(props, "texture_width", text="W") - row.prop(props, "texture_height", text="H") - col.prop(props, "font_size") - col.prop(props, "text_color") - col.prop(props, "background_transparent") - if not props.background_transparent: - col.prop(props, "background_color") - # Generate + # Generate Button layout.separator() - row = layout.row() - row.scale_y = 1.5 - row.operator("text_texture.generate", text="Generate Texture", icon='IMAGE_DATA') + draw_generate_button(layout) + + layout.separator() + + # COLLAPSIBLE SECTIONS + + # Image Overlays Section (collapsed by default) + overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA') + if overlay_section: + draw_image_overlays_section(overlay_section, props) + + # Positioning & Layout Section (collapsed by default) + position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER') + if position_section: + draw_positioning_section(position_section, props) + + # Font Settings Section (collapsed by default) + font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA') + if font_section: + draw_font_settings_section(font_section, props) + + # Preview Options Section (collapsed by default) + preview_section = draw_collapsible_header(layout, "expand_preview_options", "Preview Options", 'IMAGE_DATA') + if preview_section: + draw_preview_options_section(preview_section, props) + + # Preview Button in expanded section + preview_section.separator() + button_row = preview_section.row() + button_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW') + + # Presets Section (collapsed by default) + presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET') + if presets_section: + draw_presets_section(presets_section, props) # ============================================================================ # REGISTRATION # ============================================================================ classes = ( + TEXT_TEXTURE_ImageOverlay, TEXT_TEXTURE_Preset, TEXT_TEXTURE_Properties, TEXT_TEXTURE_OT_generate, TEXT_TEXTURE_OT_refresh_preview, + TEXT_TEXTURE_OT_view_preview_zoom, + TEXT_TEXTURE_OT_add_overlay, + TEXT_TEXTURE_OT_remove_overlay, TEXT_TEXTURE_OT_open_panel, TEXT_TEXTURE_OT_save_preset, + TEXT_TEXTURE_OT_save_preset_enter, + TEXT_TEXTURE_OT_set_anchor_point, + TEXT_TEXTURE_OT_sync_margins, TEXT_TEXTURE_OT_load_preset, TEXT_TEXTURE_OT_delete_preset, TEXT_TEXTURE_OT_refresh_fonts, TEXT_TEXTURE_PT_panel, - TEXT_TEXTURE_PT_presets, TEXT_TEXTURE_PT_panel_3d, TEXT_TEXTURE_PT_panel_properties, ) def register(): + print("[TTG DEBUG] ========== ADDON REGISTRATION START ==========") for cls in classes: - bpy.utils.register_class(cls) + print(f"[TTG DEBUG] Registering class: {cls.__name__}") + try: + bpy.utils.register_class(cls) + print(f"[TTG DEBUG] Successfully registered: {cls.__name__}") + except Exception as e: + print(f"[TTG DEBUG] FAILED to register {cls.__name__}: {e}") + print("[TTG DEBUG] Registering scene properties...") bpy.types.Scene.text_texture_props = PointerProperty(type=TEXT_TEXTURE_Properties) + print("[TTG DEBUG] Scene properties registered successfully") + + # CRITICAL FIX: Force UI refresh after registration + print("[TTG DEBUG] Forcing UI refresh...") + try: + # Update all areas to ensure panels are properly displayed + for window in bpy.context.window_manager.windows: + for area in window.screen.areas: + area.tag_redraw() + print("[TTG DEBUG] UI refresh completed successfully") + except Exception as e: + print(f"[TTG DEBUG] UI refresh failed: {e}") # Add to menus bpy.types.NODE_MT_add.append(menu_func_node_editor) @@ -1152,6 +2261,16 @@ def register(): # Add to Image menu if available if hasattr(bpy.types, 'IMAGE_MT_image'): bpy.types.IMAGE_MT_image.append(menu_func_image) + + # Initialize presets after registration to prevent race conditions + # Use a timer to ensure the scene is ready + def delayed_preset_init(): + initialize_presets() + return None # Stop timer + + print("[TTG DEBUG] Registration completed - scheduling preset initialization") + bpy.app.timers.register(delayed_preset_init, first_interval=0.1) + print("[TTG DEBUG] ========== ADDON REGISTRATION END ==========") def unregister(): # Clean up preview diff --git a/test_positioning_system.py b/test_positioning_system.py new file mode 100644 index 0000000..eee2d5d --- /dev/null +++ b/test_positioning_system.py @@ -0,0 +1,558 @@ +#!/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_ui_and_compatibility.py b/test_ui_and_compatibility.py new file mode 100644 index 0000000..69c949d --- /dev/null +++ b/test_ui_and_compatibility.py @@ -0,0 +1,231 @@ +#!/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