diff --git a/build/file_exclusions.json b/build/file_exclusions.json index 15711bd..172542e 100644 --- a/build/file_exclusions.json +++ b/build/file_exclusions.json @@ -11,15 +11,13 @@ "operators/preset_ops.py", "operators/text_editor_ops.py" ], - "exclude_patterns": ["*batch*", "*advanced*", "*overlay*", "*utility*"], - "max_texture_size": 1024, + "exclude_patterns": ["*batch*", "*advanced*", "*utility*"], "max_concurrent_operations": 1 }, "full": { "description": "Full version - all features included", "exclude_files": ["blender_manifest.toml"], "exclude_patterns": [], - "max_texture_size": 4096, "max_concurrent_operations": 4 } }, diff --git a/build/sync_version.py b/build/sync_version.py index 90d3e25..553889d 100755 --- a/build/sync_version.py +++ b/build/sync_version.py @@ -68,7 +68,7 @@ def create_template_variables(config: Dict, version: str, version_type: str = "f # Define features based on version type if version_type == "free": - enabled_features = ["basic", "preview", "image_overlays", "basic_utilities"] + enabled_features = ["basic", "preview"] else: # full version enabled_features = ["basic", "preview", "image_overlays", "basic_utilities", "advanced_styling", "normal_maps", "presets", "batch_processing", "advanced_utilities", "text_fitting"] @@ -105,7 +105,6 @@ def create_template_variables(config: Dict, version: str, version_type: str = "f 'BLENDER_VERSION_PATCH': str(blender_patch), 'DEPENDENCIES': dependencies_str, 'ENABLED_FEATURES': json.dumps(enabled_features), - 'MAX_TEXTURE_SIZE': str(version_config['max_texture_size']), 'MAX_CONCURRENT_OPERATIONS': str(version_config['max_concurrent_operations']) } @@ -204,4 +203,4 @@ if __name__ == "__main__": sync_version(args.version, args.type) except Exception as e: print(f"Error: {e}") - sys.exit(1) \ No newline at end of file + sys.exit(1) diff --git a/build/templates/constants.py.template b/build/templates/constants.py.template index b0e8c33..546698e 100644 --- a/build/templates/constants.py.template +++ b/build/templates/constants.py.template @@ -28,12 +28,10 @@ def get_version_limits(): """Get version-specific limits.""" if VERSION_TYPE == "free": return { - "max_texture_size": {{MAX_TEXTURE_SIZE}}, "max_concurrent_operations": {{MAX_CONCURRENT_OPERATIONS}} } else: return { - "max_texture_size": 4096, "max_concurrent_operations": 4 } @@ -117,9 +115,13 @@ def has_text_fitting(): def get_max_resolution(): - """Get maximum texture resolution based on version""" + """Get maximum texture resolution based on version. + + Returns: + None if texture resolution is unrestricted, otherwise an integer limit. + """ limits = get_version_limits() - return limits.get("max_texture_size", 1024) + return limits.get("max_texture_size") def should_show_feature_lock(feature_name): """Check if feature lock indicator should be shown.""" @@ -157,11 +159,14 @@ def get_feature_availability_text(): def get_version_info_details(): """Get detailed version information for display.""" limits = get_version_limits() + max_texture_size = limits.get('max_texture_size') + if not max_texture_size: + max_texture_size = 'Unlimited' info = { 'version_type': VERSION_TYPE.upper(), - 'max_texture_size': limits.get('max_texture_size', 'Unlimited'), + 'max_texture_size': max_texture_size, 'max_concurrent_operations': limits.get('max_concurrent_operations', 'Unlimited'), 'features_enabled': len(ENABLED_FEATURES), 'upgrade_available': is_free_version() } - return info \ No newline at end of file + return info diff --git a/dist/text_texture_generator_v1.0.0.zip b/dist/text_texture_generator_v1.0.0.zip index ca8fb8c..dc6b476 100644 Binary files a/dist/text_texture_generator_v1.0.0.zip and b/dist/text_texture_generator_v1.0.0.zip differ diff --git a/dist/text_texture_generator_v1.0.0_free.zip b/dist/text_texture_generator_v1.0.0_free.zip index 74899c5..64d3551 100644 Binary files a/dist/text_texture_generator_v1.0.0_free.zip and b/dist/text_texture_generator_v1.0.0_free.zip differ diff --git a/marketing/BLENDERMARKET_LISTING.md b/marketing/BLENDERMARKET_LISTING.md index 4243a45..1c47b8d 100644 --- a/marketing/BLENDERMARKET_LISTING.md +++ b/marketing/BLENDERMARKET_LISTING.md @@ -6,9 +6,9 @@ The Text Texture Generator is an advanced Blender addon that transforms text input into high-quality, production-ready image textures with comprehensive customization capabilities. Built for professional 3D artists and studios, this tool eliminates the need for external graphics software by providing sophisticated text rendering directly within Blender's material workflow. -The addon utilizes advanced text processing algorithms to convert up to 1024 characters into crisp, scalable textures with professional typography control. Every generated texture integrates seamlessly with Blender's material system, automatically creating optimized node setups that maintain full editability and non-destructive workflows. +The addon utilizes advanced text processing algorithms to convert large text blocks into crisp, scalable textures with professional typography control. Every generated texture integrates seamlessly with Blender's material system, automatically creating optimized node setups that maintain full editability and non-destructive workflows. -Designed for technical users who demand precision and control, the addon features a sophisticated 9-point anchor positioning system, advanced effect processing (including stroke, shadow, glow, and normal map generation), and intelligent preset management for consistent results across projects. The high-resolution output capability (up to 4096×4096) ensures compatibility with professional rendering requirements and close-up detail work. +Designed for technical users who demand precision and control, the addon features a sophisticated 9-point anchor positioning system, advanced effect processing (including stroke, shadow, glow, and normal map generation), and intelligent preset management for consistent results across projects. The high-resolution output capability—limited only by your hardware—ensures compatibility with professional rendering requirements and close-up detail work. Cross-platform compatibility across Windows, macOS, and Linux environments, combined with Blender 4.0+ optimization, makes this tool suitable for diverse studio pipelines and collaborative workflows where consistency and reliability are paramount. @@ -16,9 +16,8 @@ Cross-platform compatibility across Windows, macOS, and Linux environments, comb ### Advanced Text Processing -- **Extended Character Support**: Process up to 1024 characters with full Unicode compatibility +- **Extended Character Support**: Process lengthy copy with full Unicode compatibility - **Professional Typography Control**: Precise font selection, sizing, and character spacing -- **Multi-line Text Handling**: Intelligent line breaking and paragraph formatting - **Dynamic Text Scaling**: Resolution-independent text rendering with crisp edge quality ### Precision Positioning System @@ -38,7 +37,7 @@ Cross-platform compatibility across Windows, macOS, and Linux environments, comb ### High-Resolution Output -- **Scalable Resolution**: Output sizes from 256×256 to 4096×4096 pixels +- **Scalable Resolution**: Flexible output sizing with no artificial caps - **Format Optimization**: PNG, JPEG, and OpenEXR support with compression options - **Memory Management**: Efficient processing for large textures without system overload - **Batch Processing**: Multiple texture generation with consistent settings @@ -47,7 +46,7 @@ Cross-platform compatibility across Windows, macOS, and Linux environments, comb #### **🆓 Free Version - Full Core Experience** -- **Complete Text Rendering Pipeline**: Professional typography with up to 1024×1024 resolution +- **Complete Text Rendering Pipeline**: Professional typography with unrestricted texture dimensions - **Essential Positioning System**: 9-point anchor positioning with pixel-perfect alignment - **Seamless Blender Integration**: Automatic material node creation and assignment - **Professional Font Support**: System font detection and custom font file compatibility @@ -56,7 +55,7 @@ Cross-platform compatibility across Windows, macOS, and Linux environments, comb #### **⭐ Full Version - Unleash Professional Power** -- **Ultra-High Resolution**: Generate stunning 4096×4096 textures for commercial work +- **Ultra-High Resolution**: Generate stunning textures at any resolution your project demands - **Advanced 3D Effects**: Automatic normal map generation with customizable depth and blur - **Complete Preset Ecosystem**: Simplified two-tier preset management with import/export for team workflows - **Professional Batch Processing**: Advanced utility operations and workflow automation tools diff --git a/marketing/demo.blend b/marketing/demo.blend index 62df8f8..d8f42c2 100644 Binary files a/marketing/demo.blend and b/marketing/demo.blend differ diff --git a/marketing/demo.blend1 b/marketing/demo.blend1 index 82551c8..c03f6bb 100644 Binary files a/marketing/demo.blend1 and b/marketing/demo.blend1 differ diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc index 266d1b1..8a42469 100644 Binary files a/src/__pycache__/__init__.cpython-311.pyc and b/src/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/core/__pycache__/generation_engine.cpython-311.pyc b/src/core/__pycache__/generation_engine.cpython-311.pyc index 392f0fe..f2905e5 100644 Binary files a/src/core/__pycache__/generation_engine.cpython-311.pyc and b/src/core/__pycache__/generation_engine.cpython-311.pyc differ diff --git a/src/core/generation_engine.py b/src/core/generation_engine.py index 321b7bd..7cab154 100644 --- a/src/core/generation_engine.py +++ b/src/core/generation_engine.py @@ -53,16 +53,16 @@ def generate_texture_image(props, width, height): bicubic_filter = getattr(resampling_module, 'BICUBIC', getattr(Image, 'BICUBIC', Image.BILINEAR)) def _iter_enabled_components(): - """Yield enabled composition components.""" + """Yield enabled composition components with their index.""" overlays = getattr(props, 'image_overlays', []) try: overlay_list = list(overlays) except TypeError: overlay_list = [] - for overlay in overlay_list: + for index, overlay in enumerate(overlay_list): if not getattr(overlay, 'enabled', True): continue - yield overlay + yield index, overlay def _calculate_overlay_position(overlay, overlay_size): """Calculate top-left pixel position for an overlay image.""" @@ -91,102 +91,305 @@ def generate_texture_image(props, width, height): x = max(0, min(max_x, x)) y = max(0, min(max_y, y)) return x, y + + def _make_text_overlay(component, base_font_size): + """Return (image, baseline_offset, descent, render_meta) for a text component.""" + text_value = getattr(component, 'text_content', '').strip() + if not text_value: + return None, None, None, None + scale = getattr(component, 'scale', 1.0) + try: + scale = float(scale if scale is not None else 1.0) + except (TypeError, ValueError): + scale = 1.0 + if not math.isfinite(scale) or scale <= 0: + return None, None, None, None + desired_size = max(4, int(round(max(base_font_size, 4) * scale))) + text_font = load_font_for_size(desired_size) - def _apply_composition_components(pil_canvas): - """Composite enabled components (text or image) onto the base canvas.""" - components = sorted( - _iter_enabled_components(), - key=lambda comp: getattr(comp, 'z_index', 0) - ) + temp_draw = ImageDraw.Draw(Image.new('RGBA', (1, 1))) - for component in components: - if getattr(component, 'component_type', 'IMAGE') == 'TEXT': - text_value = getattr(component, 'text_content', '').strip() - if not text_value: + overlay_img = None + baseline_offset = None + component_descent = None + render_meta = None + + # Prefer anchoring to the left baseline so we can align with the main text baseline. + try: + baseline_bbox = temp_draw.textbbox( + (0, 0), + text_value, + font=text_font, + anchor='ls' + ) + except Exception: + baseline_bbox = None + + if baseline_bbox: + left, top, right, bottom = baseline_bbox + text_width = max(1, int(round(right - left))) + text_height = max(1, int(round(bottom - top))) + overlay_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0)) + overlay_draw = ImageDraw.Draw(overlay_img) + text_color = tuple(int(c * 255) for c in props.text_color[:4]) + # Place the baseline at a consistent offset from the top of the overlay. + baseline_offset = -top + draw_x = -left + draw_y = -top + overlay_draw.text((draw_x, draw_y), text_value, font=text_font, fill=text_color, anchor='ls') + else: + # Fallback for PIL versions without baseline-aware textbbox. + try: + text_bbox = temp_draw.textbbox((0, 0), text_value, font=text_font) + except Exception: + width, height = temp_draw.textsize(text_value, font=text_font) + text_bbox = (0, 0, width, height) + left, top, right, bottom = text_bbox + text_width = max(1, int(round(right - left))) + text_height = max(1, int(round(bottom - top))) + overlay_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0)) + overlay_draw = ImageDraw.Draw(overlay_img) + text_color = tuple(int(c * 255) for c in props.text_color[:4]) + offset_x = -left + offset_y = -top + baseline_offset = offset_y + overlay_draw.text((offset_x, offset_y), text_value, font=text_font, fill=text_color) + + content_box = overlay_img.getbbox() if overlay_img else None + crop_left = crop_top = 0 + if content_box: + crop_left, crop_top, crop_right, crop_bottom = content_box + overlay_img = overlay_img.crop(content_box) + if baseline_offset is not None: + baseline_offset -= crop_top + component_descent = (crop_bottom - crop_top) - (baseline_offset or 0) + else: + component_descent = (overlay_img.height if overlay_img else 0) - (baseline_offset or 0) + + if overlay_img and (baseline_offset is None or baseline_offset < 0 or baseline_offset > overlay_img.height): + try: + ascent, descent = text_font.getmetrics() + except Exception: + ascent, descent = overlay_img.height, 0 + baseline_offset = min(max(0, int(round(ascent))), overlay_img.height) + component_descent = max(0, overlay_img.height - baseline_offset) + + baseline_offset = max(0, int(round(baseline_offset or 0))) + component_descent = max(0, int(round(component_descent or 0))) + render_meta = { + 'text': text_value, + 'font': text_font, + 'color': tuple(int(c * 255) for c in props.text_color[:4]), + 'draw_offset_x': (draw_x - crop_left) if baseline_bbox else (offset_x - crop_left), + 'draw_offset_y': (draw_y - crop_top) if baseline_bbox else (offset_y - crop_top), + 'anchor': 'ls' if baseline_bbox else None, + 'width': overlay_img.width if overlay_img else 0, + 'height': overlay_img.height if overlay_img else 0 + } + return overlay_img, baseline_offset, component_descent, render_meta + + def _make_image_overlay(component): + """Return RGBA image for an image component or None.""" + resolved_path = getattr(component, 'image_path', '') + if not resolved_path: + return None + scale = getattr(component, 'scale', 1.0) + try: + scale = float(scale if scale is not None else 1.0) + except (TypeError, ValueError): + scale = 1.0 + if not math.isfinite(scale) or scale <= 0: + return None + scale_to_apply = scale + if is_svg_path(resolved_path): + try: + raster_info = ensure_svg_raster(resolved_path, scale) + resolved_path = raster_info.path + scale_to_apply = raster_info.applied_scale + except SVGConversionError as svg_error: + print(f"WARNING: Failed to rasterize SVG overlay '{component.image_path}': {svg_error}") + return None + try: + with Image.open(resolved_path) as raw_overlay: + overlay_img = raw_overlay.convert('RGBA') + except Exception as overlay_error: + print(f"WARNING: Failed to load overlay image '{resolved_path}': {overlay_error}") + return None + if scale_to_apply != 1.0: + new_size = ( + max(1, int(round(overlay_img.width * scale_to_apply))), + max(1, int(round(overlay_img.height * scale_to_apply))) + ) + if new_size != overlay_img.size: + overlay_img = overlay_img.resize(new_size, lanczos_filter) + return overlay_img + + def _apply_component_layers(pil_canvas, stage, base_text_rect, base_font_size): + """Composite enabled components according to z-index and placement.""" + components_data = [] + for index, component in _iter_enabled_components(): + z_value = getattr(component, 'z_index', 0) + if stage == 'before' and z_value >= 0: + continue + if stage == 'after' and z_value < 0: + continue + component_type = getattr(component, 'component_type', 'IMAGE') + if component_type == 'TEXT': + overlay_img, baseline_offset, component_descent, render_meta = _make_text_overlay(component, base_font_size) + else: + overlay_img = _make_image_overlay(component) + baseline_offset = overlay_img.height if overlay_img is not None else None + component_descent = overlay_img.height if overlay_img is not None else None + render_meta = None + if overlay_img is None and not render_meta: + continue + rotation = float(getattr(component, 'rotation', 0.0) or 0.0) + direct_draw = False + width_from_image = overlay_img.width if overlay_img is not None else 0 + height_from_image = overlay_img.height if overlay_img is not None else 0 + if component_type == 'TEXT' and render_meta and not (rotation % 360): + direct_draw = True + else: + if overlay_img is None: continue - scale = getattr(component, 'scale', 1.0) - try: - scale = float(scale if scale is not None else 1.0) - except (TypeError, ValueError): - scale = 1.0 - text_size = max(8, int(round(props.font_size * max(scale, 0.1)))) - text_font = load_font_for_size(text_size) - try: - text_bbox = text_font.getbbox(text_value) - text_width = text_bbox[2] - text_bbox[0] - text_height = text_bbox[3] - text_bbox[1] - except AttributeError: - try: - text_width, text_height = text_font.getsize(text_value) - except AttributeError: - # Fallback to a temporary draw context - tmp_draw = ImageDraw.Draw(Image.new('RGBA', (1, 1))) - text_width, text_height = tmp_draw.textsize(text_value, font=text_font) - - overlay_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0)) - overlay_draw = ImageDraw.Draw(overlay_img) - text_color = tuple(int(c * 255) for c in props.text_color[:4]) - overlay_draw.text((0, 0), text_value, font=text_font, fill=text_color) - - rotation = float(getattr(component, 'rotation', 0.0) or 0.0) if rotation % 360: overlay_img = overlay_img.rotate(-rotation, expand=True, resample=bicubic_filter) + width_from_image = overlay_img.width + height_from_image = overlay_img.height + width_value = 0 + height_value = 0 + if component_type == 'TEXT' and render_meta: + if direct_draw: + width_value = render_meta.get('width', width_from_image) + height_value = render_meta.get('height', height_from_image) + else: + width_value = width_from_image + height_value = height_from_image + elif overlay_img is not None: + width_value = width_from_image + height_value = height_from_image + components_data.append({ + 'index': index, + 'component': component, + 'image': overlay_img, + 'z': z_value, + 'type': component_type, + 'baseline_offset': baseline_offset, + 'descent': component_descent, + 'render_info': render_meta, + 'direct_draw': direct_draw, + 'rotation': rotation, + 'width': width_value, + 'height': height_value + }) + + if not components_data: + return pil_canvas + + sequential_groups = {'PREPEND': [], 'APPEND': []} + absolute_entries = [] + for entry in components_data: + placement = getattr(entry['component'], 'placement_mode', 'ABSOLUTE') or 'ABSOLUTE' + if placement in sequential_groups: + sequential_groups[placement].append(entry) + else: + x_pos, y_pos = _calculate_overlay_position(entry['component'], entry['image'].size) + entry['position'] = (x_pos, y_pos) + absolute_entries.append(entry) + + base_center_x = width * 0.5 + base_center_y = height * 0.5 + base_left = base_center_x + base_right = base_center_x + base_top = base_center_y - (base_font_size / 2.0) + base_bottom = base_center_y + (base_font_size / 2.0) + base_baseline = base_bottom + if base_text_rect: + base_center_x = base_text_rect['center_x'] + base_center_y = base_text_rect['center_y'] + base_left = base_text_rect['left'] + base_right = base_text_rect['right'] + base_top = base_text_rect.get('top', base_top) + base_bottom = base_text_rect.get('bottom', base_bottom) + base_baseline = base_text_rect.get('baseline', base_bottom) + else: + base_top = max(0.0, min(float(height - base_font_size), base_top)) + base_bottom = max(base_top + 1.0, min(float(height), base_bottom)) + base_baseline = base_bottom + + def assign_sequential_positions(entries, placement): + if not entries: + return [] + sorted_entries = sorted(entries, key=lambda item: item['index']) + cursor = base_left if placement == 'PREPEND' else base_right + assigned = [] + for entry in sorted_entries: + component = entry['component'] + entry_width = entry.get('width', entry['image'].width if entry['image'] else 0) + entry_height = entry.get('height', entry['image'].height if entry['image'] else 0) + spacing_factor = float(getattr(component, 'text_spacing', 0.0) or 0.0) / 100.0 + inter_spacing_factor = float(getattr(component, 'image_spacing', 0.0) or 0.0) / 100.0 + spacing_pixels = spacing_factor * width + inter_pixels = inter_spacing_factor * width - x_pos, y_pos = _calculate_overlay_position(component, overlay_img.size) - + if placement == 'PREPEND': + cursor -= spacing_pixels + cursor -= entry_width + x_pos = cursor + cursor -= inter_pixels + else: + cursor += spacing_pixels + x_pos = cursor + cursor += entry_width + cursor += inter_pixels + if base_text_rect: + if entry['type'] == 'TEXT' and entry['baseline_offset'] is not None: + y_pos = int(round(base_baseline - entry['baseline_offset'])) + else: + y_pos = int(round(base_bottom - entry_height)) + else: + center_y = base_center_y + y_pos = int(round(center_y - entry_height / 2.0)) + x_pos = int(round(x_pos)) + y_pos = max(0, min(height - entry_height, y_pos)) + x_pos = max(0, min(width - entry_width, x_pos)) + entry['position'] = (x_pos, y_pos) + assigned.append(entry) + return assigned + + positioned_entries = absolute_entries + positioned_entries.extend(assign_sequential_positions(sequential_groups['PREPEND'], 'PREPEND')) + positioned_entries.extend(assign_sequential_positions(sequential_groups['APPEND'], 'APPEND')) + + positioned_entries.sort(key=lambda item: item['z']) + + draw_ctx = None + for entry in positioned_entries: + x_pos, y_pos = entry.get('position', (0, 0)) + if entry.get('direct_draw') and entry.get('render_info'): + if draw_ctx is None: + draw_ctx = ImageDraw.Draw(pil_canvas) + info = entry['render_info'] + draw_pos = ( + x_pos + info.get('draw_offset_x', 0), + y_pos + info.get('draw_offset_y', 0) + ) + anchor = info.get('anchor') + draw_ctx.text( + draw_pos, + info.get('text', ''), + font=info.get('font'), + fill=info.get('color'), + anchor=anchor + ) + else: + overlay_img = entry.get('image') + if overlay_img is None: + continue overlay_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) overlay_layer.paste(overlay_img, (x_pos, y_pos), overlay_img) pil_canvas = Image.alpha_composite(pil_canvas, overlay_layer) - continue - - resolved_path = getattr(component, 'image_path', '') - if not resolved_path: - continue - scale = getattr(component, 'scale', 1.0) - try: - scale = float(scale if scale is not None else 1.0) - except (TypeError, ValueError): - scale = 1.0 - - if not math.isfinite(scale) or scale <= 0: - continue - - scale_to_apply = scale - - if is_svg_path(resolved_path): - try: - raster_info = ensure_svg_raster(resolved_path, scale) - resolved_path = raster_info.path - scale_to_apply = raster_info.applied_scale - except SVGConversionError as svg_error: - print(f"WARNING: Failed to rasterize SVG overlay '{component.image_path}': {svg_error}") - continue - - try: - with Image.open(resolved_path) as raw_overlay: - overlay_img = raw_overlay.convert('RGBA') - except Exception as overlay_error: - print(f"WARNING: Failed to load overlay image '{resolved_path}': {overlay_error}") - continue - - if scale_to_apply != 1.0: - new_size = ( - max(1, int(round(overlay_img.width * scale_to_apply))), - max(1, int(round(overlay_img.height * scale_to_apply))) - ) - if new_size != overlay_img.size: - overlay_img = overlay_img.resize(new_size, lanczos_filter) - - rotation = float(getattr(component, 'rotation', 0.0) or 0.0) - if rotation % 360: - overlay_img = overlay_img.rotate(-rotation, expand=True, resample=bicubic_filter) - - x_pos, y_pos = _calculate_overlay_position(component, overlay_img.size) - - overlay_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) - overlay_layer.paste(overlay_img, (x_pos, y_pos), overlay_img) - pil_canvas = Image.alpha_composite(pil_canvas, overlay_layer) - + return pil_canvas # Create PIL image with transparent background @@ -198,6 +401,10 @@ def generate_texture_image(props, width, height): bg_color = tuple(int(c * 255) for c in props.background_color[:3]) + (255,) pil_img = Image.new('RGBA', (width, height), bg_color) + base_text_rect = None + final_font_size = props.font_size + before_layers_applied = False + # Draw text if provided (including prepend/append) full_text_parts = [] if props.prepend_text.strip(): @@ -212,7 +419,7 @@ def generate_texture_image(props, width, height): if full_text_parts: pass - draw = ImageDraw.Draw(pil_img) + measure_draw = ImageDraw.Draw(pil_img) # Combine text based on layout mode if props.prepend_append_layout == 'HORIZONTAL': @@ -304,12 +511,12 @@ def generate_texture_image(props, width, height): test_font = load_font_for_size(test_size) try: - bbox = draw.textbbox((0, 0), full_text, font=test_font) + bbox = measure_draw.textbbox((0, 0), full_text, font=test_font) test_width = bbox[2] - bbox[0] test_height = bbox[3] - bbox[1] except (ValueError, AttributeError): # Fallback for bitmap fonts - test_width, test_height = draw.textsize(full_text, font=test_font) + test_width, test_height = measure_draw.textsize(full_text, font=test_font) if test_width <= target_width and test_height <= target_height: pass @@ -323,7 +530,12 @@ def generate_texture_image(props, width, height): pass font_size = props.font_size + final_font_size = font_size font = load_font_for_size(font_size) + try: + base_ascent, base_descent = font.getmetrics() + except Exception: + base_ascent, base_descent = (final_font_size, 0) # Calculate text position and draw if layout_mode == "vertical" and props.prepend_append_layout == 'VERTICAL': @@ -331,18 +543,25 @@ def generate_texture_image(props, width, height): # Draw each part separately for vertical layout y_offset = 0 total_height = 0 - + part_widths = [] + part_baselines = [] + # Calculate total height first part_heights = [] for part in full_text_parts: pass try: - bbox = draw.textbbox((0, 0), part, font=font) + bbox = measure_draw.textbbox((0, 0), part, font=font) part_height = bbox[3] - bbox[1] + part_width = bbox[2] - bbox[0] + baseline = base_ascent except (ValueError, AttributeError): # Fallback for bitmap fonts - _, part_height = draw.textsize(part, font=font) + part_width, part_height = measure_draw.textsize(part, font=font) + baseline = base_ascent part_heights.append(part_height) + part_widths.append(part_width) + part_baselines.append(baseline) total_height += part_height # Add spacing between parts @@ -353,6 +572,23 @@ def generate_texture_image(props, width, height): # Center vertically start_y = (height - total_height) // 2 + max_width = max(part_widths) if part_widths else 0 + left_x = (width - max_width) // 2 + base_text_rect = { + 'left': float(left_x), + 'right': float(left_x + max_width), + 'top': float(start_y), + 'bottom': float(start_y + total_height), + 'center_x': float(left_x + max_width / 2.0), + 'center_y': float(start_y + total_height / 2.0), + 'baseline': float(start_y + base_ascent), + 'descent': float(base_descent), + 'draw_y': float(start_y), + 'ascent': float(base_ascent) + } + pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size) + before_layers_applied = True + draw = ImageDraw.Draw(pil_img) # Draw each part current_y = start_y @@ -360,12 +596,7 @@ def generate_texture_image(props, width, height): for i, part in enumerate(full_text_parts): pass - try: - bbox = draw.textbbox((0, 0), part, font=font) - part_width = bbox[2] - bbox[0] - except (ValueError, AttributeError): - # Fallback for bitmap fonts - part_width, _ = draw.textsize(part, font=font) + part_width = part_widths[i] x = (width - part_width) // 2 # Center horizontally draw.text((x, current_y), part, font=font, fill=text_color) @@ -377,17 +608,47 @@ def generate_texture_image(props, width, height): else: pass # Horizontal layout or single text + left_offset = 0 + right_offset = 0 + top_offset = 0 + bottom_offset = 0 + try: - text_bbox = draw.textbbox((0, 0), full_text, font=font) + text_bbox = measure_draw.textbbox((0, 0), full_text, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] + left_offset = text_bbox[0] + right_offset = text_bbox[2] + top_offset = text_bbox[1] + bottom_offset = text_bbox[3] except (ValueError, AttributeError): # Fallback for bitmap fonts that don't support textbbox - text_width, text_height = draw.textsize(full_text, font=font) + text_width, text_height = measure_draw.textsize(full_text, font=font) + right_offset = text_width + bottom_offset = text_height # Center text x = (width - text_width) // 2 y = (height - text_height) // 2 + left = x + left_offset + right = x + right_offset + top = y + top_offset + bottom = y + bottom_offset + base_text_rect = { + 'left': float(left), + 'right': float(right), + 'top': float(top), + 'bottom': float(bottom), + 'center_x': float((left + right) / 2.0), + 'center_y': float((top + bottom) / 2.0), + 'baseline': float(y + base_ascent), + 'descent': float(base_descent), + 'draw_y': float(y), + 'ascent': float(base_ascent) + } + pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size) + before_layers_applied = True + draw = ImageDraw.Draw(pil_img) # Draw text text_color = tuple(int(c * 255) for c in props.text_color[:4]) @@ -395,8 +656,9 @@ def generate_texture_image(props, width, height): else: pass - # Apply any configured image overlays before converting to Blender pixels - pil_img = _apply_composition_components(pil_img) + if not before_layers_applied: + pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size) + pil_img = _apply_component_layers(pil_img, 'after', base_text_rect, final_font_size) # Convert PIL image to Blender format (flip vertically to fix mirroring) pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM) # Fix mirroring issue diff --git a/src/operators/__init__.py b/src/operators/__init__.py index f2dab60..5844c26 100644 --- a/src/operators/__init__.py +++ b/src/operators/__init__.py @@ -3,6 +3,7 @@ Text Texture Generator - Operators Module Contains all Blender operators for text texture generation and management. """ +from ..utils.constants import is_full_version from .generation_ops import * __all__ = [] @@ -44,23 +45,23 @@ except ImportError: pass # preset_ops module not available (free version) PRESET_OPERATORS = [] -try: - pass - from .text_editor_ops import * - from .text_editor_ops import ( - TEXT_TEXTURE_OT_edit_text, - ) - TEXT_EDITOR_OPERATORS = [ - 'TEXT_TEXTURE_OT_edit_text', - ] -except ImportError as e: - import sys - import traceback - print(f"WARNING: text_editor_ops import failed in operators/__init__.py: {e}", file=sys.stderr) - traceback.print_exc() - # text_editor_ops module not available (free version) - TEXT_EDITOR_OPERATORS = [] +if is_full_version(): + try: + pass + from .text_editor_ops import * + from .text_editor_ops import ( + TEXT_TEXTURE_OT_edit_text, + ) + TEXT_EDITOR_OPERATORS = [ + 'TEXT_TEXTURE_OT_edit_text', + ] + except ImportError as e: + import sys + print(f"WARNING: text_editor_ops import failed in operators/__init__.py: {e}", file=sys.stderr) + TEXT_EDITOR_OPERATORS = [] +else: + TEXT_EDITOR_OPERATORS = [] try: pass @@ -106,8 +107,6 @@ __all__.extend([ # Add preset operators if available __all__.extend(PRESET_OPERATORS) -# Add utility operators if available - # Add text editor operators if available __all__.extend(TEXT_EDITOR_OPERATORS) __all__.extend(UTILITY_OPERATORS) diff --git a/src/ui/__pycache__/panels.cpython-311.pyc b/src/ui/__pycache__/panels.cpython-311.pyc index bd68e28..5a30685 100644 Binary files a/src/ui/__pycache__/panels.cpython-311.pyc and b/src/ui/__pycache__/panels.cpython-311.pyc differ diff --git a/src/ui/panels.py b/src/ui/panels.py index 5659df1..3736970 100644 --- a/src/ui/panels.py +++ b/src/ui/panels.py @@ -85,6 +85,21 @@ def draw_collapsible_header(layout, prop_name, text, icon='TRIA_DOWN'): return box if expanded else None +def draw_premium_collapsible_section(layout, prop_name, title, icon, availability_func): + """Draw a collapsible header that locks when feature is unavailable.""" + header_row = layout.row(align=True) + try: + available = availability_func() + except Exception: + available = False + if not available: + header_row.enabled = False + lock_header = header_row.row() + lock_header.label(text=title, icon=icon) + lock_header.label(text="🔒 PRO", icon='LOCKED') + return None + return draw_collapsible_header(header_row, prop_name, title, icon) + def draw_version_badge(layout): pass """Draw version badge in panel header""" @@ -151,7 +166,10 @@ def draw_version_info_section(layout, props): limits_col = col.column(align=True) limits_col.separator() limits_col.label(text="Current Limits:") - limits_col.label(text=f"• Max texture size: {version_info['max_texture_size']}px") + max_texture_display = version_info['max_texture_size'] + if isinstance(max_texture_display, (int, float)): + max_texture_display = f"{int(max_texture_display)}px" + limits_col.label(text=f"• Max texture size: {max_texture_display}") limits_col.label(text=f"• Active features: {version_info['features_enabled']}") # Feature availability status @@ -989,7 +1007,13 @@ class TEXT_TEXTURE_PT_panel(Panel): draw_shader_options_only(shader_expanded, props) # Composition Section (collapsed by default) - composition_section = draw_collapsible_header(layout, "expand_composition", "Composition", 'SEQ_PREVIEW') + composition_section = draw_premium_collapsible_section( + layout, + "expand_composition", + "Composition", + 'SEQ_PREVIEW', + has_image_overlays + ) if composition_section: draw_composition_section(composition_section, props) @@ -1186,7 +1210,13 @@ class TEXT_TEXTURE_PT_panel_3d(Panel): draw_shader_options_only(shader_expanded, props) # Composition Section (collapsed by default) - composition_section = draw_collapsible_header(layout, "expand_composition", "Composition", 'SEQ_PREVIEW') + composition_section = draw_premium_collapsible_section( + layout, + "expand_composition", + "Composition", + 'SEQ_PREVIEW', + has_image_overlays + ) if composition_section: draw_composition_section(composition_section, props) @@ -1363,7 +1393,13 @@ class TEXT_TEXTURE_PT_panel_properties(Panel): # === PREMIUM FEATURES === # Composition Section (collapsed by default) - composition_section = draw_collapsible_header(layout, "expand_composition", "Composition", 'SEQ_PREVIEW') + composition_section = draw_premium_collapsible_section( + layout, + "expand_composition", + "Composition", + 'SEQ_PREVIEW', + has_image_overlays + ) if composition_section: draw_composition_section(composition_section, props) diff --git a/src/utils/__pycache__/constants.cpython-311.pyc b/src/utils/__pycache__/constants.cpython-311.pyc index 3245e45..b095e97 100644 Binary files a/src/utils/__pycache__/constants.cpython-311.pyc and b/src/utils/__pycache__/constants.cpython-311.pyc differ diff --git a/src/utils/constants.py b/src/utils/constants.py index b1f7aca..fa5cb6d 100644 --- a/src/utils/constants.py +++ b/src/utils/constants.py @@ -28,12 +28,10 @@ def get_version_limits(): """Get version-specific limits.""" if VERSION_TYPE == "free": return { - "max_texture_size": 1024, "max_concurrent_operations": 1 } else: return { - "max_texture_size": 4096, "max_concurrent_operations": 4 } @@ -117,9 +115,13 @@ def has_text_fitting(): def get_max_resolution(): - """Get maximum texture resolution based on version""" + """Get maximum texture resolution based on version. + + Returns: + None if texture resolution is unrestricted, otherwise an integer limit. + """ limits = get_version_limits() - return limits.get("max_texture_size", 1024) + return limits.get("max_texture_size") def should_show_feature_lock(feature_name): """Check if feature lock indicator should be shown.""" @@ -157,11 +159,14 @@ def get_feature_availability_text(): def get_version_info_details(): """Get detailed version information for display.""" limits = get_version_limits() + max_texture_size = limits.get('max_texture_size') + if not max_texture_size: + max_texture_size = 'Unlimited' info = { 'version_type': VERSION_TYPE.upper(), - 'max_texture_size': limits.get('max_texture_size', 'Unlimited'), + 'max_texture_size': max_texture_size, 'max_concurrent_operations': limits.get('max_concurrent_operations', 'Unlimited'), 'features_enabled': len(ENABLED_FEATURES), 'upgrade_available': is_free_version() } - return info \ No newline at end of file + return info diff --git a/tests/e2e/test_free_version_e2e.py b/tests/e2e/test_free_version_e2e.py index 43ea4c4..9cb9b1b 100644 --- a/tests/e2e/test_free_version_e2e.py +++ b/tests/e2e/test_free_version_e2e.py @@ -5,7 +5,7 @@ These tests specify the expected behavior when VERSION_TYPE="free". They validate that free version users: 1. Can use basic features (text generation, preview) 2. CANNOT access PRO features (shaders, presets, overlays, normal maps) -3. Have REDUCED limits (max_texture_size=1024, max_concurrent=1) +3. Maintain concurrency cap (max_concurrent=1) without texture size limits 4. See UI locks/badges on PRO features TESTING STRATEGY: @@ -17,7 +17,7 @@ TESTING STRATEGY: EXPECTED BEHAVIOR (FREE VERSION): - ENABLED_FEATURES = ["basic", "preview"] only - BLOCKED: shader_generation, normal_maps, image_overlays, presets, advanced -- LIMITS: max_texture_size=1024, max_concurrent_operations=1 +- LIMITS: Unlimited texture size, max_concurrent_operations=1 - UI: Shows "🔒 PRO" badges on locked features """ @@ -411,19 +411,19 @@ print("SUCCESS: Text fitting properly blocked") assert "SUCCESS: Text fitting properly blocked" in output -def test_free_version_texture_size_limited(blender_container, addon_package): +def test_free_version_allows_large_textures(blender_container, addon_package): """ - SPECIFICATION: Free version MUST limit max_texture_size to 1024px (not 4096px). + SPECIFICATION UPDATE: Free version should allow large texture dimensions without artificial caps. Expected behavior (VERSION_TYPE="free"): - - get_version_limits()['max_texture_size'] == 1024 - - Attempting 4096px should fail or clamp to 1024 - - UI should disable/hide 2048+ size options + - get_version_limits() does NOT define 'max_texture_size' + - Generating a 4096px texture succeeds without clamping + - UI should allow entering high-resolution values Current state (VERSION_TYPE="full"): - - Test SKIPS (max_texture_size is 4096) + - Test SKIPS (runs only when free build is active) - **CRITICAL BUG**: get_version_limits() currently returns same values for both versions! + **PREVIOUS BUG**: get_version_limits() returned identical caps for both versions. """ version_type, enabled_features = get_version_type(blender_container, addon_package) @@ -438,13 +438,27 @@ import sys from text_texture_generator.utils.constants import get_version_limits limits = get_version_limits() -# CRITICAL: This is the BUG - get_version_limits() returns same for both versions! -max_size = limits.get('max_texture_size', 4096) -if max_size != 1024: - print(f"FAIL: max_texture_size should be 1024 for free version, got {max_size}") +# Ensure no artificial texture size cap is reported +max_size = limits.get('max_texture_size') +if max_size: + print(f"FAIL: Expected no texture size cap, got {max_size}") sys.exit(1) -print(f"SUCCESS: Texture size properly limited to {max_size}px") +# Attempt a large texture generation +props = bpy.context.scene.text_texture_props +props.text = "High Resolution Test" +props.texture_width = 4096 +props.texture_height = 4096 + +try: + bpy.ops.text_texture.generate_shader() + if props.texture_width != 4096 or props.texture_height != 4096: + print(f"FAIL: Texture dimensions were clamped to {props.texture_width}x{props.texture_height}") + sys.exit(1) + print("SUCCESS: Large texture generated without limits") +except Exception as e: + print(f"FAIL: Large texture generation failed: {e}") + sys.exit(1) """ tar_stream = io.BytesIO() @@ -469,7 +483,7 @@ print(f"SUCCESS: Texture size properly limited to {max_size}px") # This test WILL FAIL because get_version_limits() has the bug assert test_result.exit_code == 0, f"Size limit test failed:\n{output}" - assert "SUCCESS: Texture size properly limited to 1024px" in output + assert "SUCCESS: Large texture generated without limits" in output def test_free_version_ui_shows_pro_locks(blender_container, addon_package): @@ -852,4 +866,3 @@ except ImportError: assert test_result.exit_code == 0, f"Operator poll test failed:\n{output}" assert "SUCCESS: has_multiline_text helper removed" in output assert "SUCCESS: Multiline editor operator removed" in output - diff --git a/tests/e2e/test_full_version_e2e.py b/tests/e2e/test_full_version_e2e.py index dbe2814..6e5497b 100644 --- a/tests/e2e/test_full_version_e2e.py +++ b/tests/e2e/test_full_version_e2e.py @@ -4,7 +4,7 @@ E2E tests for full version feature completeness. Tests run in Docker with real Blender to verify: - All features enabled in ENABLED_FEATURES list - Shader generation works -- Large texture sizes (4096px) supported +- Large texture sizes supported (no artificial cap) - No UI locks/PRO badges visible NO MOCKS - real Blender bpy API operations. @@ -259,15 +259,13 @@ except Exception as e: assert "SUCCESS: Shader generation works" in output -def test_full_version_large_texture_4096px( +def test_full_version_supports_large_textures( blender_container, addon_package, tmp_path ): """ - Verify large texture size (4096px) works - this is the full version limit. - - Full version should support up to 4096px. + Verify large texture sizes work without an explicit limit in the full version. """ script_content = """ import bpy @@ -284,19 +282,20 @@ except Exception as e: try: from text_texture_generator.utils.constants import get_version_limits - # Verify max texture size limits = get_version_limits() - max_size = limits.get('max_texture_size', 0) + max_size = limits.get('max_texture_size') - if max_size != 4096: - print(f"FAIL: Expected max_texture_size=4096, got {max_size}") + if max_size: + print(f"FAIL: Expected unlimited texture size, got {max_size}") sys.exit(1) - # For now, just verify the limit and produce expected output - # Full integration test would require registered properties - print("Texture size: 4096x4096") - print("Image has pixel data") - print("SUCCESS: 4096px texture generated") + props = bpy.context.scene.text_texture_props + props.text = "Full Version High Resolution" + props.texture_width = 4096 + props.texture_height = 4096 + bpy.ops.text_texture.generate_shader() + print(f"Texture size: {props.texture_width}x{props.texture_height}") + print("SUCCESS: Full version large texture generated") except Exception as e: print(f"ERROR: {e}") @@ -374,8 +373,7 @@ except Exception as e: ) assert "Texture size: 4096x4096" in output - assert "Image has pixel data" in output - assert "SUCCESS: 4096px texture generated" in output + assert "SUCCESS: Full version large texture generated" in output def test_full_version_no_ui_locks( @@ -407,11 +405,11 @@ try: # Check version limits limits = get_version_limits() - max_size = limits.get('max_texture_size', 0) + max_size = limits.get('max_texture_size') print(f"MAX_TEXTURE_SIZE: {max_size}") - if max_size != 4096: - print(f"FAIL: Expected max_texture_size=4096, got {max_size}") + if max_size: + print(f"FAIL: Expected no texture size limit, got {max_size}") sys.exit(1) # Check if PRO features are locked @@ -431,6 +429,9 @@ try: # For now, just verify limits without accessing unregistered properties # Full integration test would require registered properties + props = bpy.context.scene.text_texture_props + props.texture_width = 4096 + props.texture_height = 4096 print(f"Can set 4096px: True") print("SUCCESS: UI is fully unlocked") @@ -511,7 +512,7 @@ except Exception as e: ) assert "VERSION_TYPE: full" in output - assert "MAX_TEXTURE_SIZE: 4096" in output + assert "MAX_TEXTURE_SIZE: None" in output assert "has_pro_locks: False" in output assert "Can set 4096px: True" in output diff --git a/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc b/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc index 44a653f..ecd294a 100644 Binary files a/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc and b/tests/unit/core/__pycache__/test_generation_engine.cpython-311-pytest-8.3.5.pyc differ diff --git a/tests/unit/core/test_generation_engine.py b/tests/unit/core/test_generation_engine.py index 5307507..e094777 100644 --- a/tests/unit/core/test_generation_engine.py +++ b/tests/unit/core/test_generation_engine.py @@ -149,7 +149,7 @@ class TestGenerateTextureImage: assert result is not None assert normal_map is None # Normal maps not enabled mock_pil_image_module.new.assert_called_once() - mock_pil_imagedraw_module.Draw.assert_called_once() + assert mock_pil_imagedraw_module.Draw.call_count >= 1 mock_draw.text.assert_called() mock_bpy.data.images.new.assert_called_once() mock_pil_image.transpose.assert_called_once() @@ -715,6 +715,115 @@ class TestGenerateTextureImage: assert max(blue_columns) <= expected_x + overlay_source.width assert max(blue_columns) < width // 2 # should remain on left half + def test_prepend_text_component_shares_baseline(self): + """Prepend text components should align exactly with the main text baseline.""" + Image = pytest.importorskip("PIL.Image") + with patch('src.core.generation_engine.bpy') as mock_bpy: + width = height = 256 + + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (width * height * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + props = create_mock_props( + text="World", + background_transparent=True, + background_color=(0.0, 0.0, 0.0, 0.0), + text_color=(1.0, 1.0, 1.0, 1.0), + font_size=72, + ) + + component = MagicMock() + component.enabled = True + component.component_type = 'TEXT' + component.text_content = "Test" + component.placement_mode = 'PREPEND' + component.scale = 1.0 + component.rotation = 0.0 + component.z_index = 0 + component.text_spacing = 5.0 + component.image_spacing = 0.0 + props.image_overlays = [component] + + result, _ = generate_texture_image(props, width, height) + + assert result is mock_blender_image + + pixels = mock_blender_image.pixels + + def collect_bounds(x_start, x_end): + ys = [] + for y in range(height): + for x in range(x_start, x_end): + idx = (y * width + x) * 4 + alpha = pixels[idx + 3] + if alpha > 0.5: + ys.append(y) + break + if not ys: + return None + return min(ys), max(ys) + + left_bounds = collect_bounds(0, width // 2) + right_bounds = collect_bounds(width // 2, width) + + assert left_bounds and right_bounds, "Expected both text regions to be present" + + left_top, left_bottom = left_bounds + right_top, right_bottom = right_bounds + + assert abs(left_top - right_top) <= 1, ( + f"Top alignment mismatch: prepend top={left_top}, main top={right_top}" + ) + + def test_absolute_component_negative_z_draws_behind_text(self, tmp_path): + """Absolute components with negative z-index should render behind the main text.""" + Image = pytest.importorskip("PIL.Image") + with patch('src.core.generation_engine.bpy') as mock_bpy: + width = height = 128 + overlay_path = tmp_path / "background_overlay.png" + overlay_source = Image.new("RGBA", (128, 128), (255, 0, 0, 255)) + overlay_source.save(overlay_path) + + mock_blender_image = MagicMock() + mock_blender_image.pixels = [0.0] * (width * height * 4) + mock_bpy.data.images.new.return_value = mock_blender_image + mock_bpy.data.images.__contains__.return_value = False + + props = create_mock_props( + text="T", + background_transparent=False, + background_color=(0.0, 0.0, 0.0, 1.0), + ) + + overlay = MagicMock() + overlay.enabled = True + overlay.component_type = 'IMAGE' + overlay.image_path = str(overlay_path) + overlay.placement_mode = 'ABSOLUTE' + overlay.x_position = 0.5 + overlay.y_position = 0.5 + overlay.scale = 1.0 + overlay.rotation = 0.0 + overlay.z_index = -2 + overlay.text_spacing = 0.0 + overlay.image_spacing = 0.0 + props.image_overlays = [overlay] + + result, normal_map = generate_texture_image(props, width, height) + + assert result is mock_blender_image + assert normal_map is None + + pixels = mock_blender_image.pixels + quads = [pixels[i:i+4] for i in range(0, len(pixels), 4)] + max_brightness = max((r + g + b) / 3.0 for r, g, b, _ in quads) + red_pixels = sum(1 for r, g, b, _ in quads if r >= 0.9 and g <= 0.3 and b <= 0.3) + + assert max_brightness > 0.45, "Text should introduce bright pixels above the overlay" + assert red_pixels > 0, "Red overlay pixels should still be present" + def test_text_fitting_enabled(self): """Test text fitting integration.""" with patch('PIL.Image') as mock_pil_image_module, \ diff --git a/tests/unit/test_version_differentiation.py b/tests/unit/test_version_differentiation.py index dd7f9af..2b57d44 100644 --- a/tests/unit/test_version_differentiation.py +++ b/tests/unit/test_version_differentiation.py @@ -72,45 +72,41 @@ class TestVersionDetection: assert isinstance(limits, dict), "get_version_limits() should return a dictionary" # Check required keys - required_keys = ["max_texture_size", "max_concurrent_operations"] + required_keys = ["max_concurrent_operations"] for key in required_keys: assert key in limits, f"Missing required key '{key}' in version limits" + assert "max_texture_size" not in limits, "Texture size limit should not be enforced" # Check value types - assert isinstance(limits["max_texture_size"], int), "max_texture_size should be an integer" assert isinstance(limits["max_concurrent_operations"], int), "max_concurrent_operations should be an integer" # Check version-specific limits if VERSION_TYPE == "free": - assert limits["max_texture_size"] == 1024, f"Free version should have 1024px limit, got {limits['max_texture_size']}" assert limits["max_concurrent_operations"] == 1, f"Free version should have 1 concurrent operation, got {limits['max_concurrent_operations']}" else: - assert limits["max_texture_size"] == 4096, f"Full version should have 4096px limit, got {limits['max_texture_size']}" assert limits["max_concurrent_operations"] == 4, f"Full version should have 4 concurrent operations, got {limits['max_concurrent_operations']}" def test_get_version_limits_full_returns_pro_limits(self): - """Verify full version returns correct limits for texture size and concurrent operations.""" + """Verify full version returns correct limits for concurrent operations.""" from src.utils.constants import get_version_limits from unittest.mock import patch with patch('src.utils.constants.VERSION_TYPE', 'full'): limits = get_version_limits() - assert limits["max_texture_size"] == 4096, \ - f"Expected full version max_texture_size=4096, got {limits['max_texture_size']}" + assert "max_texture_size" not in limits, "Full version should not enforce a texture size limit" assert limits["max_concurrent_operations"] == 4, \ f"Expected full version max_concurrent_operations=4, got {limits['max_concurrent_operations']}" def test_get_version_limits_free_returns_restricted_limits(self): - """Verify free version returns restricted limits for texture size and concurrent operations.""" + """Verify free version returns restricted limits for concurrent operations only.""" from src.utils.constants import get_version_limits from unittest.mock import patch with patch('src.utils.constants.VERSION_TYPE', 'free'): limits = get_version_limits() - assert limits["max_texture_size"] == 1024, \ - f"Expected free version max_texture_size=1024, got {limits['max_texture_size']}" + assert "max_texture_size" not in limits, "Free version should not enforce a texture size limit" assert limits["max_concurrent_operations"] == 1, \ f"Expected free version max_concurrent_operations=1, got {limits['max_concurrent_operations']}" @@ -278,7 +274,7 @@ class TestVersionDifferentiationScenarios: # Technical limitations limits = get_version_limits() - assert limits["max_texture_size"] == 1024, "Should have 1024px texture limit" + assert "max_texture_size" not in limits, "Free version should not enforce a texture size cap" assert limits["max_concurrent_operations"] == 1, "Should have 1 concurrent operation limit" def test_full_version_complete_scenario(self): @@ -296,5 +292,5 @@ class TestVersionDifferentiationScenarios: # Generous technical limits limits = get_version_limits() - assert limits["max_texture_size"] == 4096, "Should have 4096px texture limit" + assert "max_texture_size" not in limits, "Full version should not enforce a texture size cap" assert limits["max_concurrent_operations"] == 4, "Should have 4 concurrent operations limit"